Skip to main content

Command Palette

Search for a command to run...

Construindo um App com IA: Um Guia com Angular, Genkit e o Poder do Gemini no Mundo de Naruto

Updated
13 min read
A

Desenvolvedor e palestrinha

E aí, dev? Se você é fã de animes e de desenvolvimento de software, vai adorar este projeto. Que tal unir a força do universo Naruto com o poder da Inteligência Artificial para criar uma aplicação moderna?

Neste artigo, vamos construir o "Hokage Desk": um aplicativo onde você descreve uma missão ninja e uma IA, agindo como um mestre de Konoha, gera um pergaminho de missão completo. Para isso, usaremos Angular no front-end e, no back-end, a dupla dinâmica Genkit e a API do Gemini, a mais nova geração de modelos de IA do Google.

Prepare-se para mergulhar em um projeto que combina uma API de IA de ponta com um dos universos mais amados da cultura pop. Vamos nessa?

Pré-requisitos

Antes de começarmos nossa jornada ninja, garanta que você tenha as seguintes ferramentas instaladas em sua máquina:

Obtendo sua Chave de API do Gemini

Para que nossa IA funcione, precisamos de uma chave de API do Gemini. É grátis e fácil de conseguir.

  1. Acesse o Google AI Studio.

  2. Faça login com sua conta do Google.

  3. No menu à esquerda, clique em "Get API key".

  4. Clique em "Create API key in new project".

  5. Copie a chave de API gerada. Ela será seu passe para o mundo da IA.

Configurando a Variável de Ambiente

O Genkit precisa "ler" essa chave. Para isso, crie uma variável de ambiente em seu sistema chamada GOOGLE_API_KEY com a chave que você copiou.

GOOGLE_API_KEY=<SUA_API_KEY>

1. Criando o Projeto Angular: A Fundação da Aldeia

Vamos começar erguendo as estruturas da nossa aplicação com o Angular CLI. Vamos criar um projeto já com Server-Side Rendering (SSR) ativado, o que é uma ótima prática.

ng new hokage-desk --ssr

Quando perguntado sobre o sistema de CSS, escolha Tailwind CSS, pois ele nos dará a agilidade necessária para estilizar nossa aldeia digital.

Com o projeto criado, instale as dependências que usaremos para o Genkit e o servidor Express:

cd hokage-desk
npm i genkit
npm i express@^4.21.1
npm i @genkit-ai/express
npm i @genkit-ai/google-genai
npm i zod@^3.25
npm i uuid

2. Configurando o Fluxo do Genkit: O Cérebro da Operação

Agora, vamos para a parte mais legal: criar o cérebro de IA que irá gerar nossas missões.

Crie o Arquivo de Fluxo

Dentro da pasta src/, crie um novo arquivo chamado flows.ts. É aqui que a mágica vai acontecer.

Definindo o Fluxo

Abra o arquivo src/flows.ts e cole o código a seguir. Este é o coração da nossa funcionalidade.

import { genkit } from 'genkit';
import { z } from 'zod';
import { googleAI } from '@genkit-ai/google-genai';
import { v4 as uuidv4 } from 'uuid';

const ai = genkit({
  plugins: [googleAI()],
  model: googleAI.model('gemini-2.5-flash', {
    temperature: 0.8,
  }),
});

// Define input schema
export const MissionDefinitionSchema = z.object({
  definition: z.string().describe('Definition of the mission'),
});

