Skip to content
Closed
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
28 changes: 4 additions & 24 deletions ai/ai-samples/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
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';
import AutomaticFunctionCallingView from './features/automatic-function-calling';

const NAV_ITEMS = [
{ path: '/text-generation', label: 'Text Generation' },
Expand All @@ -15,35 +8,22 @@ const NAV_ITEMS = [
{ path: '/function-calling', label: 'Function Calling' },
{ path: '/automatic-function-calling', label: 'Automatic Function Calling' },
{ path: '/image-generation', label: 'Image Generation' },

];
Comment on lines 10 to 12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The new Video Analysis feature is missing from the sidebar navigation items. Add it to NAV_ITEMS so users can navigate to it.

Suggested change
{ path: '/image-generation', label: 'Image Generation' },
];
{ path: '/image-generation', label: 'Image Generation' },
{ path: '/video-analysis', label: 'Video Analysis' },
];


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 'automatic-function-calling': return <AutomaticFunctionCallingView />;
case 'image-generation': return <ImageGenerationView />;
}
}

// Otherwise, return the multi-feature app shell layout

return (
<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}
<Link
to={path}
className={`nav-link ${pathname === path ? 'nav-link-active' : ''}`}
>
{label}
Expand Down
23 changes: 8 additions & 15 deletions ai/ai-samples/src/features/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,37 @@ export default function ChatView() {
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 = () => {
const handleResetChat = React.useCallback(() => {
try {
chatSessionRef.current = startNewChat();
setError(null);
} catch (err: any) {
setError(err.message || 'Failed to initialize chat session. Please check your Firebase configuration.');
} catch (err: unknown) {
const errorMessage = err instanceof Error
? err.message
: 'Failed to initialize chat session. Please check your Firebase configuration.';
setError(errorMessage);
}
setMessages([]);
setInput('');
};
}, []);

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

const userMessage = input.trim();
setInput(''); // Clear input immediately
setInput('');
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.');
Expand Down
109 changes: 85 additions & 24 deletions ai/ai-samples/src/features/text-generation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,110 @@
import { useState } from 'react';
import { generateText } from './service';
import { generateText, streamText } from './service';

export default function TextGeneration() {
export default function TextGenerationView() {
const [prompt, setPrompt] = useState<string>('');
const cleanPrompt = prompt.trim();
const [systemInstruction, setSystemInstruction] = useState<string>('');
const [useStreaming, setUseStreaming] = useState<boolean>(true);
const [response, setResponse] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);

const handleGenerate = async () => {
if (!prompt.trim()) return;
if (!cleanPrompt) 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');
const cleanInstruction = systemInstruction.trim() || undefined;

if (useStreaming) {
await streamText(
cleanPrompt,
(chunk) => setResponse((prev) => prev + chunk),
cleanInstruction
);
} else {
const text = await generateText(cleanPrompt, cleanInstruction);
setResponse(text);
}
} catch (err: unknown) {
const errorMessage = err instanceof Error
? err.message
: 'An unexpected error occurred';

setError(errorMessage);
} finally {
setLoading(false);
}
};


return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>Text Generation</h2>

<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Ask the AI a question..."
rows={5}
style={{ width: '100%', marginBottom: '10px', padding: '10px' }}
/>

<button onClick={handleGenerate} disabled={loading || !prompt.trim()} style={{ padding: '10px 20px' }}>
{loading ? 'Generating...' : 'Generate'}
</button>

{error && <p style={{ color: 'red', marginTop: '15px' }}>{error}</p>}
{response && <div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f0f0f0' }}><p style={{ whiteSpace: 'pre-wrap' }}>{response}</p></div>}

{/* System Instruction Input */}
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#5f6368' }}>
System Instruction (Persona/Rules)
</label>
<input
type="text"
value={systemInstruction}
onChange={(e) => setSystemInstruction(e.target.value)}
placeholder="e.g., You are a helpful assistant..."
disabled={loading}
style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
/>
</div>

{/* Main Prompt Input */}
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#5f6368' }}>
Prompt
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Ask the AI a question..."
rows={5}
disabled={loading}
style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
/>
</div>

{/* Controls: Checkbox and Button */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={useStreaming}
onChange={(e) => setUseStreaming(e.target.checked)}
disabled={loading}
/>
Stream response
</label>

<button
onClick={handleGenerate}
disabled={loading || !cleanPrompt}
style={{ padding: '10px 20px', cursor: (loading || !cleanPrompt) ? 'not-allowed' : 'pointer' }}
>
{loading ? 'Generating...' : 'Generate'}
</button>
</div>

{/* Error Message */}
{error && <p style={{ color: '#c5221f', marginTop: '15px' }}>{error}</p>}

{/* Response Box */}
{response && (
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f0f0f0', borderRadius: '8px' }}>
<p style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{response}</p>
</div>
)}
</div>
);
}
35 changes: 11 additions & 24 deletions ai/ai-samples/src/features/text-generation/service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { getAiModel } from '../../services/firebaseAIService';

/**
* Generates text from a text prompt.
* Generates text from a text prompt, optionally using a system instruction.
* @param prompt The string instruction sent to the model.
* @param systemInstruction (Optional) The persona or constraints for the model.
* @returns The text string response generated by the model.
*/
export async function generateText(prompt: string): Promise<string> {
export async function generateText(prompt: string, systemInstruction?: string): Promise<string> {
try {
const model = getAiModel('gemini-3.5-flash');
const options = systemInstruction ? { systemInstruction } : undefined;
const model = getAiModel('gemini-3.5-flash', options);

const result = await model.generateContent(prompt);

return result.response.text();
} catch (error) {
console.error('Error generating text with Firebase AI:', error);
Expand All @@ -21,10 +23,13 @@ export async function generateText(prompt: string): Promise<string> {
* 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.
* @param systemInstruction (Optional) The persona or constraints for the model.
*/
export async function streamText(prompt: string, onChunk: (chunk: string) => void): Promise<void> {
export async function streamText(prompt: string, onChunk: (chunk: string) => void, systemInstruction?: string): Promise<void> {
try {
const model = getAiModel('gemini-3.5-flash');
const options = systemInstruction ? { systemInstruction } : undefined;
const model = getAiModel('gemini-3.5-flash', options);

const result = await model.generateContentStream(prompt);

for await (const chunk of result.stream) {
Expand All @@ -37,22 +42,4 @@ export async function streamText(prompt: string, onChunk: (chunk: string) => voi
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<string> {
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;
}
}
Loading
Loading