Angular

25 jul, 2025

Construindo um aplicativo de bate-papo de IA simples com Spring AI e Angular

Publicidade

Neste tutorial, criaremos um aplicativo de bate-papo simples com IA usando Spring AI no backend e Angular no frontend. Este é um ótimo ponto de partida para quem busca integrar recursos de IA em seus aplicativos web.

O que construiremos

Criaremos uma interface de bate-papo simples onde os usuários podem:

  • Enviar mensagens para um assistente de IA
  • Receba respostas inteligentes fornecidas pela Gemini

espaço reservado

A aplicação consistirá em:

  • Um backend Spring Boot com integração Spring AI
  • Um frontend Angular com uma interface de chat responsiva
  • Comunicação em tempo real entre frontend e backend

Pré-requisitos

Antes de começar, certifique-se de ter:

  • Java 21 ou superior
  • Node.js e npm
  • CLI Angular ( npm install -g @angular/cli)
  • Um ID de Projeto Gemini (veja como criar um aqui Gemini )
  • Seu IDE favorito (usarei VS Code e IntelliJ )

💡 Ganhe 3 meses grátis do IntelliJ Ultimate com o cupom: LoianeGroner.

Estrutura do Projeto

Veja como nosso projeto está organizado:

spring-ai-angular/
├── api-ai/                          # Spring Boot backend
│   └── src/main/java/com/loiane/api_ai/
│       └── chat/
│           ├── SimpleChatService.java
│           ├── ChatController.java
│           └── ChatResponse.java
└── angular-ai/                      # Angular frontend
    └── src/app/
        └── chat/
            ├── chat-service.ts
            ├── chat-response.ts
            └── simple-chat/
                ├── simple-chat.ts
                ├── simple-chat.html
                └── simple-chat.scss

Criaremos um projeto Spring Boot e um projeto Angular CLI e colocaremos ambos na pasta spring-ai-angular

Configurando o Spring AI Backend

Crie um novo projeto Spring Boot usando o Spring Initializr ou seu IDE.

Seleções:

  • Projeto: Maven
  • Linguagem: Java
  • Spring Boot: 3.5.3 (selecione a versão mais recente)
  • Insira os metadados do seu projeto (nome do pacote, nome do artefato) conforme desejado
  • Java: 24 (ou mais recente disponível)

Dependências:

  • Spring Web: Para criar endpoints REST.
  • Vertex AI Gemini: Para integração com os modelos de linguagem do Google.

Ou selecione a dependência de IA de sua preferência.

Verifique o arquivo pom.xml

Certifique-se pom.xmlde incluir as dependências necessárias para a integração do Google Gemini:

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-vertex-ai-gemini-spring-boot-starter</artifactId>
  </dependency>
</dependencies>

A dependência spring-ai-vertex-ai-gemini-spring-boot-starterfornece os componentes necessários e a autoconfiguração para usar a API do Gemini em um aplicativo Spring Boot, semelhante ao que spring-ai-openai-spring-boot-starteré fornecido pelo OpenAI.

Etapa 1: Configure seu ambiente

Ao usar a IA do Google em um projeto Spring AI, precisamos configurar duas propriedades no application.propertiesarquivo (ou yaml):

spring.ai.vertex.ai.gemini.projectId=${GEMINI_PROJECT_ID}
spring.ai.vertex.ai.gemini.location=us-east4

Embora o ID do projeto possa ser compartilhado, é uma boa prática também passar esse valor como uma variável de ambiente caso você esteja usando projetos diferentes para ambientes diferentes (DEV, QA, PROD).

Etapa 2: Crie o serviço de bate-papo

Vamos criar nosso SimpleChatServiceque lida com as interações da IA:

// filepath: api-ai/src/main/java/com/loiane/api_ai/chat/SimpleChatService.java
package com.loiane.api_ai.chat;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class SimpleChatService {

    private final ChatClient chatClient;

    public SimpleChatService(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    public String chat(String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

Onde:

  • Oferece ChatClientuma API para comunicação com um modelo de IA. Suporta modelos de programação síncrona e de streaming.
  • ChatClienté criado usando um ChatClient.Builderobjeto. Você pode obter uma instância autoconfigurada ChatClient.Builderpara qualquer configuração automática do ChatModel Spring Boot ou criar uma programaticamente.
  • chat methodrecebe um message. Usaremos o Promptobjeto do ChatCliente definiremos a entrada do usuário, passando a mensagem recebida. O call()método envia uma solicitação ao modelo de IA e content()retorna a resposta do modelo de IA como uma String.

E o que está acontecendo aqui?

  • Estamos usando o Spring AI ChatClientpara interagir com o OpenAI.
  • O serviço é anotado @Servicepara injeção de dependência.
  • chatmétodo recebe uma mensagem do usuário e retorna a resposta da IA.

Etapa 3: Crie o controlador REST

Agora vamos criar o controlador que expõe nossa funcionalidade de bate-papo:

// filepath: api-ai/src/main/java/com/loiane/api_ai/chat/ChatController.java
package com.loiane.api_ai.chat;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final SimpleChatService chatService;

    public ChatController(SimpleChatService chatService) {
        this.chatService = chatService;
    }

    @PostMapping
    public ChatResponse chat(@RequestBody String message) {
        return new ChatResponse(this.simpleChatService.chat(message));
    }
}

Pontos principais:

  • Estamos usando injeção de construtor para a dependência de serviço como prática recomendada (e não a @Autowiredanotação).
  • Este controlador manipula solicitações POST para o /api/chatendpoint, extraindo a mensagem do corpo da solicitação.

ChatResponseé um recordpara que possamos formatar a saída:

public record ChatResponse(String message) {}

Etapa 4: Teste via solicitação HTTP

Se você estiver usando o IntelliJ IDEA Ultimate , poderá criar um arquivo api.httpcom o seguinte conteúdo (isso é muito conveniente para testes de solicitações HTTP):

POST http://localhost:8080/api/chat
Content-Type: application/json

{
    "message": "Tell me a joke"
}

Como alternativa, você também pode usar o PostMan ou ferramentas semelhantes para simular a solicitação HTTP.

Se enviarmos a solicitação, poderemos obter algo como a seguinte saída:

{
  "message": "Why don't scientists trust atoms? \n\nBecause they make up everything! \n \nLet me know if you'd like to hear another one! 😄  \n"
}

Criando o Frontend Angular

Gere um novo projeto Angular usando Angular CLI:

ng new angular-ai --routing

Etapa 1: adicionar material angular ao projeto

Antes de começar a adicionar componentes e serviços, vamos instalar o Angular Material como nossa biblioteca de componentes:

ng add @angular/material

Siga as instruções selecionando seu tema preferido (estou usando Azure and Blue) e se você gostaria de adicionar a tipografia.

Todas as etapas também estão documentadas aqui: https://material.angular.dev/guide/getting-started

Etapa 2: Crie o serviço de bate-papo

Primeiro, vamos criar um serviço para lidar com a comunicação da API:

ng generate service chat/chat-service

Observe que, desde o Angular v20, a convenção de nomenclatura mudou e, em vez de um arquivo chat-service.ts, não estamos sufixando o servicenome com um hiffen.

// filepath: angular-ai/src/app/chat/chat-service.ts
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ChatResponse } from './chat-response';

@Injectable({
  providedIn: 'root'
})
export class ChatService {

  private readonly API = '/api/chat';
  private readonly http = inject(HttpClient);

  sendChatMessage(message: string): Observable<ChatResponse> {
    return this.http.post<ChatResponse>(this.API, { message });
  }
}

O que está acontecendo aqui?

  • private readonly API = '/api/chat': Definimos uma constante para o ponto de extremidade da nossa API. O uso readonlygarante que esse valor não possa ser alterado após a inicialização, o que é uma boa prática para valores de configuração.
  • private readonly http = inject(HttpClient): Esta é a sintaxe moderna de injeção de dependência do Angular usando a inject()função. É uma alternativa à injeção de construtor e é particularmente útil em serviços. A função HttpClienté injetada para lidar com solicitações HTTP.
  • sendChatMessage(message: string): Observable<ChatResponse>: Este método recebe uma mensagem do usuário como parâmetro de string e retorna um Observable<ChatResponse>. O Observable nos permite lidar com respostas HTTP assíncronas de forma reativa.
  • return this.http.post<ChatResponse>(this.API, { message }): Fazemos uma solicitação POST para o nosso backend Spring Boot. O tipo <ChatResponse>informa ao TypeScript qual tipo de resposta esperar. O segundo parâmetro { message }cria um objeto JSON com a mensagem do usuário que corresponde ao que o nosso controlador Spring Boot espera.

Pontos principais:

  • Estamos usando inject()a função moderna do Angular para injeção de dependência em vez de injeção de construtor.
  • O serviço gerencia a comunicação HTTP com nosso backend Spring Boot.
  • O método retorna um Observable, pois, neste momento, httpResourcenão é recomendado para requisições POST, então continuamos usando HttpClient.
  • { message }sintaxe cria uma carga JSON que nosso Spring Boot @RequestBodypode desserializar.

Em seguida, vamos criar a ChatResponseinterface que corresponde ao ChatResponseregistro. Use o seguinte comando para criar o arquivo:

ng generate interface chat/chat-response

Com o seguinte conteúdo:

// filepath: angular-ai/src/app/chat/chat-response.ts
export interface ChatMessage {
  message: string;
}

Etapa 3: gerar o componente de bate-papo

Vamos criar nosso componente de bate-papo simples:

ng generate component chat/simple-chat

Confira o novo estilo Angular v2025+: https://angular.dev/style-guide . Observe que o nome não é mais simple-chat.componentts, mas simple-chat.tssim uma convenção de nomenclatura mais clara!

Agora vamos implementar nosso componente de bate-papo:

// filepath: angular-ai/src/app/chat/simple-chat/simple-chat.ts
import { Component, effect, ElementRef, inject, signal, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatToolbar } from '@angular/material/toolbar';
import { catchError, of } from 'rxjs';
import { ChatResponse } from '../chat-response';
import { ChatService } from '../chat-service';

@Component({
  selector: 'app-simple-chat',
  imports: [MatCardModule, MatInputModule, MatButtonModule, FormsModule, MatToolbar, MatIconModule],
  templateUrl: './simple-chat.html',
  styleUrl: './simple-chat.scss'
})
export class SimpleChat {

  private readonly chatHistory = viewChild.required<ElementRef>('chatHistory');
  private readonly chatService = inject(ChatService);
  private readonly local = true;

  userInput = '';
  isLoading = false;

  messages = signal<ChatResponse[]>([
    { message: 'Hello, how can I help you today?', isBot: true },
  ]);

  // Effect to auto-scroll when messages change
  private readonly autoScrollEffect = effect(() => {
    this.messages(); // Read the signal to track changes
    setTimeout(() => this.scrollToBottom(), 0); // Use setTimeout to ensure DOM is updated
  });

  sendMessage(): void {
    this.trimUserMessage();
    if (this.userInput !== '' && !this.isLoading) {
      this.updateMessages(this.userInput);
      this.isLoading = true;
      if (this.local) {
        this.simulateResponse();
      } else {
        this.sendChatMessage();
      }
    }
  }

  private trimUserMessage() {
    this.userInput = this.userInput.trim();
  }

  private updateMessages(message: string, isBot = false) {
    this.messages.update(messages => [...messages, { message, isBot }]);
  }

  private getResponse() {
    setTimeout(() => {
      const response = 'This is a simulated response from the AI model.';
      this.updateMessages(response, true);
      this.isLoading = false;
    }, 2000);
  }

  private simulateResponse() {
    this.getResponse();
    this.userInput = '';
  }

  private scrollToBottom(): void {
    try {
      const chatElement = this.chatHistory();
      if (chatElement?.nativeElement) {
        chatElement.nativeElement.scrollTop = chatElement.nativeElement.scrollHeight;
      }
    } catch (err) {
      console.error('Failed to scroll chat history:', err);
    }
  }

  private sendChatMessage() {
    this.chatService.sendChatMessage(this.userInput)
    .pipe(
      catchError(() => {
        this.updateMessages('Sorry, I am unable to process your request at the moment.', true);
        this.isLoading = false;
        return of();
      })
    )
    .subscribe((response: ChatResponse) => {
      if (response) {
        this.updateMessages(response.message, true);
      }
      this.userInput = '';
      this.isLoading = false;
    });
  }
}

O que está acontecendo aqui?

  • private readonly chatHistory = viewChild.required<ElementRef>('chatHistory'): Isso usa o sinal moderno do Angular viewChildpara obter uma referência ao elemento DOM do histórico de bate-papo. O requiredmétodo garante que o elemento exista, e usaremos essa referência para a funcionalidade de rolagem automática.
  • private readonly chatService = inject(ChatService): Estamos injetando nosso serviço de chat usando inject()a função moderna do Angular em vez da injeção de construtor. Esta é a abordagem recomendada em aplicações Angular modernas.
  • private readonly local = true: Este é um sinalizador de alternância que nos permite alternar entre o modo de simulação local (para testes) e chamadas de API reais. Quando true, simula respostas; quando false, chama o backend real do Spring Boot.
  • messages = signal<ChatResponse[]>([...]): Estamos usando sinais do Angular para gerenciar o estado das nossas mensagens de bate-papo. Os sinais são a nova primitiva reativa do Angular que rastreia alterações automaticamente e atualiza a interface do usuário com eficiência. Inicializamos isso com uma mensagem de boas-vindas do bot.
  • private readonly autoScrollEffect = effect(() => {...}): Isso cria um efeito que rola automaticamente para o final do chat sempre que novas mensagens são adicionadas. Os efeitos no Angular são executados sempre que suas dependências (neste caso, o messagessinal) mudam. Este é um dos meus casos de uso favoritos effectsem vez de console.log.
  • sendMessage(): Este é o método principal que lida com o envio de mensagens. Ele valida a entrada, adiciona a mensagem do usuário ao chat e simula uma resposta ou chama a API real com base no localsinalizador.
  • private updateMessages(message: string, isBot = false): Um método utilitário que atualiza o sinal de mensagens criando uma nova matriz com a mensagem adicional. O isBotparâmetro determina se a mensagem é do usuário ou do assistente de IA.
  • private sendChatMessage(): Este método manipula a solicitação HTTP real para o nosso backend Spring Boot. Ele usa operadores RxJS como catchErrorpara lidar com erros de forma elegante e subscribeprocessar a resposta.

Principais conceitos angulares utilizados:

  • Sinais : Para gerenciamento de estado reativo ( messagessinal).
  • Efeitos : Para efeitos colaterais automáticos, como rolagem ( autoScrollEffect).
  • ViewChild : Para referências de elementos DOM ( chatHistory).
  • Injeção de dependência moderna : usando inject()função.
  • Componentes autônomos : não há necessidade de declarações NgModule.
  • RxJS : Para manipular operações HTTP assíncronas para a chamada POST para a API Spring.

Este componente demonstra padrões Angular modernos e fornece uma interface de bate-papo limpa e reativa que pode funcionar tanto no modo de simulação para testes quanto com o backend real do Spring Boot.

Etapa 4: Crie o modelo de bate-papo

Aqui está o HTML que se conecta ao nosso componente:

<!-- filepath: angular-ai/src/app/chat/simple-chat/simple-chat.html -->
<mat-card class="chat-container">
  <div class="chat-header">
    <mat-toolbar>
      <span>Simple Chat</span>
    </mat-toolbar>
  </div>
  <div class="chat-history" #chatHistory>
    <div class="messages">
    @for (message of messages(); track message) {
      <div class="message">
        <div class="message-bubble" [class.user]="!message.isBot">
          
        </div>
      </div>
    }
    @if (isLoading) {
      <div class="message">
        <div class="message-bubble">
          <span class="typing">...</span>
        </div>
      </div>
    }
  </div>
  </div>

  <div class="chat-input">
    <mat-form-field class="full-width">
      <mat-label>Ask anything</mat-label>
      <input matInput [(ngModel)]="userInput" (keyup.enter)="sendMessage()">
      @if (userInput) {
        <button matSuffix mat-icon-button aria-label="Send" (click)="sendMessage()" [disabled]="isLoading">
          <mat-icon>send</mat-icon>
        </button>
     }
    </mat-form-field>
  </div>
</mat-card>

O que está acontecendo aqui?

  • <mat-card class="chat-container">:Estamos usando o componente card do Angular Material como o contêiner principal para nossa interface de bate-papo.
  • <div class="chat-header">com <mat-toolbar>: Cria uma seção de cabeçalho usando o componente de barra de ferramentas do Angular Material.
  • <div class="chat-history" #chatHistory>: Este é o nosso contêiner de mensagens rolável. A #chatHistoryvariável de referência do modelo nos permite acessar este elemento DOM a partir do nosso componente usando viewChilda funcionalidade de rolagem automática.
  • @for (message of messages(); track message; let system = $even): Esta é a nova sintaxe de fluxo de controle do Angular (introduzida no Angular v17). Estamos iterando sobre nosso messages()sinal, usando a trackcláusula para otimização de desempenho.
  • [class.user]="!message.isBot": Esta é uma vinculação de classe que aplica condicionalmente a userclasse CSS quando a mensagem NÃO é do bot. Isso nos permite estilizar as mensagens do usuário de forma diferente das mensagens do bot (normalmente alinhadas à direita e à esquerda) e de uma forma preferencial em vez de NgClassdiretivas.
  • @if (isLoading): Outro exemplo da nova sintaxe de fluxo de controle do Angular. Isso mostra condicionalmente um indicador de carregamento com pontos de digitação ( ...) quando a IA está processando uma resposta.
  • <mat-form-field class="full-width">: Componente de campo de formulário do Angular Material que fornece o estilo de entrada e a funcionalidade de rótulo. A full-widthclasse garante que ocupe o espaço disponível.
  • [(ngModel)]="userInput": Vinculação de dados bidirecional que conecta o campo de entrada à userInputpropriedade do nosso componente. Quando o usuário digita, ele atualiza a propriedade; quando a propriedade muda, ele atualiza a entrada. Como temos apenas um campo, não há necessidade de formulários complexos aqui.
  • (keyup.enter)="sendMessage()": Vinculação de evento que chama nosso sendMessage()método quando o usuário pressiona a tecla Enter. Isso fornece uma maneira conveniente de enviar mensagens sem clicar no botão.
  • @if (userInput): Exibe o botão de envio apenas quando há texto no campo de entrada. Este é um toque agradável de UX que evita o envio de mensagens vazias.
  • [disabled]="isLoading": Vinculação de propriedade que desabilita o botão de envio enquanto uma solicitação está em andamento, impedindo múltiplos envios.

Principais conceitos angulares utilizados:

  • Nova sintaxe de fluxo de controle : @for@ifem vez de *ngFore*ngIf
  • Variáveis de referência de modelo : #chatHistorypara acesso DOM
  • Ligação de classe : [class.user]para estilo condicional
  • Vinculação de eventos : (click)(keyup.enter)para interações do usuário
  • Vinculação de propriedade : [disabled]para estados de botões dinâmicos
  • Ligação de dados bidirecional : [(ngModel)]para entrada de formulário
  • Componentes de materiais angulares : mat-cardmat-toolbarmat-form-field, etc.

Etapa 5: estilize a interface de bate-papo

Agora vamos adicionar algum estilo ao nosso HTML:

// filepath: angular-ai/src/app/chat/simple-chat/simple-chat.component.scss
.chat-container {
  display: flex;
  flex-direction: column;
  height: 80vh;
  margin: 36px;
}

.chat-header {
  flex: 0 0 auto; /* Header doesn't grow or shrink */
}

.chat-history {
  flex: 1 1 auto; /* Chat history takes up available space */
  overflow-y: auto; /* Add scrollbar if content overflows */
  padding: 10px;
}

.messages {
  flex: 1;
  margin-bottom: 16px;
  overflow-y: auto;
  padding-right: 8px;
}

.message {
  margin-bottom: 8px;
}

.message-bubble {
  padding: 10px;
  border-radius: 10px;
  max-width: 80%;
  display: inline-block;
}

.user {
  background-color: #d7e3ff; /* Light blue for user messages */
  border-radius: 10px;
  align-self: flex-end; /* Align to the right */
  margin-left: auto;
}


.chat-input {
  flex: 0 0 auto; /* Input bar doesn't grow or shrink */
  padding: 10px;
}

.full-width {
  width: 100%;
}

.typing {
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  animation: typing 1s steps(10) infinite alternate; // changed from 2s to 1s
}

@keyframes typing {
  from {
    width: 0;
  }
  to {
    width: 100%;
  }
}

O que está acontecendo aqui?

  • .chat-container: Este é o nosso contêiner principal usando CSS Flexbox. Definimos que ele flex-direction: columnempilhe elementos verticalmente, com uma altura fixa de 80vh(80% da altura da janela de visualização) e margem consistente. Isso cria a base para o layout do nosso chat.
  • .chat-header: Usos flex: 0 0 autoque significam que ele não cresce nem encolhe — mantém seu tamanho natural. Isso garante que nosso cabeçalho permaneça no topo com altura consistente, independentemente das alterações de conteúdo.
  • .chat-history: Esta é a área de mensagens rolável. Usamos flex: 1 1 autopara que ocupe todo o espaço disponível entre o cabeçalho e a entrada. overflow-y: autoAdiciona uma barra de rolagem quando as mensagens excedem a altura do contêiner.
  • .messages: Estilo adicional para o contêiner de mensagens com espaçamento e preenchimento adequados. padding-right: 8pxConsidera o espaço da barra de rolagem.
  • .message: Espaçamento simples entre mensagens individuais usado margin-bottom: 8pxpara criar separação visual.
  • .message-bubble: Isso estiliza cada balão de mensagem com cantos arredondados, preenchimento e uma largura máxima de 80% para evitar que as mensagens ocupem toda a largura. Isso display: inline-blockpermite o alinhamento adequado.
  • .user: Esta classe é aplicada condicionalmente às mensagens do usuário (lembre-se [class.user]="!message.isBot"do modelo). Ela usa um fundo azul-claro margin-left: autopara empurrar as mensagens do usuário para o lado direito do chat, criando o padrão típico de alinhamento de chat.
  • .chat-input: Semelhante ao cabeçalho, este serve flex: 0 0 autopara manter seu tamanho na parte inferior do contêiner. O preenchimento garante o espaçamento adequado das bordas.
  • .full-width: Uma classe utilitária que faz com que o campo de entrada ocupe toda a largura disponível dentro de seu contêiner.
  • .typing: Isso cria o indicador de digitação animado. Usamos overflow: hiddenwhite-space: nowrappara controlar a exibição do texto e, em seguida, aplicamos uma animação personalizada que cria o efeito de digitação.
  • @keyframes typing: Isso define a animação que faz com que os pontos de digitação pareçam “digitar”, animando a largura de 0 a 100%. Isso steps(10) infinite alternatecria um efeito de digitação suave e contínua.

Etapa 6: Atualizar o componente do aplicativo

Agora que nossa interface de bate-papo está pronta, vamos voltar ao componente principal e adicionar o código restante.

// filepath: angular-ai/src/app/app.ts
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterLink, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, MatToolbarModule, MatButtonModule, MatIconModule, RouterLink],
  templateUrl: './app.html',
  styleUrl: './app.scss'
})
export class App {
  readonly title = 'AI-Spring-Angular';
}