// Define output schema
// Final schema for the Ninja Mission
export const missionSchema = z
  .object({
    id: z.string().describe('Unique identifier for the mission'),
    title: z.string().describe('A creative and fitting title for the mission.'),
    difficulty: z
      .string()
      .describe(
        'Classify the mission into one of the following ranks, based on its complexity and danger: D, C, B, A, or S.'
      ),
    missionValue: z
      .string()
      .describe(
        "Define a reward in Ryō. The value must be consistent with the mission's difficulty (e.g., Rank-D missions are low-value, Rank-S missions are very high-value)."
      ),
    detailedDescription: z
      .string()
      .describe(
        "Create a detailed, narrative-style mission briefing based on the user's initial input. It should include the background context, a clear primary objective, known risks or enemy intel, and the mission's location. This should read like an official mission scroll given to the team leader."
      ),
    ninjaTeamLevel: z
      .string()
      .describe(
        'Based on the mission\'s difficulty, suggest the team\'s rank. Use standard Naruto ranks like Genin, Chunin, Jonin, or ANBU. For legendary missions, you could even specify Sannin ou Kage-level.'
      ),
    assignedTeam: z
      .string()
      .describe(
        "Assign a suitable team. If a known, official team from the Naruto or Boruto universe fits the mission and members (e.g., 'Team 7', 'Ino-Shika-Cho', 'Team Guy'), use its official name. If no existing team is a perfect fit, create a new, thematic squad name (e.g., 'Sand Village Barrier Unit', 'Mist Village Cipher Squad')."
      ),
    teamMembers: z
      .array(
        z.object({
          name: z
            .string()
            .describe(
              "Select a real character from the Naruto or Boruto anime who would be suitable for this mission's rank and objective."
            ),
          specialty: z
            .string()
            .describe(
              "State this character's known signature jutsu or primary skill (e.g., 'Rasengan', 'Sharingan', 'Byakugan', 'Shadow Possession Jutsu')."
            ),
        })
      )
      .describe(
        'A list containing exactly 3 members for the ninja team. The members chosen must be real characters from Naruto or Boruto.'
      ),
  })
  .describe('The complete mission file in JSON format.');

// Define a recipe generator flow
export const missionGeneratorFlow = ai.defineFlow(
  {
    name: 'missionGeneratorFlow',
    inputSchema: MissionDefinitionSchema,
    outputSchema: missionSchema,
  },
  async (input) => {
    // Create a prompt based on the input
    const prompt = `
        You are a mission assignment expert from Konohagakure, with deep knowledge of every shinobi and official team from both the Naruto and Boruto eras.
        Your primary task is to take a brief mission concept and expand it into a complete, official mission file. The initial concept from the user is: "${input.definition}".
        All generated content must be in the same language as the user's input.
        IMPORTANT LOGIC:
        1.  Generate a creative and fitting title for the mission.
        2.  Elaborate on the user's concept to create a detailed mission description, including background, objectives, and known risks.
        3.  Select REAL characters from the Naruto or Boruto universe for the team. The team composition must be logical for the mission's assigned difficulty. Do not assign Kage-level shinobi to a D-rank mission.
        4.  For the team name, prioritize using an official team name (e.g., 'Team 7') if the members align. Otherwise, create a fitting squad name.
        Generate the output in the requested JSON format.
      `;

    // Generate structured recipe data using the same schema
    const { output } = await ai.generate({
      prompt,
      output: { schema: missionSchema },
    });

    if (!output) throw new Error('Failed to generate recipe');

    return {
      ...output,
      id: uuidv4(),
    };
  }
);

O que este código faz?

  1. Configura o Genkit: Dizemos ao Genkit para usar o plugin da Google AI e especificamos o modelo gemini-2.5-flash, que é rápido e poderoso.

  2. Define um "System Prompt": Damos instruções detalhadas ao Gemini, dizendo a ele para atuar como um especialista em missões de Konoha.

  3. Usa Zod para Validação: Criamos esquemas de validação (schemas) com Zod. Isso é incrível porque garante que a entrada do usuário e, mais importante, a saída da IA, sigam uma estrutura JSON exata que definimos. É como um contrato com a IA.

  4. Cria o missionGeneratorFlow: Este é o nosso fluxo principal. Ele recebe a definição da missão, envia para o Gemini com o nosso prompt e o schema, e retorna um objeto JSON completo e validado.


3. Criando o Endpoint da API: Conectando o Back-end

Com nosso fluxo de IA pronto, precisamos de uma porta de entrada para ele em nosso servidor Angular.

Abra o arquivo src/server.ts e adicione o seguinte trecho no início do arquivo:

import {
  AngularNodeAppEngine,
  createNodeRequestHandler,
  isMainModule,
  writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { missionGeneratorFlow } from './flows';
import { join } from 'node:path';

const browserDistFolder = join(import.meta.dirname, '../browser');

const app = express();
const angularApp = new AngularNodeAppEngine();

app.use(express.json());

app.post('/api/mission', async (req, res) => {
  try {
    const result = await missionGeneratorFlow(req.body);
    res.json(result);
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: (error as Error).message });
  }
});

