Skip to content
Open
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
37 changes: 22 additions & 15 deletions ai/ai-samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ This repository demonstrates the following capabilities:
* Multimodal
* Structured Output
* Function Calling
* Automatic Function Calling
* Image Generation
* Grounding with Google Search

## Setup & Configuration

Expand All @@ -21,6 +23,7 @@ To connect this sample app to your Firebase project, register a new Web App in y
1. Navigate to this directory and install dependencies:
```bash
npm install
```

2. Add your Firebase config

Expand All @@ -31,34 +34,38 @@ Copy the example config file and fill in your project values. Open src/config/fi
3. Running the samples

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

To run indivual features in isolated mode, run the single feature directly without the app shell using one of these scripts:
To run individual 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
```bash
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:auto-function # Automatic Function Calling
npm run dev:image # Image Generation
npm run dev:grounding # Grounding with Google Search
Comment thread
sedanah-m marked this conversation as resolved.
```

After running any of the above commands, open your browser to https://localhost:*** (provided in the console)
After running any of the above commands, open your browser to http://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.
App check protects your API Key from unauthorized use. It is not required to run the samples locally but highly recommended before deploying to production.

Debug token:
firebaseAIService.ts includes App Check intilization for local development. To enable it:
firebaseAIService.ts includes App Check initialization 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.
2. On the first run, a debug 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

Expand All @@ -69,4 +76,4 @@ For production, use reCAPTCHA v3 as the App Check provider:
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.
See the [App Check Docs](https://firebase.google.com/docs/app-check/web/recaptcha-provider) for full instructions.
1 change: 1 addition & 0 deletions ai/ai-samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dev:function": "VITE_ISOLATED_FEATURE=function-calling vite",
"dev:auto-function": "VITE_ISOLATED_FEATURE=automatic-function-calling vite",
"dev:image": "VITE_ISOLATED_FEATURE=image-generation vite",
"dev:grounding": "VITE_ISOLATED_FEATURE=grounding-with-google-search vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
Expand Down
3 changes: 1 addition & 2 deletions ai/ai-samples/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ const NAV_ITEMS = [
{ path: '/function-calling', label: 'Function Calling' },
{ path: '/automatic-function-calling', label: 'Automatic Function Calling' },
{ path: '/image-generation', label: 'Image Generation' },

{ path: '/grounding-with-google-search', label: 'Grounding with Google Search' },
];

export default function App() {
const { pathname } = useLocation();


return (
<div className="app-shell">
<nav className="sidebar">
Expand Down
140 changes: 140 additions & 0 deletions ai/ai-samples/src/features/grounding-with-google-search/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { useState, useRef, useEffect } from 'react';
import { generateGroundedContent, GroundedResult } from './service';

/**
* Encapsulates the Google Search suggestions HTML/CSS within a Shadow DOM
* as documented in Firebase's SearchEntrypoint reference:
* container.attachShadow({ mode: 'open' }).innerHTML = renderedContent;
* (https://firebase.google.com/docs/reference/js/ai.searchentrypoint)
*/
function SearchSuggestionsWidget({ renderedContent }: { renderedContent: string }) {
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!containerRef.current) return;
const shadowRoot =
containerRef.current.shadowRoot ||
containerRef.current.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = renderedContent;
}, [renderedContent]);

return <div ref={containerRef} style={{ minHeight: '40px' }} />;
}

