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 */} +
+ setInput(e.target.value)} + placeholder="Type your message..." + disabled={loading} + style={{ + flex: 1, + padding: '12px', + borderRadius: '8px', + border: '1px solid #dadce0', + fontSize: '1rem', + }} + /> + +
); -} +} \ 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

+ +