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
4 changes: 4 additions & 0 deletions ai/ai-samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This repository demonstrates the following capabilities:
* Image Generation
* Video Analysis
* Grounding with Google Search
* Server Prompt Templates

## Setup & Configuration

Expand Down Expand Up @@ -51,6 +52,9 @@ npm run dev:auto-function # Automatic Function Calling
npm run dev:image # Image Generation
npm run dev:video # Video Analysis
npm run dev:grounding # Grounding with Google Search
# Note: Server Prompt Templates requires template setup in the Firebase Console first.
# See src/features/server-prompt-templates/README.md for setup instructions before running:
npm run dev:template # Server Prompt Templates
Comment thread
sedanah-m marked this conversation as resolved.
```

After running any of the above commands, open your browser to http://localhost:*** (provided in the console)
Expand Down
3 changes: 2 additions & 1 deletion ai/ai-samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"dev:auto-function": "VITE_ISOLATED_FEATURE=automatic-function-calling vite",
"dev:image": "VITE_ISOLATED_FEATURE=image-generation vite",
"dev:video": "VITE_ISOLATED_FEATURE=video-analysis vite",
"dev:grounding": "VITE_ISOLATED_FEATURE=grounding-with-google-search vite",
"dev:grounding": "VITE_ISOLATED_FEATURE=grounding-with-google-search vite",
"dev:template": "VITE_ISOLATED_FEATURE=server-prompt-templates vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
Expand Down
1 change: 1 addition & 0 deletions ai/ai-samples/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const NAV_ITEMS = [
{ path: '/image-generation', label: 'Image Generation' },
{ path: '/video-analysis', label: 'Video Analysis' },
{ path: '/grounding-with-google-search', label: 'Grounding with Google Search' },
{ path: '/server-prompt-templates', label: 'Server Prompt Templates' },
];

export default function App() {
Expand Down
57 changes: 57 additions & 0 deletions ai/ai-samples/src/features/server-prompt-templates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Firebase AI Logic: Server Prompt Templates Sample

This quickstart demonstrates how to fetch and execute centrally managed prompt templates stored in the Firebase Console using the Firebase AI Logic SDK.

## Why Server Prompt Templates?

- **No deployment updates**: Change prompts, tune system instructions, or switch underlying models directly in the Firebase Console without having to release a new app version.
- **Security**: Protect against exposing your prompt client-side.

---

## 1. Firebase Console Setup

Before running this sample, create and lock the template in your Firebase project:

1. Open the [Firebase Console](https://console.firebase.google.com/) and select your project.
2. In the left navigation, navigate to **AI Logic > Prompt Templates**.
3. Click **Create Template**. Note that these starter templates provide the format and syntax for some common use cases and this tutorial assumes you've selected the `Input + System Instructions` option. Once you clicked that, configure the following:
- **Template ID**: `invoice-generator`
- **Model**: `gemini-3.5-flash-lite` (or any available Gemini model)
4. In the **Prompt Content** / **System Instructions** text area, paste the following prompt:
```text
{{role "system"}}
All output must be a clearly structured invoice document.
Use a tabular or clearly delineated list format for line items.

{{role "user"}}
Create an example customer invoice for a customer named {{customerName}}.
```
5. Click **Save**.
6. **Important**: While client applications can execute both unlocked (draft) and locked templates during development, you should always **Lock** your template before deploying to production. Locking freezes the prompt configuration, ensuring that subsequent console edits do not accidentally change your production app's behavior.

---

## 2. Running the Sample Locally

Run the sample inside the full app shell:
```bash
npm run dev
```

Or run this feature directly in isolated mode:
```bash
npm run dev:template
```

Open your browser to the local URL (e.g., `http://localhost:***` provided in the console).

---

## 3. Possible Troubleshooting & Failure Surfaces

| Issue | Cause | Resolution |
|---|---|---|
| `NOT_FOUND` / 404 | Template ID mismatch or template doesn't exist in the active Firebase project | Verify that the template ID in your code is exactly `invoice-generator` and that you are initialized in the correct Firebase Project |
| Missing Variable output | Variable names in client code don't match console | Ensure the keys passed to `templateVariables` match the `{{variable}}` placeholders in your prompt template. |
| `PERMISSION_DENIED` | Firebase AI Logic API not enabled or App Check blocked | Follow the Firebase AI Logic guided setup in the console and ensure your API key / App Check tokens are valid. |
148 changes: 148 additions & 0 deletions ai/ai-samples/src/features/server-prompt-templates/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useState } from 'react';
import { generateFromTemplate } from './service';

