本教學將示範如何使用 Angular、Firebase AI Logic 與 Gemini 打造混合式影像分析應用程式。使用者只需上傳圖片、點擊按鈕,並等待 Gemini Nano 或 Gemini 3.5 Flash 模型產生標籤、替代文字(Alt-text)以及推薦建議。
Firebase AI Logic 的強大之處在於其混合與裝置端網頁 SDK(Hybrid and On-device Web SDK)能自動決定要使用裝置端本機或是雲端 AI 模型。在此範例中,當在 Chrome 瀏覽器中執行時,SDK 會下載並使用 Gemini Nano。當在其他瀏覽器中執行時,則會由 Gemini 3.5 Flash 模型處理影像分析任務。底層所使用的模型對使用者而言完全是透明的,且不論後端為何,都能獲得高品質的影像分析回應。
雖然 Angular 和 CSS 提供了使用者介面與樣式,但此範例的 AI 功能是由 Firebase 服務(例如 Firebase AI Logic 和 Firebase Remote Config)所提供。
npm i -g firebase-tools
使用 npm 全域安裝 firebase-tools。
npm i --save-exact firebase tailwindcss postcss @tailwindcss/postcss jsonrepair
npm i --save-exact --save-dev angular-eslint firebase-tools husky lint-staged serve
安裝 Firebase AI Logic、CSS 樣式以及 JSON 回應串流所需的其他依賴套件。安裝開發依賴套件(dev dependencies)是為了偵測程式碼異味(code smells)、確保程式碼品質,並呼叫 Firebase CLI 執行特定任務。
此專案的完整原始碼已託管於 NG Firebase Image Analyzer;然而,以下章節將說明 Firebase 專案的設定,並提供 Firebase 混合與裝置端網頁 SDK 的程式碼範例。
以下章節將逐步引導您進行 Firebase 專案設定。
在任何 Angular 服務向 Firebase AI Logic 發送請求以執行 AI 任務之前,必須先在 Angular 專案中設定好 Firebase 專案。
firebase logout
firebase login
注意: 建議在設定新專案之前,先登出並重新登入 Firebase 以重新整理驗證認證。
建立一個 firebase 資料夾來儲存 .firebasesrc 與 .firebase.json 這兩個 Firebase 設定檔。您可以使用 Antigravity CLI 與 Gemini 在該資料夾中產生這些設定檔。Antigravity CLI 是 Google 開發的編碼代理程式(coding agent),在命令列中使用 AI 模型來解決複雜問題。您可以在此頁面找到相關資訊。
在我的提示詞中,我指示了 AI 模型預設的 Firebase 專案 ID 以及要安裝的服務。此範例利用了 Firebase AI Logic、用於生產環境部署的 App Hosting,以及用於模擬託管的 Firebase Local Emulator Suite,以此加速開發週期。
以下是一個非常簡單的 firebase/.firebasesrc:
{
"projects": {
"default": "<Firebase Project ID>"
}
}
以下是完整的設定檔 firebase/firebase.json:
{
"apphosting": [
{
"backendId": "ng-firebase-image-analyzer",
"rootDir": "..",
"ignore": ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log", "functions"]
}
],
"emulators": {
"apphosting": {
"port": 5005,
"rootDirectory": "..",
"startCommand": "npm run start"
},
"ui": {
"enabled": true
}
}
}
該 apphosting 區塊顯示了 Angular 程式碼的根目錄位於父資料夾,並在排除上傳的清單中忽略了一系列資料夾與檔案。
該 emulators 區塊啟用了 App Hosting 模擬,Angular 應用程式會執行在連接埠 5005,URL 為 http://localhost:5005。
以下是簡單的 apphosting.yaml:
runConfig:
minInstances: 0
maxInstances: 2
此 YAML 檔案將活動中執行個體限制在 0 到 2 之間,以防止因無限擴充而產生預期之外的雲端成本。
接下來,建立一個新的 firebase/scripts 資料夾來儲存 Node.js 指令碼,以利 Firebase 與 Angular 的整合。
mkdir -p firebase/scripts
我並非手動從 Firebase 控制台下載預設的 Firebase Remote Config JSON 檔案,而是指示 Antigravity CLI 與 Gemini 下載 Firebase Remote Config 數值、將回應轉換為 JSON 鍵值對(key-value pairs),然後將檔案寫入至 ../public/remote-config-defaults.json。如此一來,Angular 應用程式便可在開發期間與生產環境建置中,透過程式碼存取該 JSON 檔案。
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const firebaseDir = path.resolve(__dirname, '..');
const output = execSync('npx firebase remoteconfig:get --json --project default', {
encoding: 'utf-8',
cwd: firebaseDir,
});
const config = JSON.parse(output);
const parameters = config?.result?.parameters;
const defaults = {};
if (parameters) {
for (const [key, paramObj] of Object.entries(parameters)) {
if (paramObj.defaultValue && paramObj.defaultValue.value !== undefined) {
defaults[key] = paramObj.defaultValue.value;
}
}
}
const outputPath = path.join(firebaseDir, '..', 'public', 'remote-config-defaults.json');
fs.writeFileSync(outputPath, `${JSON.stringify(defaults, null, 2)}\n`, 'utf-8');
console.log(`Successfully wrote ${outputPath}`);
在 package.json 中新增指令碼來執行該 Node.js 檔案。
"scripts": {
"config:fetch": "node firebase/scripts/get-firebase-remote-config.js"
}
輸出結果如下所示:
{
"geminiModelName": "gemini-3.5-flash",
"vertexAILocation": "global",
"thinkingLevel": "LOW"
}
完整程式碼可在這個原始碼檔案中找到。
Angular 應用程式在向 Firebase AI Logic 發送任何請求之前,必須先在啟動(bootstrap)過程中初始化 Firebase App 與 App Check。我們使用 Node.js 指令碼從 .env 讀取並產生 Firebase App 設定物件、reCAPTCHA Enterprise 金鑰與 App Check 偵錯權杖(Debug Token),然後將其寫入 public/firebase.config.json。我已將此檔案加入至 .gitignore 中,因為 Recaptcha Enterprise 金鑰與 App Check 偵錯權杖都是高度敏感的資訊,絕不能外洩。
複製 .env.example 並重新命名為 .env,然後填入有效資訊。
FIREBASE_API_KEY="<Firebase API Key>"
FIREBASE_AUTH_DOMAIN="<Firebase Auth Domain>"
FIREBASE_PROJECT_ID="<Firebase Project ID>"
FIREBASE_STORAGE_BUCKET="<Firebase Storage Bucket>"
FIREBASE_MESSAGING_SENDER_ID="<Firebase Messaging Sender ID>"
FIREBASE_APP_ID="<Firebase App ID>"
FIREBASE_RECAPTCHA_ENTERPRISE_KEY="<Recaptcha Enterprise Key>"
FIREBASE_APPCHECK_DEBUG_TOKEN="<App Check debug token>"
const fs = require('fs');
const path = require('path');
const envPath = path.resolve(__dirname, '../.env');
process.loadEnvFile(envPath);
const app = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID,
};
// Check if recaptcha enterprise key is missing
if (!process.env.FIREBASE_RECAPTCHA_ENTERPRISE_KEY) {
console.warn('Warning: Recaptcha Enterprise key is missing');
}
const config = {
app,
recaptchaEnterpriseKey: process.env.FIREBASE_RECAPTCHA_ENTERPRISE_KEY,
appCheckDebugToken: process.env.FIREBASE_APPCHECK_DEBUG_TOKEN || '',
};
const outputPath = path.resolve(__dirname, '../../public/firebase.config.json');
fs.writeFileSync(outputPath, JSON.stringify(config, null, 2), 'utf-8');
console.log(`Successfully generated ${outputPath} from .env`);
在 package.json 中新增指令碼來執行該 Node.js 檔案。
"scripts": {
"config:generate": "node firebase/scripts/generate-firebase-config.js"
}
JSON 物件如下所示:
{
"app": {
"apiKey": "firebase api key",
"authDomain": "auth domain",
"projectId": "project key",
"storageBucket": "storage bucket",
"messagingSenderId": "message sender id",
"appId": "app id"
},
"recaptchaEnterpriseKey": "<Recaptcha key>",
"appCheckDebugToken": "<app check debug key>"
}
完整程式碼可在這個原始碼檔案中找到。
App Hosting 是在 Firebase 主控台中進行設定。
asia-east1)。.env 中的環境變數相同。 環境變數螢幕截圖
Angular 應用程式會初始化 Firebase App,並搭配雲端架構的 Gemini 模型、裝置端參數、雲端端參數、系統提示詞以及安全設定來建構出 HybridParams。接著,使用者上傳影像與使用者提示詞,指示模型執行影像分析以產生回應。
Firebase AI Logic Hybrid 與 On-device Web SDK 會判斷瀏覽器是否支援 Gemini Nano 來處理影像轉文字(image-to-text)任務。如果支援 Gemini Nano,便會使用它在裝置端分析影像以產生標籤、替代文字和推薦建議。否則,該請求將會被發送至雲端,並由 Gemini 3.5 Flash 模型進行處理。