Este componente de aplicativo serve como nosso shell de aplicativo, fornecendo a barra de ferramentas de navegação principal e uma saída de roteador onde nosso componente de bate-papo será exibido.

app.htmlarquivo:

<mat-toolbar>
  <span class="title"></span>
  <button mat-button aria-label="Simple Chat" routerLink="/simple-chat">Simple Chat</button>
</mat-toolbar>
<router-outlet></router-outlet>

E finalmente, o arquivo SCSS:

.title {
  margin-right: 20px;
}

Etapa 7: Rotas e configuração principal

Se um app.routesarquivo não foi criado durante a criação do projeto, vá em frente e crie um com o seguinte conteúdo:

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

export const routes: Routes = [
  { path: '', redirectTo: 'simple-chat', pathMatch: 'full' },
  { path: 'simple-chat',
    loadComponent: () => import('./chat/simple-chat/simple-chat').then(c => c.SimpleChat)
  },
  { path: '**', redirectTo: 'simple-chat' }
];

O que está acontecendo aqui?

  • { path: '', redirectTo: 'simple-chat', pathMatch: 'full' }: Esta é a configuração de rota padrão. Quando os usuários acessam a URL raiz ( /), eles serão redirecionados automaticamente para /simple-chat. Isso pathMatch: 'full'garante que esse redirecionamento só aconteça quando o caminho estiver exatamente vazio (não apenas quando começar com vazio).
  • { path: 'simple-chat', loadComponent: () => import('./chat/simple-chat/simple-chat').then(c => c.SimpleChat) }: Isso define nossa rota de bate-papo usando carregamento lento .
    • loadComponent: Usa importações dinâmicas para componentes autônomos.
    • import('./chat/simple-chat/simple-chat'): Importa dinamicamente nosso componente de bate-papo
    • .then(c => c.SimpleChat): Extrai a SimpleChatclasse do módulo importado
  • { path: '**', redirectTo: 'simple-chat' }: Esta é a rota curinga que captura quaisquer URLs não correspondentes. Se alguém tentar acessar uma rota inexistente, será redirecionado para o nosso componente de chat. Esta é uma boa prática de UX para lidar com cenários do tipo 404.

