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
- Pré-requisitos
- Estrutura do Projeto
- Configurando o Spring AI Backend
- Criando o Frontend Angular
- Etapa 1: adicionar material angular ao projeto
- Etapa 2: Crie o serviço de bate-papo
- Etapa 3: gerar o componente de bate-papo
- Etapa 4: Crie o modelo de bate-papo
- Etapa 5: estilize a interface de bate-papo
- Etapa 6: Atualizar o componente do aplicativo
- Etapa 7: Rotas e configuração principal
- Etapa 8: Proxy para a API
- Testando o aplicativo
- Conclusão
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
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.xml
de 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-starter
fornece 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.properties
arquivo (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 SimpleChatService
que 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
ChatClient
uma API para comunicação com um modelo de IA. Suporta modelos de programação síncrona e de streaming. - O
ChatClient
é criado usando umChatClient.Builder
objeto. Você pode obter uma instância autoconfiguradaChatClient.Builder
para qualquer configuração automática do ChatModel Spring Boot ou criar uma programaticamente. - O
chat method
recebe ummessage
. Usaremos oPrompt
objeto doChatClient
e definiremos a entrada do usuário, passando a mensagem recebida. Ocall()
método envia uma solicitação ao modelo de IA econtent()
retorna a resposta do modelo de IA como uma String.
E o que está acontecendo aqui?
- Estamos usando o Spring AI
ChatClient
para interagir com o OpenAI. - O serviço é anotado
@Service
para injeção de dependência. - O
chat
mé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
@Autowired
anotação). - Este controlador manipula solicitações POST para o
/api/chat
endpoint, extraindo a mensagem do corpo da solicitação.
O ChatResponse
é um record
para 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.http
com 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
service
nome 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 usoreadonly
garante 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 ainject()
função. É uma alternativa à injeção de construtor e é particularmente útil em serviços. A funçãoHttpClient
é 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 umObservable<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,
httpResource
não é recomendado para requisições POST, então continuamos usandoHttpClient
. - A
{ message }
sintaxe cria uma carga JSON que nosso Spring Boot@RequestBody
pode desserializar.
Em seguida, vamos criar a ChatResponse
interface que corresponde ao ChatResponse
registro. 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
, massimple-chat.ts
sim 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 AngularviewChild
para obter uma referência ao elemento DOM do histórico de bate-papo. Orequired
mé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 usandoinject()
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. Quandotrue
, simula respostas; quandofalse
, 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, omessages
sinal) mudam. Este é um dos meus casos de uso favoritoseffects
em vez deconsole.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 nolocal
sinalizador.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. OisBot
parâ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 comocatchError
para lidar com erros de forma elegante esubscribe
processar a resposta.
Principais conceitos angulares utilizados:
- Sinais : Para gerenciamento de estado reativo (
messages
sinal). - 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#chatHistory
variável de referência do modelo nos permite acessar este elemento DOM a partir do nosso componente usandoviewChild
a 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 nossomessages()
sinal, usando atrack
cláusula para otimização de desempenho.[class.user]="!message.isBot"
: Esta é uma vinculação de classe que aplica condicionalmente auser
classe 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 deNgClass
diretivas.@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. Afull-width
classe garante que ocupe o espaço disponível.[(ngModel)]="userInput"
: Vinculação de dados bidirecional que conecta o campo de entrada àuserInput
propriedade 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 nossosendMessage()
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
e@if
em vez de*ngFor
e*ngIf
- Variáveis de referência de modelo :
#chatHistory
para acesso DOM - Ligação de classe :
[class.user]
para estilo condicional - Vinculação de eventos :
(click)
e(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-card
,mat-toolbar
,mat-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 eleflex-direction: column
empilhe elementos verticalmente, com uma altura fixa de80vh
(80% da altura da janela de visualização) e margem consistente. Isso cria a base para o layout do nosso chat..chat-header
: Usosflex: 0 0 auto
que 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. Usamosflex: 1 1 auto
para que ocupe todo o espaço disponível entre o cabeçalho e a entrada.overflow-y: auto
Adiciona 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: 8px
Considera o espaço da barra de rolagem..message
: Espaçamento simples entre mensagens individuais usadomargin-bottom: 8px
para 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. Issodisplay: inline-block
permite 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-claromargin-left: auto
para 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 serveflex: 0 0 auto
para 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. Usamosoverflow: hidden
ewhite-space: nowrap
para 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%. Issosteps(10) infinite alternate
cria 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.
O app.html
arquivo:
<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.routes
arquivo 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
. IssopathMatch: '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 aSimpleChat
classe 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.ts
arquivo:
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 nossoroutes
array 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.js
arquivo 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/api
será interceptada e encaminhada para o nosso backend Spring Boot. Isso corresponde ao endpoint que definimos em nossoChatService
(private 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/api
solicitaçõ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çãofalse
para 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 start
em vez de ng serve
diretamente.
Ou adicione-o ao seu angular.json
arquivo 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-ai
diretório e execute:
./mvnw spring-boot:run
O backend deve iniciar na porta 8080.
Etapa 2: iniciar o frontend
No angular-ai
diretório, execute:
npm start
O aplicativo Angular iniciará na porta 4200 e será aberto automaticamente no seu navegador ( -o
parâmetro que adicionamos ao start
script).
Etapa 3: Teste o bate-papo
- Abra seu navegador e vá para
http://localhost:4200
- Navegue até a seção Bate-papo Simples
- Digite uma mensagem e pressione Enter ou clique em Enviar
- 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.
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