Vamos aprender juntos como integrar o gemini 2.0 na nossa aplicação angular e criar nossa própria pokéagenda baseado em LLM. Todos os links e passo a passo também se encontram neste GitHub NgPokedexLLM.
Criando App Inicial
Instalando Dependências do projeto
Utilizando no terminal o angular cli, vamos criar uma nova aplicação com o comando ng new ng-gemini
, selecionar as opções (estilo: scss, ssr: não) e adicionar a lib do angular material com o comando ng add @angular/material
, neste caso quando perguntado sobre o tema, utilizaremos a opção "custom":
npm i -g @angular/cli
ng version
ng new ng-pokedex-llm
// Which stylesheet format would you like to use? scss
// Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? No
cd ng-gemini
Também vamos instalar outras dependências, incluindo a principal para este exercício, sobre o uso do gemini:
npm i @google/generative-ai@^0.16.0
Integrando Gemini com Angular
Apenas texto
Vamos começar integrando o Gemini para responder a perguntas sobre Pokémon usando apenas texto. Nesse caso como não demos muito contexto, ele pode dar uma resposta bem aberta.
src/app/app.component.ts
import { Component, signal } from '@angular/core';
import { GoogleGenerativeAI } from '@google/generative-ai';
const API_KEY = '<sua_api_key_aqui>';
@Component({
selector: 'app-root',
imports: [],
template: `
<h1>Integrando Angular com Gemini</h1>
<h3>{{ prompt }}</h3>
<p>{{ text() }}</p>
`,
})
export class AppComponent {
private genAI = new GoogleGenerativeAI(API_KEY);
private model = this.genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
prompt = 'Quem é esse pokemon?';
text = signal('');
constructor() {
this.generateText();
}
async generateText() {
const result = await this.model.generateContent(this.prompt);
this.text.set(result.response.text());
}
}
Multimodal (texto + imagem)
Agora, vamos salvar a imagem de um Pokémon da nossa escolha na pasta /public com o nome de "pokemon.png" (deixei uma imagem de um Pokémon chamado "Golduck"), e solicitar ao gemini um conteúdo baseado em um array composto de um prompt (texto) e de uma imagem (convertida em texto/base64). Também vamos organizar a funcionalidade de chamar o Gemini e de converter imagem para base64 em dois novos serviços (GeminiService e ImageService):
src/app/media.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MediaService {
async imageToBase64(imagePath: string): Promise<string> {
try {
const response = await fetch(imagePath);
const blob = await response.blob();
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const imageDataUrl = reader.result as string;
const base64Image = imageDataUrl.split(',')[1];
resolve(base64Image);
}
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.error('Erro ao converter imagem para Base64:', error);
return '';
}
}
}
src/app/gemini.service.ts
import { Injectable } from '@angular/core';
import { GoogleGenerativeAI, Part } from '@google/generative-ai';
const API_KEY = '<sua_api_key_aqui>';
@Injectable({
providedIn: 'root'
})
export class GeminiService {
private genAI = new GoogleGenerativeAI(API_KEY);
private model = this.genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
async generateContent(request: string | Array<string | Part>): Promise<string> {
const data = await this.model.generateContent(request);
return data.response.text();
}
}
src/public/pokemon.png Link da imagem
src/app/app.component.ts
import { Component, inject, signal } from '@angular/core';
import { MediaService } from './media.service';
import { GeminiService } from './gemini.service';
@Component({
selector: 'app-root',
imports: [],
template: `
<h1>Integrando Angular com Gemini</h1>
<h3>{{ prompt }}</h3>
<p>{{ text() }}</p>
`,
})
export class AppComponent {
private geminiService = inject(GeminiService);
private mediaService = inject(MediaService);
prompt = 'Quem é esse pokemon?';
text = signal('');
constructor() {
this.generateText();
}
async generateText() {
const base64 = await this.mediaService.imageToBase64('/pokemon.png');
const imagePart = { inlineData: { data: base64, mimeType: 'image/png', }};
const result = await this.geminiService.generateContent([this.prompt, imagePart]);
this.text.set(result);
}
}
Criação do Layout principal
Vamos agora criar uma Pokédex que utiliza a câmera do dispositivo para identificar Pokémon em tempo real. A cada 500ms (5 segundos) iremos capturar um snapshot da camera, e enviar com um prompt solicitando o retorno em um formato específico. Caso o retorno seja positivo, que ele identifique um pokemon com uma precisão adequada, iremos abrir um alert com o nome do Pokémon.
src/app/media.service.ts
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MediaService {
private video!: HTMLVideoElement;
private canvas!: HTMLCanvasElement;
private base64Image = signal<string>('');
base64 = this.base64Image.asReadonly();
startCamera(video: HTMLVideoElement, canvas: HTMLCanvasElement): void {
// verificar se a câmera está disponível ('user' ou 'environment')
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } })
.then(stream => {
this.video = video;
this.video.srcObject = stream;
this.canvas = canvas;
this.startCapture();
})
.catch(error => {
console.error('Erro ao acessar a câmera: ', error);
})
}
startCapture(): void {
setInterval(() => {
this.captureAndSend();
}, 5000); // intervalo entre capturas em milisegundos
}
private captureAndSend(): void {
// capturar a imagem da câmera utilizando o canvas
const context = this.canvas.getContext('2d')!;
const { width, height } = this.video.getBoundingClientRect();
this.canvas.width = width;
this.canvas.height = height;
context.drawImage(this.video, 0, 0, width, height);
const imageDataUrl = this.canvas.toDataURL('image/jpeg');
// atualizar a imagem como base64 (e propagar para o componente)
this.base64Image.set(imageDataUrl.split(',')[1]);
}
async imageToBase64(imagePath: string): Promise<string> {
try {
const response = await fetch(imagePath);
const blob = await response.blob();
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const imageDataUrl = reader.result as string;
const base64Image = imageDataUrl.split(',')[1];
resolve(base64Image);
}
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.error('Erro ao converter imagem para Base64:', error);
return '';
}
}
}
src/app/app.component.ts
import { Component, effect, ElementRef, inject, viewChild } from '@angular/core';
import { Part } from '@google/generative-ai';
import { MediaService } from './media.service';
import { GeminiService } from './gemini.service';
type PokemonData = {
name: string;
number: number;
probability: number;
}
@Component({
selector: 'app-root',
imports: [],
template: `
<video #video autoplay playsinline></video>
<canvas #canvas></canvas>
`,
})
export class AppComponent {
private geminiService = inject(GeminiService);
private mediaService = inject(MediaService);
videoElement = viewChild<ElementRef<HTMLVideoElement>>('video');
canvasElement = viewChild<ElementRef<HTMLCanvasElement>>('canvas');
base64 = this.mediaService.base64;
viewChildRef = effect(() => {
const video = this.videoElement()!.nativeElement;
const canvas = this.canvasElement()!.nativeElement;
this.mediaService.startCamera(video, canvas);
});
captureRef = effect(() => {
this.detectPokemon(this.base64());
})
private async detectPokemon(base64: string) {
if (!base64) return;
const prompt = `
Identifique o Pokémon na imagem, usando como base a Pokédex oficial.
Retorne um JSON no seguinte formato:
{"name": "Nome do Pokémon", "number": Numero do pokemon, "probability": 0.99}
A resposta deve ser APENAS o JSON, sem texto adicional ou formatação.
Se a imagem não contiver um Pokémon reconhecido, retorne {"nome": "Desconhecido", "numero": 0, "probabilidade": 0.0}.
Priorize a precisão e evite suposições.
`;
const imagePart: Part = {
inlineData: {
data: base64,
mimeType: 'image/jpeg',
}
}
const responseText = await this.geminiService.generateContent([prompt, imagePart]);
// converter o texto string JSON em um objeto JavaScript
const cleanedText = responseText.replace(/`json\s*|\s*`/g, '');
const pokemonResponse = JSON.parse(cleanedText) as PokemonData;
// mostrar nome do pokemon encontrado se probabilidade for suficiente
if (pokemonResponse.number && pokemonResponse.probability > .9) {
alert(pokemonResponse.name);
}
}
}
Conforme também explicado em vídeo, tudo bem deixarmos a constante "API_KEY" no serviço para testarmos localmente, mas antes de fazer o deploy no github pages, e para conseguir testar a aplicação no celular, modifique o serviço do gemini para solicitar a API_KEY através do localstorage, porque deste modo não precisamos subir um dado sensível ao versionar o código no github, ou constar a chave no código frontend em produção. Para criamos essa chave, precisamos acessar a página do AI Studio API keys. Esta chave para utilização dos modelos mencionados nos exemplos acima (gemini-2.0-flash) é gratuita com restrições de uso (RPM - requisições por minuto).
src/app/app.component.ts
import { Injectable } from '@angular/core';
import { GenerativeModel, GoogleGenerativeAI, Part } from '@google/generative-ai';
@Injectable({
providedIn: 'root'
})
export class GeminiService {
private genAI!: GoogleGenerativeAI;
private model!: GenerativeModel;
constructor() {
let apiKey = localStorage.getItem('API_KEY') || '';
while (!apiKey) {
apiKey = prompt("Digite sua API_KEY") || '';
}
localStorage.setItem('API_KEY', apiKey);
this.genAI = new GoogleGenerativeAI(apiKey);
this.model = this.genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
}
async generateContent(request: string | Array<string | Part>): Promise<string> {
const data = await this.model.generateContent(request);
return data.response.text();
}
}
Deixei também o link de demonstração no about do repositório para testarem! Também temos a página de documentação oficial sobre a biblioteca e seu uso para web no site Google AI for Developers. Espero que tenham gostado deste conteúdo! Também sigam e compartilhem a página Angularizando no LinkedIn!