E finalmente, vamos revisar o app.config.tsarquivo:

import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideZonelessChangeDetection(),
    provideRouter(routes),
    provideHttpClient()
  ]
};

O que está acontecendo aqui?

  • import { provideHttpClient } from '@angular/common/http': Isso importa o provedor de cliente HTTP que precisamos para fazer chamadas de API para nosso backend do Spring Boot.
  • provideZonelessChangeDetection(): Esta é a nova estratégia experimental de detecção de alterações do Angular que não depende do Zone.js. Ela tem melhor desempenho e funciona particularmente bem com sinais, que estamos usando em todo o nosso componente de bate-papo.
  • provideRouter(routes): Isso configura o sistema de roteamento do Angular com nossas definições de rota. Ele pega nosso routesarray e configura o sistema de navegação para nossa aplicação.
  • provideHttpClient(): Isso configura o cliente HTTP do Angular para fazer solicitações de API. Precisamos disso para nos comunicar com nosso backend do Spring Boot ao enviar mensagens de bate-papo.

Esta configuração representa a abordagem moderna do Angular para a configuração de aplicações. É mais explícita, adaptável a árvores e com melhor desempenho do que a abordagem tradicional do NgModule. provideZonelessChangeDetection()É particularmente interessante, pois funciona perfeitamente com nosso componente de bate-papo baseado em sinais, proporcionando melhor desempenho sem a sobrecarga do Zone.js.