// O código do servidor continua aqui...

Este código cria uma rota POST em /api/mission. Quando o front-end envia uma definição de missão para este endpoint, ele aciona nosso missionGeneratorFlow e devolve a missão completa gerada pela IA. Simples assim!


4. Criando o Serviço de Missões no Angular: Gerenciando o Estado

De volta ao front-end, vamos criar um serviço para cuidar do estado das nossas missões e da comunicação com o back-end.

Gere o Serviço

Use o Angular CLI para criar um novo serviço:

ng generate service mission

Implemente o Serviço

Abra o arquivo recém-criado src/app/mission.service.ts e adicione este código:

import { HttpClient } from '@angular/common/http';
import { Injectable, computed, inject, signal } from '@angular/core';
import { take } from 'rxjs';

export interface Mission {
  id: string;
  title: string;
  difficulty: string;
  missionValue: string;
  detailedDescription: string;
  ninjaTeamLevel: string;
  assignedTeam: string;
  teamMembers: {
    name: string;
    specialty: string;
  }[];
}

@Injectable({
  providedIn: 'root',
})
export class MissionService {
  missions = signal<Mission[]>([]);
  loading = signal(false);

  private readonly http = inject(HttpClient);

  createMission(definition: string) {
    this.loading.set(true);
    this.http
      .post<Mission>('/api/mission', { definition })
      .pipe(take(1))
      .subscribe((response) => {
        this.missions.update((missions) => [...missions, response]);
        this.loading.set(false);
      });
  }

  getMissionById(id: string) {
    return computed(() => this.missions().find((mission) => mission.id === id));
  }
}

Por que este serviço é importante?

  1. Reatividade com Signals: Ele usa um signal (missions) para armazenar a lista de missões. Qualquer componente que usar este signal será atualizado automaticamente quando uma nova missão chegar.

  2. Estado de Carregamento: O loading signal nos ajuda a mostrar um feedback visual para o usuário enquanto a IA está "pensando".

  3. Comunicação com a API: O método createMission faz a chamada POST para o nosso back-end e atualiza o signal de missões com a resposta.

  4. Busca por ID: O getMissionById nos permite encontrar uma missão específica na lista, o que será útil para a nossa página de detalhes.


5. Criando os Componentes da Interface: O Rosto da Aplicação

Chegou a hora de dar vida à nossa interface.

Configurando o index.html e o Tema

Primeiro, vamos preparar nosso index.html e o tema com Tailwind.

Abra src/index.html e deixe-o assim:

<!doctype html>
<html lang="en" class="dark">
<head>
  <meta charset="utf-8">
  <title>HokageDesk</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
</head>
<body class="bg-background-dark font-display text-gray-100">
  <app-root></app-root>
</body>
</html>

Agora, crie um arquivo src/theme.css com as variáveis de cor e fontes:

@theme {
  --color-primary: #4A6741;
  --color-background-light: #FDFBF5;
  --color-background-dark: #2D2D2D;
  --color-card-dark: #3C3C3C;
  --color-leaf-green: #4A5C43;
  --color-leaf-green-dark: #3A4A35;
  --color-text-light: #2D3748;
  --color-text-dark: #E2E8F0;
  --font-display: "Space Grotesk", sans-serif;
  --radius: 0.25rem;
  --radius-lg: 0.5rem;
  --radius-xl: 0.75rem;
  --radius-full: 9999px;
}

Finalmente, abra src/styles.css e importe tudo:

@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
@import "tailwindcss";
@import "./theme.css";

Gere os Componentes

Vamos usar o Angular CLI para gerar os dois componentes principais da nossa UI:

ng generate component dashboard
ng generate component detail

Configurando as Rotas

Abra src/app/app.routes.ts e defina as rotas para nossos novos componentes. Usaremos loadComponent para carregá-los de forma assíncrona (lazy loading).

import { Routes } from '@angular/router';

