From 7d65a2499671df9f0157bb3ec2a8f82d6302f572 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Fri, 10 Apr 2026 21:34:59 -0400 Subject: [PATCH] feat: Add hybrid inference and structured output support to sample app - Added InferenceMode controls in MainLayout and ChatView. - Added JSON Schema textarea in RightSidebar and wired it to responseJsonSchema. - Updated available model names to Gemini 2.5 series. --- ai/ai-react-app/README.md | 8 ++ .../src/components/Layout/MainLayout.tsx | 11 ++ .../src/components/Layout/RightSidebar.tsx | 136 ++++++++++++++++++ .../src/services/firebaseAIService.ts | 11 +- ai/ai-react-app/src/views/ChatView.tsx | 42 ++++-- 5 files changed, 194 insertions(+), 14 deletions(-) diff --git a/ai/ai-react-app/README.md b/ai/ai-react-app/README.md index 972eecfd7..7ec59b938 100644 --- a/ai/ai-react-app/README.md +++ b/ai/ai-react-app/README.md @@ -24,6 +24,14 @@ For more information about the Firebase AI SDK, see the [Firebase AI Logic Docs] yarn dev ``` +## Hybrid Mode (On-Device Inference) + +This sample supports Hybrid Mode, which allows falling back to on-device inference using Chrome's Prompt API when available. + +To use Hybrid Mode: +1. Enable the "Prompt API" in Chrome. See the [Chrome AI Prompt API documentation](https://developer.chrome.com/docs/ai/prompt-api) for instructions on how to enable it and download the required model (Gemini Nano). +2. Toggle "Hybrid Mode" in the right sidebar of the application. + ## Support - [Firebase Support](https://firebase.google.com/support/) diff --git a/ai/ai-react-app/src/components/Layout/MainLayout.tsx b/ai/ai-react-app/src/components/Layout/MainLayout.tsx index 215ae4d37..3a5f17901 100644 --- a/ai/ai-react-app/src/components/Layout/MainLayout.tsx +++ b/ai/ai-react-app/src/components/Layout/MainLayout.tsx @@ -15,6 +15,7 @@ import { GoogleAIBackend, getAI, ResponseModality, + InferenceMode, } from "firebase/ai"; import { AVAILABLE_GENERATIVE_MODELS, @@ -51,6 +52,8 @@ const MainLayout: React.FC = ({ responseModalities: [ResponseModality.TEXT, ResponseModality.IMAGE], }, }); + const [isHybridMode, setIsHybridMode] = useState(false); + const [inferenceMode, setInferenceMode] = useState(InferenceMode.PREFER_ON_DEVICE); const [selectedAspectRatio, setSelectedAspectRatio] = useState(); const [usageMetadata, setUsageMetadata] = useState( @@ -111,6 +114,8 @@ const MainLayout: React.FC = ({ onUsageMetadataChange={setUsageMetadata} currentParams={generativeParams} activeMode={activeMode} + isHybridMode={isHybridMode} + inferenceMode={inferenceMode} /> ); case "nanobanana": @@ -129,6 +134,8 @@ const MainLayout: React.FC = ({ onUsageMetadataChange={setUsageMetadata} currentParams={generativeParams} activeMode={activeMode} + isHybridMode={isHybridMode} + inferenceMode={inferenceMode} /> ); } @@ -160,6 +167,10 @@ const MainLayout: React.FC = ({ setNanoBananaParams={setNanoBananaParams} selectedAspectRatio={selectedAspectRatio} setSelectedAspectRatio={setSelectedAspectRatio} + isHybridMode={isHybridMode} + setIsHybridMode={setIsHybridMode} + inferenceMode={inferenceMode} + setInferenceMode={setInferenceMode} /> diff --git a/ai/ai-react-app/src/components/Layout/RightSidebar.tsx b/ai/ai-react-app/src/components/Layout/RightSidebar.tsx index cfa4fbeed..c927bd3f9 100644 --- a/ai/ai-react-app/src/components/Layout/RightSidebar.tsx +++ b/ai/ai-react-app/src/components/Layout/RightSidebar.tsx @@ -15,6 +15,7 @@ import { FunctionCallingMode, UsageMetadata, ResponseModality, + InferenceMode, } from "firebase/ai"; export interface ExtendedGenerationConfig extends GenerationConfig { @@ -34,6 +35,10 @@ interface RightSidebarProps { setNanoBananaParams: React.Dispatch>; selectedAspectRatio?: string; setSelectedAspectRatio: (ar?: string) => void; + isHybridMode: boolean; + setIsHybridMode: React.Dispatch>; + inferenceMode: InferenceMode; + setInferenceMode: React.Dispatch>; } const RightSidebar: React.FC = ({ @@ -45,7 +50,58 @@ const RightSidebar: React.FC = ({ setNanoBananaParams, selectedAspectRatio, setSelectedAspectRatio, + isHybridMode, + setIsHybridMode, + inferenceMode, + setInferenceMode, }) => { + const [schemaText, setSchemaText] = React.useState( + JSON.stringify(generativeParams.generationConfig?.responseJsonSchema || {}, null, 2) + ); + const [modelStatus, setModelStatus] = React.useState("unknown"); + + React.useEffect(() => { + setSchemaText( + JSON.stringify(generativeParams.generationConfig?.responseJsonSchema || {}, null, 2) + ); + }, [generativeParams.generationConfig?.responseJsonSchema]); + + React.useEffect(() => { + if (isHybridMode) { + checkModelAvailability(); + } + }, [isHybridMode]); + + const checkModelAvailability = async () => { + setModelStatus("checking"); + try { + const ai = (window as any).LanguageModel; + if (!ai) { + setModelStatus("unavailable"); + return; + } + const availability = await ai.availability(); + setModelStatus(availability); + } catch (err) { + console.error("Error checking model availability:", err); + setModelStatus("error"); + } + }; + + const handleDownloadModel = async () => { + setModelStatus("downloading"); + try { + const ai = (window as any).LanguageModel; + if (ai) { + await ai.create(); + setModelStatus("available"); + } + } catch (err) { + console.error("Error downloading model:", err); + setModelStatus("error"); + } + }; + const handleModelParamsUpdate = ( updateFn: (prevState: ModelParams) => ModelParams, ) => { @@ -154,6 +210,8 @@ const RightSidebar: React.FC = ({ if (checked) { // Turn ON JSON nextState.generationConfig.responseMimeType = "application/json"; + nextState.generationConfig.responseJsonSchema = { type: "object", properties: {} }; // Default schema + nextState.generationConfig.responseSchema = undefined; // Turn OFF Function Calling by clearing its related fields nextState.generationConfig.responseSchema = undefined; @@ -162,6 +220,7 @@ const RightSidebar: React.FC = ({ } else { // Turn OFF JSON nextState.generationConfig.responseMimeType = undefined; + nextState.generationConfig.responseJsonSchema = undefined; nextState.generationConfig.responseSchema = undefined; } } else if (name === "function-call-toggle") { @@ -178,6 +237,7 @@ const RightSidebar: React.FC = ({ // Turn OFF JSON mode by clearing its related fields nextState.generationConfig.responseMimeType = undefined; + nextState.generationConfig.responseJsonSchema = undefined; nextState.generationConfig.responseSchema = undefined; } else { // Turn OFF Function Calling @@ -191,6 +251,7 @@ const RightSidebar: React.FC = ({ // Turn OFF JSON mode and Function Calling nextState.generationConfig.responseMimeType = undefined; + nextState.generationConfig.responseJsonSchema = undefined; nextState.generationConfig.responseSchema = undefined; nextState.toolConfig = undefined; } else { @@ -342,6 +403,54 @@ const RightSidebar: React.FC = ({
Tools
+
+ + +
+ {isHybridMode && ( + <> +
+ To use on-device inference, ensure you have enabled the Prompt API in Chrome and downloaded the model. + See Chrome AI Prompt API for details. +
+
+ +
+ {modelStatus} + {modelStatus === "downloadable" && ( + + )} + {modelStatus === "downloading" && ( + // Using an emoji as a simple spinner + )} +
+
+
+ + +
+ + )}
@@ -364,6 +473,33 @@ const RightSidebar: React.FC = ({ >
+ {isStructuredOutputActive && ( +
+ +