Etapa 8: Proxy para a API

Pessoalmente, não sou muito fã de usar CORS se não for necessário. Para desenvolvimento local, isso definitivamente não é necessário e considero uma boa prática não usar CORS localmente. Por esse motivo, prefiro sempre criar um proxy.conf.jsarquivo na pasta raiz do projeto:

// file path: angular-ai/proxy.conf.js
const PROXY_CONFIG = [
  {
    context: ["/api"],
    target: "http://localhost:8080/",
    secure: false,
    logLevel: "debug",
  },
];
module.exports = PROXY_CONFIG;

O que está acontecendo aqui?

  • const PROXY_CONFIG = [...]: Isso cria uma matriz de configuração de proxy que informa ao servidor de desenvolvimento do Angular como lidar com solicitações de API durante o desenvolvimento.
  • context: ["/api"]: Especifica quais caminhos de URL devem ser proxy. Qualquer solicitação que comece com /apiserá interceptada e encaminhada para o nosso backend Spring Boot. Isso corresponde ao endpoint que definimos em nosso ChatServiceprivate readonly API = '/api/chat').
  • target: "http://localhost:8080/": Este é o destino para onde as solicitações proxy serão encaminhadas. Como nosso aplicativo Spring Boot roda na porta 8080 por padrão, todas as /apisolicitações do nosso aplicativo Angular (rodando na porta 4200) serão enviadas para .http://localhost:8080/api
  • secure: false: Isso desabilita a verificação do certificado SSL. Como estamos trabalhando com HTTP (não HTTPS) no desenvolvimento local, configuramos essa opção falsepara evitar problemas relacionados a SSL.
  • logLevel: "debug": Isso permite o registro detalhado das operações de proxy no console. É útil para depurar problemas de proxy durante o desenvolvimento — você verá exatamente quais solicitações estão sendo processadas por proxy e para onde estão indo.
  • module.exports = PROXY_CONFIG: Isso exporta a configuração para que o Angular CLI possa usá-la ao iniciar o servidor de desenvolvimento.