接下來,我們將準備好透過初始化 Firebase 應用程式、獲取 Remote Config,以及透過 App Check 啟用安全性功能,將 Firebase 整合至 Angular。
Angular 會在啟動(bootstrap)期間初始化 Firebase 應用程式。我們宣告幾個相依注入權杖(Injection Tokens)以注入 navigator、window 和 Firebase AI 執行個體。接著,我們定義封裝了 Firebase SDK 的 Angular 服務,以初始化 Firebase App、獲取並啟用 Firebase Remote Config 數值,並透過 App Check 保護 AI 資源免受濫用。
import { isPlatformBrowser } from '@angular/common';
import { inject, InjectionToken, PLATFORM_ID } from '@angular/core';
import { AI } from 'firebase/ai';
export const FIREBASE_AI = new InjectionToken<AI>('FirebaseAI');
export const WINDOW = new InjectionToken<Window | null>('WINDOW', {
providedIn: 'root',
factory: () => {
const platformId = inject(PLATFORM_ID);
return isPlatformBrowser(platformId) ? window : null;
},
});
export const NAVIGATOR = new InjectionToken<Navigator | null>('NAVIGATOR', {
providedIn: 'root',
factory: () => {
const win = inject(WINDOW);
return win ? win.navigator : null;
},
});
當執行平台為瀏覽器時,處理工廠函式(factory functions)會分別傳回 Window 與 Navigator 物件。
import { FIREBASE_AI } from '@/features/ai/constants/ai.const';
import { ConfigService } from '@/features/ai/services/config.service';
import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';
import { getAI, VertexAIBackend } from 'firebase/ai';
import { getValue } from 'firebase/remote-config';
export function provideFirebaseAI(): EnvironmentProviders {
return makeEnvironmentProviders([
{
provide: FIREBASE_AI,
useFactory: () => {
const configService = inject(ConfigService);
const location = getValue(configService.RemoteConfig, 'vertexAILocation').asString() || 'global';
return getAI(configService.firebaseApp, {
backend: new VertexAIBackend(location),
});
},
},
]);
}
此 provideFirebaseAI 提供者(provider)使用 FIREBASE_AI 注入權杖來建構以 Vertex AI 作為後端的 Firebase AI。在此架構中之所以採用 Vertex AI,是因為其端點(endpoints)可在香港等地區直接存取,而標準的 Gemini API 則需要 VPN。
ConfigService 的核心方法是 initialize(),其會初始化 Firebase App、設定 App Check 執行個體,並獲取與啟用 Firebase Remote Config 數值。
import { inject, InjectionToken, isDevMode } from '@angular/core';
import { WINDOW, NAVIGATOR } from '@/core/constants/navigator.const';
export const LOCAL_DOMAINS = new InjectionToken<string[]>('LOCAL_DOMAINS', {
providedIn: 'root',
factory: () => ['localhost', '127.0.0.1', '::1', '[::1]'],
});
export function injectIsLocalhost(): () => boolean {
const win = inject(WINDOW);
const localDomains = inject(LOCAL_DOMAINS);
return () => !!win && localDomains.includes(win.location.hostname);
}
export function configureAppCheckDebugToken(configToken?: string, isLocalhost?: boolean): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).FIREBASE_APPCHECK_DEBUG_TOKEN = configToken || isDevMode() || !!isLocalhost;
}
injectIsLocalhost 建立並傳回一個高階函式(higher-order function),用於判斷應用程式是否在 localhost 本地端執行。
export function injectOnlineStatus(): () => boolean {
const navigator = inject(NAVIGATOR);
return () => navigator?.onLine ?? true;
}
另一方面,injectOnlineStatus 則傳回一個高階函式,用以判斷應用程式目前是在線上(online)還是離線(offline)狀態執行。
import firebaseConfig from '@/public/firebase.config.json';
import remoteConfigDefaults from '@/public/remote-config-defaults.json';
import { isDevMode, Service } from '@angular/core';
import { FirebaseApp, FirebaseOptions, initializeApp } from 'firebase/app';
import { AppCheck, initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';
import { fetchAndActivate, getRemoteConfig, RemoteConfig } from 'firebase/remote-config';
@Service()
export class ConfigService {
#app: FirebaseApp | undefined = undefined;
#remoteConfig: RemoteConfig | undefined = undefined;
#isOnline = injectOnlineStatus();
#isLocalhost = injectIsLocalhost();
protected initializeAppCheckInstance(app: FirebaseApp, key: string): AppCheck {
return initializeAppCheck(app, {
provider: new ReCaptchaEnterpriseProvider(key),
isTokenAutoRefreshEnabled: true,
});
}
protected setupRemoteConfig(app: FirebaseApp): RemoteConfig {
const rc = getRemoteConfig(app);
rc.defaultConfig = remoteConfigDefaults;
rc.settings.minimumFetchIntervalMillis = isDevMode() ? 0 : 3600000;
return rc;
}
async initialize(): Promise<void> {
this.#app = this.initializeApp(firebaseConfig.app);
const isOnline = this.#isOnline();
const isLocalhost = this.#isLocalhost();
if (isOnline && firebaseConfig.recaptchaEnterpriseKey) {
configureAppCheckDebugToken(firebaseConfig.appCheckDebugToken, isLocalhost);
this.initializeAppCheckInstance(this.#app, firebaseConfig.recaptchaEnterpriseKey);
}
this.#remoteConfig = this.setupRemoteConfig(this.#app);
if (isOnline) {
await this.fetchAndActivate(this.#remoteConfig);
}
}
}
在 Firebase App 初始化之後,會檢查連線狀態以及 Enterprise reCAPTCHA 金鑰。當這兩個條件都滿足時,便會初始化 Firebase App Check 的執行個體。最後,獲取並啟用 Remote Config 以覆寫預設值。
提示:
Angular 22 架構最佳實踐:
Angular 團隊現在推薦針對全域單例(global singletons)使用@Service()裝飾器,而非@Injectable({ providedIn: 'root' })。因為它更乾淨、包含更少的樣板程式碼(boilerplate),且是專門為了與現代inject()函式型相依注入搭配使用而設計。
注意:如果您需要基於建構子的相依注入(constructor-based dependency injection)或限定範圍的提供者(scoped providers,例如元件層級的 scope),@Injectable()仍完全受到支援且是必要的。
import { provideFirebaseAI } from '@/features/ai/providers/ai.provider';
import { ConfigService } from '@/features/ai/services/config.service';
import { ApplicationConfig, inject } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
... other providers ...
provideAppInitializer(() => inject(ConfigService).initialize()),
provideFirebaseAI(),
],
};
將 provideFirebaseAI 與 provideAppInitializer 包含在 appConfig 的 providers 陣列中。
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
呼叫 bootstrapApplication 可確保 Firebase AI 模型、Firebase 應用程式、App Check 以及 Firebase Remote Config 的數值在啟動期間正確地初始化。
Firebase 整合完成後,我們將焦點轉向 Angular AI 服務以及 Angular 元件中的 JSON 回應串流。
混合式影像分析應用程式會提示 Gemini Nano 或 Gemini 3.5 Flash 模型傳回結構化輸出,即一個 JSON 物件。因此,應用程式會串流傳輸 JSON 回應,並使用 jsonrepair 程式庫將不完整的 JSON 物件修復成有效的局部 JSON 物件。
import { EnhancedGenerateContentResponse } from 'firebase/ai';
export interface PartialResponse<T> {
partialData: Partial<T>;
response?: EnhancedGenerateContentResponse;
}
import { inject, Service } from '@angular/core';
import { GenerativeModel, SchemaRequest, TypedSchema } from 'firebase/ai';
import { jsonrepair } from 'jsonrepair';
import { GenerateContentParams, PartialResponse } from '../types/ai.types';
import { AiModelCacheService } from './ai-model-cache.service';
@Service()
export class AiService {
#cacheService = inject(AiModelCacheService);
private getCachedModel({ schema, systemInstruction }: GenerateContentParams) {
const model = this.#cacheService.getOrCreateModel({
schema,
systemInstruction,
});
return model;
}
private parseStreamJSONResponse<T>(text: string, schema?: TypedSchema | SchemaRequest) {
if (schema) {
try {
return JSON.parse(text) as Partial<T>;
} catch {
return JSON.parse(jsonrepair(text)) as Partial<T>;
}
}
return { alternativeTexts: [text] } as unknown as Partial<T>;
}
private async downloadDeviceModel(model: GenerativeModel) {
if (model) {
await model.initializeDeviceModel((val) => console.log(`Download progress: ${Math.round(val * 10000) / 100}%`));
}
}
private constructRequest(params: GenerateContentParams) {
return typeof params.contents === 'string' || Array.isArray(params.contents) ? params.contents : [params.contents];
}
async *generateContentStream<T>(params: GenerateContentParams): AsyncGenerator<PartialResponse<T>> {
const model = this.getCachedModel(params);
const request = this.constructRequest(params);
await this.downloadDeviceModel(model);
const result = await model.generateContentStream(request);
let accumulatedText = '';
for await (const chunk of result.stream) {
accumulatedText = accumulatedText + chunk.text();
const parsed = this.parseStreamJSONResponse<T>(accumulatedText, params.schema);
yield {
partialData: parsed,
};
}
const finalResponse = await result.response;
const finalParsed = this.parseStreamJSONResponse<T>(finalResponse.text(), params.schema);
yield {
partialData: finalParsed,
response: finalResponse,
};
}
}
downloadDeviceModel 輔助方法會將 Gemini Nano 模型下載至 Chrome 瀏覽器。當使用者請求影像分析任務時,它會直接在裝置本機端執行,而不是在雲端伺服器上。敏感的使用者資料不會離開本地電腦,且雲端 AI 的使用成本可保持為零。
此 generateContentStream 產生器(generator)會產出局部 JSON 物件與最終回應。
ImageAnalysisService 會將影像分析任務委派給 AiService,以獲取產生的標籤、替代文字和推薦建議。
export async function fileToGenerativePart(file: File | Blob, mimeType?: string): Promise<Part> {
const resolvedMimeType = mimeType || file.type;
if (!resolvedMimeType) {
throw new Error('MIME type must be specified or present on the File/Blob.');
}
const dataUrl = await readFileAsDataURL(file);
const base64Data = dataUrl.split(',')[1];
return {
inlineData: {
data: base64Data,
mimeType: resolvedMimeType,
},
};
}
fileToGenerativePart 實用工具函式會讀取上傳的影像,並將其轉換為行內(inline)Base64 資料。該函式會傳回 Gemini 模型所需的資料和 MIME 類型。
export const SYSTEM_INSTRUCTION = `You are an expert image analyst, accessibility specialist, and professional photo editor.
Your behavior rules:
1. Always output strictly in raw JSON matching the provided schema, with no markdown formatting or conversational text.
2. For descriptions, prioritize screen-reader accessibility guidelines.
3. For tags, always provide an objective visual reason and platform-specific variants.`;
export const IMAGE_ANALYSIS_USER_PROMPT =
'Analyze the attached image and generate the alternative descriptions, tags, and styling suggestions according to your defined system instructions and the response schema.';
SYSTEM_INSTRUCTION 是系統提示詞,而 IMAGE_ANALYSIS_USER_PROMPT 則是使用者提示詞。
系統提示詞定義了 AI 作為專業相片編輯器的角色,而使用者提示詞則描述了輸入的影像,並指定預期傳回的結構化資料。
import { Schema } from 'firebase/ai';
import { AltTextsSchema } from './alt-texts.schema';
import { RecommendationsSchema } from './recommendation.schema';
import { TagsSchema } from './tag.schema';
export const ImageAnalysisSchema = Schema.object({
properties: {
alternativeTexts: AltTextsSchema,
tags: TagsSchema,
recommendations: RecommendationsSchema,
}
});
ImageAnalysisSchema 為使用者提示詞的結構化輸出,由替代文字、標籤與推薦建議組成。
import { fileToGenerativePart } from '@/core/utils/base64.util';
import { AiService } from '@/features/ai/services/ai.service';
import { ImageAnalysisSchema } from '@/features/image-analysis/schemas/image-analysis.schema';
import {
ImageAnalysisResponse,
StreamingAnalysisWithMetadata,
} from '@/features/image-analysis/types/image-analysis-metadata.type';
import { inject, Service } from '@angular/core';
import { IMAGE_ANALYSIS_USER_PROMPT, SYSTEM_INSTRUCTION } from '../prompts/image-analysis.prompt';
@Service()
export class ImageAnalysisService {
#aiService = inject(AiService);
async *analyzeImageStream(file: File | Blob, customPrompt?: string): AsyncGenerator<StreamingAnalysisWithMetadata> {
const imagePart = await fileToGenerativePart(file);
const userPrompt = customPrompt ? customPrompt : IMAGE_ANALYSIS_USER_PROMPT;
const generator = await this.#aiService.generateContentStream<ImageAnalysisResponse>({
systemInstruction: SYSTEM_INSTRUCTION,
contents: [userPrompt, imagePart],
schema: ImageAnalysisSchema,
});
for await (const update of generator) {
const partialData = update.partialData;
const analysis = {
alternativeTexts: partialData?.alternativeTexts,
tags: partialData?.tags,
recommendations: partialData?.recommendations,
};
yield {
analysis,
};
}
}
}
analyzeImageStream 是一個產生器,會傳回影像分析的輸出結果。
ImageAnalysis 元件包含了一個影像上傳器,用以選擇要分析的影像。點擊按鈕時,它會呼叫 triggerAnalysis 來串流傳輸影像分析的回應,並設定 analysisData 訊號(signal)的數值。
import { ImageAnalysisService } from '@/features/image-analysis/services/image-analysis';
import { StreamingAnalysisWithMetadata } from '@/features/image-analysis/types/image-analysis-metadata.type';
import { ImageUploader } from '@/shared/ui/image-uploader/image-uploader';
import { Component, computed, inject, signal } from '@angular/core';
import { ImageAnalysisPanel } from './image-analysis-panel/image-analysis-panel';
import { TagList } from './tag-list/tag-list';
import { ImageTag } from './tag-list/types/image-tag.type';
@Component({
selector: 'app-image-analysis',
imports: [ImageUploader, TagList, ImageAnalysisPanel],
templateUrl: './image-analysis.html',
styleUrl: './image-analysis.css',
})
export default class ImageAnalysis {
imageAnalysisService = inject(ImageAnalysisService);
// Local reactive states
imageUrl = signal<string | null>(null);
selectedFile = signal<File | null>(null);
analysisData = signal<StreamingAnalysisWithMetadata | null>(null);
tags = computed<ImageTag[]>(() => {
const data = this.analysisData();
if (!data) {
return [];
}
return (data.analysis.tags || []).map((t) => {
return {
label: t.name,
tooltip: t.sentence,
};
});
});
onFileSelected(file: File) {
this.selectedFile.set(file);
this.analysisData.set(null);
this.errorMessage.set('');
}
async triggerAnalysis() {
const file = this.selectedFile();
if (!file) {
return;
}
const stream = this.imageAnalysisService.analyzeImageStream(file);
for await (const update of stream) {
this.analysisData.set(update);
}
}
onImageRemoved() {
this.selectedFile.set(null);
this.analysisData.set(null);
this.errorMessage.set('');
}
}
當 analysisData 更新時,元件的 HTML 範本便會顯示最新的數值。
<div class="workspace-container">
<app-image-uploader
[(imageUrl)]="imageUrl"
(fileSelected)="onFileSelected($event)"
(imageRemoved)="onImageRemoved()"
></app-image-uploader>
@if (imageUrl() && !isLoading()) {
<div class="action-container">
<button (click)="triggerAnalysis()" class="primary-action-btn">
<span class="material-symbols-outlined">psychology</span>
Analyze Image ({{ seconds() }}s)
</button>
</div>
}
@if (tags().length > 0) {
<!-- Semantic Tag Cloud -->
<div class="tag-cloud-section animate-fade-in">
<h4 class="section-subtitle">Detected Classifications</h4>
<app-tag-list [tags]="tags()"></app-tag-list>
</div>
@if (analysisData()) {
<!-- Advanced Analysis Results Panel -->
<app-image-analysis-panel
class="animate-fade-in block"
[data]="analysisData()"
[imageUrl]="imageUrl()"
[source]="source()"
></app-image-analysis-panel>
}
}
</div>
此範本將輸入(inputs)傳送至 app-tag-list 與 app-image-analysis-panel 元件。
<div class="tag-list-wrapper">
<div class="tag-list-grid">
@for (tag of tags(); track tag.label) {
<app-badge [label]="tag.label" [tooltip]="tag.tooltip" />
}
</div>
</div>
以上是標籤清單元件的 HTML 範本。此元件渲染出標籤清單。當滑鼠懸停於標籤上時,會顯示工具提示(tooltip)以說明選擇該標籤的原因。
@if (data(); as analysis) {
@let innerAnalysis = analysis.analysis;
@let alternativeTexts = innerAnalysis?.alternativeTexts;
@let recommendations = innerAnalysis?.recommendations;
<div class="analysis-panel-container">
<app-tab-group>
<app-tab id="thought" label="Thought Summary">
<app-thought-summary
[alternativeTexts]="alternativeTexts"
/>
</app-tab>
<app-tab id="detailed" label="Recommendations">
<app-image-recommendation [recommendations]="recommendations" />
</app-tab>
</app-tab-group>
</div>
}
app-image-analysis-panel 在不同的分頁(頁籤)中渲染替代文字與推薦建議,供使用者檢視。
這為使用 Angular、Firebase AI Logic Hybrid and On-device Web SDK 以及 Gemini 模型建置混合式影像分析應用程式的旅程劃下了句點。
工程師可以建置在本地端執行、並在 Chrome 瀏覽器中使用 Gemini Nano 模型的 AI 應用程式。當網路不穩定時,此應用程式也能在離線狀態下正常工作,因為 AI 處理過程絕不會離開本地電腦、傳送到雲端基礎架構中。