export const routes: Routes = [
    { path: '', loadComponent: () => import('./dashboard/dashboard').then(c => c.Dashboard) },
    { path: 'detail/:id', loadComponent: () => import('./detail/detail').then(c => c.Detail) },
];

Limpe o src/app/app.html, deixando apenas o router-outlet:

<router-outlet></router-outlet>

Implementando o Dashboard

Este será nosso painel principal, com o formulário para criar missões e a lista de missões geradas.

src/app/dashboard/dashboard.ts

import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { MissionService } from '../mission';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.html',
  styleUrls: ['./dashboard.css'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [FormsModule, RouterLink],
})
export class Dashboard {
  private readonly missionService = inject(MissionService);
  missionDefinition = signal('');
  missions = this.missionService.missions;
  loading = this.missionService.loading;

  createMission() {
    this.missionService.createMission(this.missionDefinition());
    this.missionDefinition.set('');
  }
}

src/app/dashboard/dashboard.css

.avatar-stack > div {
  margin-left: -0.75rem;
}

.avatar-stack > div:first-child {
  margin-left: 0;
}

src/app/dashboard/dashboard.html

<div class="min-h-screen w-full p-4 sm:p-6 md:p-8">
  <div class="mx-auto max-w-5xl">
    <header class="text-center mb-8 md:mb-12">
      <h1 class="text-3xl sm:text-4xl md:text-5xl font-bold text-white">Genkit-Jutsu: Mission Generator</h1>
    </header>
    <main>
      <section class="mb-10 md:mb-16">
        <div class="bg-card-dark p-6 sm:p-8 rounded-xl shadow-lg">
          <form class="space-y-6">
            <div>
              <label class="block text-sm font-medium text-gray-300 mb-2" for="mission-directive">Mission Directive</label>
              <textarea
                [ngModel]="missionDefinition()"
                (ngModelChange)="missionDefinition.set($event)"
                [disabled]="loading()"
                class="w-full border rounded-lg border-gray-600 bg-gray-700/50 focus:border-primary focus:ring-primary placeholder:text-gray-500 py-2 px-3 "
                id="mission-directive"
                name="mission-directive"
                placeholder="Describe the mission objectives, targets, and any special considerations..."
                rows="4"
              ></textarea>
            </div>
            <button
              (click)="createMission()"
              [disabled]="loading()"
              class="w-full bg-primary text-white font-bold py-3 px-4 rounded-lg hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-card-dark focus:ring-primary transition-colors"
              type="button"
            >
              @if (loading()) {
              <div class="flex items-center justify-center">
                <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                </svg>
                <span>Generating...</span>
              </div>
              } @else {
              <span>Generate Mission Scroll</span>
              }
            </button>
          </form>
        </div>
      </section>

      <section>
        <h2 class="text-2xl sm:text-3xl font-bold mb-6 text-white">Generated Mission Scrolls</h2>
        @defer (on immediate) {
        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
          @for (mission of missions(); track mission.id) {
          <a [routerLink]="['/detail', mission.id]" class="bg-background-light p-6 rounded-xl shadow-md flex flex-col space-y-4 cursor-pointer hover:bg-primary/10 transition-colors">
            <div class="flex-grow">
              <div class="flex justify-between items-start mb-2">
                <h3 class="text-xl font-bold text-gray-800">{{ mission.title }}</h3>
                <span class="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2.5 py-0.5 rounded-full">Rank {{ mission.difficulty }}</span>
              </div>
              <div class="flex items-center space-x-2">
                <p class="text-sm text-gray-600">Assigned Team: {{ mission.assignedTeam }}</p>
              </div>
            </div>
          </a>
          }
        </div>
        } @loading {
        <div class="flex justify-center items-center">
          <svg class="animate-spin -ml-1 mr-3 h-10 w-10 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
          </svg>
        </div>
        }
      </section>
    </main>
  </div>
</div>

Implementando o Detail

Este componente mostrará todos os detalhes de uma missão quando o usuário clicar em um dos cards do dashboard.

src/app/detail/detail.ts

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { MissionService } from '../mission';