Por que usar um proxy em vez do CORS?

  • Desenvolvimento mais limpo: não há necessidade de configurar cabeçalhos CORS no seu aplicativo Spring Boot para desenvolvimento local.
  • Ambiente semelhante ao de produção: seu frontend faz solicitações para a mesma origem, o que é mais semelhante à configuração de produção.
  • Segurança: evita a complexidade e potenciais problemas de segurança da configuração do CORS.
  • Simplicidade: um único arquivo de configuração lida com todo o roteamento da API.

Para usar este proxy, você precisará iniciar seu servidor de desenvolvimento Angular com ( package.json):

"start": "ng serve --proxy-config proxy.conf.js -o"

Use npm run startem vez de ng servediretamente.

Ou adicione-o ao seu angular.jsonarquivo na configuração de serviço:

"serve": {
  "builder": "@angular-devkit/build-angular:dev-server",
  "options": {
    "proxyConfig": "proxy.conf.js"
  }
}

Essa configuração garante que, quando seu aplicativo Angular fizer uma solicitação /api/chat, ela seja encaminhada automaticamente para onde nosso backend do Spring Boot está escutando.http://localhost:8080/api/chat

Testando o aplicativo

Etapa 1: iniciar o backend

Navegue até o api-aidiretório e execute:

./mvnw spring-boot:run