export default function ServerPromptTemplatesView() {
const [templateId, setTemplateId] = useState('invoice-generator');
const [customerName, setCustomerName] = useState('Jane Doe');

const [response, setResponse] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleExecute = async () => {
const trimmedId = templateId.trim();
if (!trimmedId) {
setError('Please enter a Template ID.');
return;
}

const trimmedCustomerName = customerName.trim();
if (!trimmedCustomerName) {
setError('Please enter a Customer Name.');
return;
}

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

const templateVariables = {
customerName: trimmedCustomerName,
};

try {
const text = await generateFromTemplate(trimmedId, templateVariables);
setResponse(text);
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : 'An error occurred while executing the template.';
setError(message);
} finally {
setLoading(false);
}
};

const isNotFoundError =
error && (error.toLowerCase().includes('not found') || error.toLowerCase().includes('not_found'));

return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>Server Prompt Templates</h2>
<p style={{ color: '#666', marginBottom: '15px' }}>
Centrally manage, test, and update AI prompts in the Firebase Console without redeploying client code.
</p>

{/* Prerequisite Setup Notice */}
<div
style={{
backgroundColor: '#e8f0fe',
border: '1px solid #c2e7ff',
borderRadius: '6px',
padding: '12px 16px',
marginBottom: '20px',
fontSize: '14px',
color: '#174ea6',
}}
>
<strong>Console Prerequisite:</strong> Requires a published/locked template in the Firebase Console.
<strong>Console Prerequisite:</strong> Requires a saved template in the Firebase Console.
See <code>README.md</code> in this feature folder for the setup guide.
</div>

<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Template ID:
</label>
<input
type="text"
value={templateId}
onChange={(e) => setTemplateId(e.target.value)}
placeholder="invoice-generator"
style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
/>
</div>

<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Customer Name (<code>{'{{customerName}}'}</code>):
</label>
<input
type="text"
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
/>
</div>

<button
onClick={handleExecute}
disabled={loading}
style={{
padding: '10px 20px',
cursor: loading ? 'not-allowed' : 'pointer',
backgroundColor: loading ? '#ccc' : '#007BFF',
color: '#fff',
border: 'none',
borderRadius: '4px',
marginBottom: '15px',
}}
>
{loading ? 'Executing Template...' : 'Execute Template'}
</button>

{error && (
<div
style={{
color: '#D8000C',
backgroundColor: '#FFD2D2',
padding: '12px',
marginTop: '15px',
borderRadius: '4px',
fontSize: '14px',
lineHeight: '1.4',
}}
>
<strong>Error:</strong> {error}
{isNotFoundError && (
<div style={{ marginTop: '10px', paddingTop: '8px', borderTop: '1px solid #ffbaba' }}>
<strong>Setup Checklist:</strong>
<ul style={{ margin: '6px 0 0 16px', padding: 0 }}>
<li>Template <code>{templateId}</code> exists in your Firebase project?</li>
<li>Template status is <strong>Locked / Published</strong> (drafts cannot be called by client SDKs)?</li>
<li>Template is saved in the Firebase Console (and locked for production)?</li>
<li>Your web app configuration matches the Firebase project where the template is stored?</li>
</ul>
</div>
)}
</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>
);
}
28 changes: 28 additions & 0 deletions ai/ai-samples/src/features/server-prompt-templates/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getAiTemplateModel } from '../../services/firebaseAIService';

export interface InvoiceTemplateVariables {
customerName: string;
}

/**
* Executes a server-side prompt template in a single unary request.
* Centrally managed templates allow updating prompts and models without redeploying client code.
*
* @param templateId The ID of the locked template in the Firebase Console (e.g. 'invoice-generator')
* @param variables Key-value map matching the {{variables}} declared in the template
* @returns The text string generated by the model
*/
export async function generateFromTemplate(
templateId: string,
variables: Record<string, unknown>
): Promise<string> {
try {
const model = getAiTemplateModel();
const result = await model.generateContent(templateId, variables);
return result.response.text();
} catch (error: unknown) {
throw error instanceof Error
? error
: new Error('An unknown error occurred during template generation.');
}
}
6 changes: 5 additions & 1 deletion ai/ai-samples/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ImageGeneration from './features/image-generation';
import AutomaticFunctionCalling from './features/automatic-function-calling';
import VideoAnalysis from './features/video-analysis';
import GroundingWithGoogleSearch from './features/grounding-with-google-search';
import ServerPromptTemplates from './features/server-prompt-templates';

const router = createBrowserRouter([
{
Expand All @@ -27,9 +28,9 @@ const router = createBrowserRouter([
{ path: 'image-generation', element: <ImageGeneration /> },
{ path: 'video-analysis', element: <VideoAnalysis /> },
{ path: 'grounding-with-google-search', element: <GroundingWithGoogleSearch /> },
{ path: 'server-prompt-templates', element: <ServerPromptTemplates /> },
],
},

]);

const isolatedFeature = import.meta.env.VITE_ISOLATED_FEATURE;
Expand All @@ -55,6 +56,9 @@ const renderContent = () => {
return <VideoAnalysis />;
case 'grounding-with-google-search':
return <GroundingWithGoogleSearch />;
case 'server-prompt-templates':
case 'template':
return <ServerPromptTemplates />;
default:
return <RouterProvider router={router} />;
}
Expand Down
32 changes: 18 additions & 14 deletions ai/ai-samples/src/services/firebaseAIService.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { initializeApp} from 'firebase/app';
import { getAI, getGenerativeModel } from 'firebase/ai';
import { initializeApp } from 'firebase/app';
import { getAI, getGenerativeModel, getTemplateGenerativeModel, RequestOptions } from 'firebase/ai';
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';

const firebaseConfig = import.meta.env.VITE_FIREBASE_CONFIG
? JSON.parse(import.meta.env.VITE_FIREBASE_CONFIG)
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"
};
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);

Expand All @@ -20,9 +20,9 @@ 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.
// 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
});
Expand All @@ -32,4 +32,8 @@ const ai = getAI(app);

export const getAiModel = (modelName: string = 'gemini-3.5-flash-lite', additionalConfig: Record<string, any> = {}) => {
return getGenerativeModel(ai, { model: modelName, ...additionalConfig });
};

export const getAiTemplateModel = (requestOptions?: RequestOptions) => {
return getTemplateGenerativeModel(ai, requestOptions);
};
Loading