export default function GroundingWithGoogleSearchView() {
const [prompt, setPrompt] = useState('Who won the euro 2024?');
const [result, setResult] = useState<GroundedResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleGenerate = async () => {
const cleanedPrompt = prompt.trim();

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

setLoading(true);
setError(null);
setResult(null);

try {
const data = await generateGroundedContent(cleanedPrompt);
setResult(data);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'An error occurred during grounded generation.';
setError(message);
} finally {
setLoading(false);
}
};

const renderedContent = result?.groundingMetadata?.searchEntryPoint?.renderedContent;

const uniqueSources = Array.from(
new Map(
(result?.groundingMetadata?.groundingChunks ?? [])
.filter((chunk): chunk is { web: { uri: string; title?: string } } => Boolean(chunk.web?.uri))
.map((chunk) => [chunk.web.uri, chunk.web])
).values()
);

return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>Grounding with Google Search</h2>
<p style={{ color: '#666', marginBottom: '20px' }}>
Connects Gemini to real-time Google Search to provide up-to-date answers and sources.
</p>

<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 ? 'Searching & Generating...' : 'Generate Grounded Content'}
</button>

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

{result && (
<div style={{ marginTop: '20px', borderTop: '1px solid #eee', paddingTop: '15px' }}>
<h3>Response:</h3>
<p style={{ whiteSpace: 'pre-wrap', lineHeight: '1.5' }}>{result.text}</p>

{/* REQUIRED COMPLIANCE: Display Google Search suggestions in Shadow DOM if returned */}
{renderedContent && (
<div style={{ marginTop: '15px' }}>
<h4>Search Suggestions:</h4>
<SearchSuggestionsWidget renderedContent={renderedContent} />
</div>
)}

{/* REQUIRED COMPLIANCE: Display sources */}
{uniqueSources.length > 0 && (
<div style={{ marginTop: '15px' }}>
<h4>Sources:</h4>
<ul style={{ paddingLeft: '20px' }}>
{uniqueSources.map((source) => (
<li key={source.uri} style={{ marginBottom: '4px' }}>
<a
href={source.uri}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#007BFF' }}
>
{source.title || source.uri}
</a>
</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
);
}
33 changes: 33 additions & 0 deletions ai/ai-samples/src/features/grounding-with-google-search/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getAiModel } from '../../services/firebaseAIService';
import { GroundingMetadata } from 'firebase/ai';

export interface GroundedResult {
text: string;
groundingMetadata?: GroundingMetadata;
}

/**
* Generates grounded content using the Google Search tool.
* Connects the Gemini model to real-time web content for up-to-date answers,
* sources (groundingChunks), and compliant search suggestions (searchEntryPoint).
* @param prompt The string question or instruction sent to the model.
* @returns The text response and grounding metadata.
*/
export async function generateGroundedContent(prompt: string): Promise<GroundedResult> {
try {
const model = getAiModel('gemini-3.7-flash', {
tools: [{ googleSearch: {} }],
});

const result = await model.generateContent(prompt);
const text = result.response.text();
const groundingMetadata = result.response.candidates?.[0]?.groundingMetadata;

return { text, groundingMetadata };
} catch (error: unknown) {
console.error('Error generating grounded content with Firebase AI:', error);
throw error instanceof Error
? error
: new Error('An unknown error occurred during generation.');
}
}
8 changes: 6 additions & 2 deletions ai/ai-samples/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import StructuredOutput from './features/structured-output';
import FunctionCalling from './features/function-calling';
import ImageGeneration from './features/image-generation';
import AutomaticFunctionCalling from './features/automatic-function-calling';

import GroundingWithGoogleSearch from './features/grounding-with-google-search';

const router = createBrowserRouter([
{
Expand All @@ -24,7 +24,7 @@ const router = createBrowserRouter([
{ path: 'function-calling', element: <FunctionCalling /> },
{ path: 'automatic-function-calling', element: <AutomaticFunctionCalling /> },
{ path: 'image-generation', element: <ImageGeneration /> },

{ path: 'grounding-with-google-search', element: <GroundingWithGoogleSearch /> },
],
},
]);
Expand All @@ -46,6 +46,10 @@ const renderContent = () => {
return <FunctionCalling />;
case 'image-generation':
return <ImageGeneration />;
case 'automatic-function-calling':
return <AutomaticFunctionCalling />;
case 'grounding-with-google-search':
return <GroundingWithGoogleSearch />;
default:
return <RouterProvider router={router} />;
}
Expand Down
Loading