@Component({
  selector: 'app-detail',
  templateUrl: './detail.html',
  styleUrls: ['./detail.css'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
})
export class Detail {
  private readonly route = inject(ActivatedRoute);
  private readonly missionService = inject(MissionService);

  mission = this.missionService.getMissionById(
    this.route.snapshot.paramMap.get('id')!
  );
}

src/app/detail/detail.component.css

.custom-red-chip {
  background-color: #991b1b;
}

.custom-blue-chip {
  background-color: #1e3a8a;
}

src/app/detail/detail.html

<div class="min-h-screen flex items-center justify-center p-4">
  @if (mission(); as vm) {
  <main class="w-full max-w-4xl bg-primary text-text-light dark:bg-gray-800 dark:text-text-dark rounded-xl shadow-lg p-6 md:p-8">
    <header class="mb-6">
      <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
        <h1 class="text-3xl md:text-4xl font-bold text-leaf-green dark:text-leaf-green-dark">{{ vm.title }}</h1>
        <div class="flex items-center gap-3">
          <span class="custom-red-chip text-white text-xs font-bold px-3 py-1 rounded-full">Rank {{ vm.difficulty }}</span>
          <span class="custom-blue-chip text-white text-xs font-bold px-3 py-1 rounded-full">{{ vm.ninjaTeamLevel }}</span>
        </div>
      </div>
    </header>
    <div class="space-y-8">
      <section>
        <h2 class="text-xl font-bold mb-2 text-leaf-green dark:text-leaf-green-dark">Mission Briefing</h2>
        <p class="text-gray-600 dark:text-gray-400 leading-relaxed">
          {{ vm.detailedDescription }}
        </p>
      </section>
      <section>
        <h2 class="text-xl font-bold mb-2 text-leaf-green dark:text-leaf-green-dark">Mission Value</h2>
        <div class="flex items-center gap-2">
          <span class="material-symbols-outlined text-2xl text-leaf-green dark:text-leaf-green-dark">
            account_balance_wallet
          </span>
          <p class="text-lg font-bold text-gray-700 dark:text-gray-300">{{ vm.missionValue }}</p>
        </div>
      </section>
      <section>
        <h2 class="text-xl font-bold mb-4 text-leaf-green dark:text-leaf-green-dark">Assigned Team: {{ vm.assignedTeam }}</h2>
        <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
          @for (team of vm.teamMembers; track team) {
          <div class="flex flex-col items-center p-4 bg-background-light dark:bg-gray-700 rounded-lg shadow-sm">
            <h3 class="font-bold text-lg text-text-light dark:text-text-dark">{{ team.name }}</h3>
            <p class="text-sm text-gray-600 dark:text-gray-400">{{ team.specialty }}</p>
          </div>
          }
        </div>
      </section>
    </div>
  </main>
  }
</div>

Conclusão e Próximos Passos

Parabéns, shinobi do código! Você construiu com sucesso uma aplicação Angular completa que utiliza Genkit e a API do Gemini para gerar conteúdo dinâmico de IA. Você viu como é fácil integrar um back-end de IA diretamente em um projeto Angular SSR.

Para rodar sua aplicação e ver a mágica acontecer, execute:

npm start

Acesse http://localhost:4200 em seu navegador, descreva uma missão como "Proteger um construtor de pontes" ou "Infiltrar-se na base da Akatsuki", e veja a IA criar um pergaminho de missão completo para você!

E agora? O céu é o limite! Aqui estão algumas ideias para evoluir o projeto:

  • Adicionar Autenticação: Permita que diferentes "Hokages" façam login e gerenciem suas próprias missões.

  • Salvar Missões em um Banco de Dados: Integre um banco de dados como Firebase ou Supabase para persistir as missões geradas.

  • Explorar Mais a Fundo a API do Gemini: Experimente com diferentes prompts, adicione mais campos ao seu schema Zod (como "Itens Recomendados" ou "Plano de Fuga"), ou até mesmo use a capacidade multimodal do Gemini para gerar uma imagem para cada missão.

Espero que você tenha se divertido construindo este projeto tanto quanto eu me diverti escrevendo este guia. Agora é sua vez de expandir suas habilidades e criar seus próprios "jutsus" de IA.

Todo o código fonte está nesse repositório: GITHUB

Até a próxima

More from this blog

Alvaro Camillo Neto

11 posts

Desenvolvedor e palestrinha