O backend deve iniciar na porta 8080.

Etapa 2: iniciar o frontend

No angular-aidiretório, execute:

npm start

O aplicativo Angular iniciará na porta 4200 e será aberto automaticamente no seu navegador ( -oparâmetro que adicionamos ao startscript).

Etapa 3: Teste o bate-papo

  1. Abra seu navegador e vá parahttp://localhost:4200
  2. Navegue até a seção Bate-papo Simples
  3. Digite uma mensagem e pressione Enter ou clique em Enviar
  4. Você deverá ver a resposta da IA aparecer no chat

Dica : Tente fazer perguntas como “O que é Spring AI?” ou “Explique a injeção de dependência no Spring” para testar o conhecimento da IA.

espaço reservado

Conclusão

Parabéns! 🎉 Você construiu com sucesso um aplicativo de bate-papo com IA simples usando Spring AI e Angular. Esta base oferece tudo o que você precisa para criar aplicativos mais sofisticados com IA.

Neste tutorial, abordamos:

  • Configurando o Spring AI com a integração do Gemini
  • Criando uma API RESTful para funcionalidade de chat
  • Construindo uma interface de bate-papo Angular responsiva
  • Conectando o frontend e o backend
  • Lidando com interações do usuário em tempo real

O que vem a seguir?

  • Adicione memória de bate-papo para manter o contexto da conversa
  • Implementar diferentes modelos ou provedores de IA
  • Adicionar autenticação de usuário
  • Armazene o histórico de bate-papo em um banco de dados

Boa codificação! 🚀

Quer o código? Acesse o GitHub : https://github.com/loiane/spring-ai-angular

Referências