Integração do Spring Boot com DeepSeek via WebSocket e Depuração Frontend

Este artigo demonstra como integrar o Spring Boot com o modelo DeepSeek (versão Siliconflow) utilizando WebSockets para comunicação em tempo real. Abordaremos a configuração do backend em Java com Spring Boot e a criação de uma interface frontend simples para interagir com o modelo.

Recursos Adicionais:

Configuração do Projeto

  • Java: 1.8
  • SpringBoot: 2.7.7
  • deepseek-spring-boot-starter: 1.1.0
  • spring-boot-starter-websocket: 2.7.7

Estrutura do Projeto

A estrutura do projeto é organizada da seguinte forma:


       (Diagrama da estrutura do projeto seria inserido aqui)
   

Código do Projeto

1. Dependências (pom.xml)


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>org.example</groupId>
   <artifactId>deepseek-ws-demo</artifactId>
   <version>1.0-SNAPSHOT</version>
   <packaging>jar</packaging>

   <properties>
       <java.version>1.8</java.version>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <spring-boot.version>2.7.7</spring-boot.version>
       <deepseek.starter.version>1.1.0</deepseek.starter.version>
   </properties>

   <dependencies>
       <!-- Lombok -->
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>1.18.34</version>
           <scope>provided</scope>
       </dependency>

       <!-- DeepSeek Spring Boot Starter -->
       <dependency>
           <groupId>io.github.pig-mesh.ai</groupId>
           <artifactId>deepseek-spring-boot-starter</artifactId>
           <version>${deepseek.starter.version}</version>
       </dependency>

       <!-- Spring Boot Web -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
           <version>${spring-boot.version}</version>
       </dependency>

       <!-- Logging -->
       <dependency>
           <groupId>ch.qos.logback</groupId>
           <artifactId>logback-classic</artifactId>
           <version>1.2.11</version>
       </dependency>

       <!-- Spring Boot WebSocket -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-websocket</artifactId>
           <version>${spring-boot.version}</version>
       </dependency>
   </dependencies>

   <build>
       <plugins>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <version>3.8.1</version>
               <configuration>
                   <source>1.8</source>
                   <target>1.8</target>
                   <encoding>UTF-8</encoding>
               </configuration>
           </plugin>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <version>${spring-boot.version}</version>
               <configuration>
                   <mainClass>org.example.DemoApplication</mainClass>
               </configuration>
               <executions>
                   <execution>
                       <id>repackage</id>
                       <goals>
                           <goal>repackage</goal>
                       </goals>
                   </execution>
               </executions>
           </plugin>
       </plugins>
   </build>
</project>
   

2. Controlador REST para Configuração (DeepSeekController.java)


package org.example.controller;

import lombok.extern.slf4j.Slf4j;
import org.example.model.ChatConfigParams;
import org.example.websocket.DeepSeekChatHandler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@Slf4j
@RequestMapping("/api/deepseek")
public class DeepSeekController {

   @PostMapping("/configureSession")
   public ResponseEntity<Void> configureSession(@RequestBody ChatConfigParams params) {
       String sessionId = params.getSessionId();
       if (!StringUtils.hasText(sessionId)) {
           log.warn("Tentativa de configurar sessão com sessionId inválido.");
           return ResponseEntity.badRequest().build();
       }

       DeepSeekChatHandler.updateSessionParameters(sessionId, params);
       log.info("Parâmetros de sessão configurados para sessionId: {}", sessionId);
       return ResponseEntity.ok().build();
   }
}
   

3. Classe de Inicialização da Aplicação (DemoApplication.java)


package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.filter.CommonsRequestLoggingFilter;

@SpringBootApplication
public class DemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(DemoApplication.class, args);
   }

   // Bean opcional para habilitar CORS de forma mais granular se necessário
   // @Bean
   // public org.springframework.web.servlet.config.annotation.WebMvcConfigurer corsConfigurer() {
   //     return new org.springframework.web.servlet.config.annotation.WebMvcConfigurer() {
   //         @Override
   //         public void addCorsMappings(org.springframework.web.servlet.config.annotation.CorsRegistry registry) {
   //             registry.addMapping("/**")
   //                     .allowedOrigins("*")
   //                     .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
   //                     .allowedHeaders("*")
   //                     .exposedHeaders("Cache-Control", "Connection", "Content-Length", "Content-Type", "Date", "ETag", "Expires", "Keep-Alive", "Last-Modified", "Pragma", "Server", "Transfer-Encoding", "Upgrade")
   //                     .allowCredentials(true)
   //                     .maxAge(3600);
   //         }
   //     };
   // }

    // Bean para registrar logs de requisição (útil para depuração)
    @Bean
    public CommonsRequestLoggingFilter requestLoggingFilter() {
        CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
        loggingFilter.setIncludeClientInfo(true);
        loggingFilter.setIncludeQueryString(true);
        loggingFilter.setIncludePayload(true);
        loggingFilter.setIncludeHeaders(false); // Pode ser útil para ver headers
        return loggingFilter;
    }
}
   

4. Configuração de Logging (logback-spring.xml)


<?xml version="1.0" encoding="UTF-8" ?>
<configuration debug="false">
   <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
       <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
           <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
       </encoder>
   </appender>

   <root level="INFO">
       <appender-ref ref="CONSOLE" />
   </root>

   <!-- Níveis de log mais detalhados para classes específicas, se necessário -->
   <logger name="org.example.websocket.DeepSeekChatHandler" level="DEBUG" />
</configuration>
   

5. Propriedades da Aplicação (application.yaml)


deepseek:
 # URL base da API do Siliconflow
 base-url: https://api.siliconflow.cn/v1
 # Sua chave de API pessoal
 api-key: sk-your-personal-api-key

spring:
 main:
   allow-bean-definition-overriding: true # Permite substituição de beans definidos
 web:
   resources:
     add-mappings: false # Necessário se você for servir o index.html estaticamente de outra forma

server:
 port: 8080
 tomcat:
   keep-alive-timeout: 30000 # Timeout de inatividade do conector
   max-connections: 200      # Número máximo de conexões simultâneas
 servlet:
   encoding:
     charset: UTF-8
     force: true
 compression:
   enabled: false # Desabilita compressão para garantir o streaming
   

6. Configuração WebSocket (WebsocketConfig.java)


package org.example.config;

import io.github.pigmesh.ai.deepseek.core.DeepSeekClient;
import org.example.websocket.DeepSeekChatHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import javax.annotation.Resource;

@Configuration
@EnableWebSocket
public class WebsocketConfig implements WebSocketConfigurer {

   @Resource
   private DeepSeekClient deepSeekClient; // Injetado pelo starter

   @Override
   public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
       // Registra o handler para o endpoint /ws/chat
       registry.addHandler(chatWebSocketHandler(), "/ws/chat")
               .setAllowedOrigins("*"); // Permite origens de qualquer lugar (ajustar em produção)
   }

   @Bean
   public WebSocketHandler chatWebSocketHandler() {
       // Cria e configura o handler personalizado
       return new DeepSeekChatHandler(deepSeekClient);
   }
}
   

7. Handler WebSocket para Interação com DeepSeek (DeepSeekChatHandler.java)


package org.example.websocket;

import io.github.pigmesh.ai.deepseek.core.DeepSeekClient;
import io.github.pigmesh.ai.deepseek.core.chat.ChatCompletionRequest;
import io.github.pigmesh.ai.deepseek.core.chat.ResponseFormatType;
import io.github.pigmesh.ai.deepseek.core.chat.StreamChatCompletionResponse;
import lombok.extern.slf4j.Slf4j;
import org.example.model.ChatConfigParams;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component // Marca como um componente gerenciado pelo Spring
public class DeepSeekChatHandler extends TextWebSocketHandler {

   private static final Map<String, WebSocketSession> activeSessions = new ConcurrentHashMap<>();
   private static final Map<String, ChatConfigParams> sessionConfigurations = new ConcurrentHashMap<>();

   private final DeepSeekClient deepSeekClient;

   public DeepSeekChatHandler(DeepSeekClient deepSeekClient) {
       this.deepSeekClient = deepSeekClient;
   }

   // Método estático para permitir que o controlador configure parâmetros da sessão
   public static void updateSessionParameters(String sessionId, ChatConfigParams params) {
       sessionConfigurations.put(sessionId, params);
       log.debug("Configurações atualizadas para a sessão {}: {}", sessionId, params);
   }

   @Override
   public void afterConnectionEstablished(WebSocketSession session) {
       String sessionId = session.getId();
       activeSessions.put(sessionId, session);
       log.info("Nova conexão WebSocket estabelecida. Session ID: {}", sessionId);

       try {
           // Envia informações iniciais para o cliente
           session.sendMessage(new TextMessage("system_log:Conexão estabelecida com sucesso."));
           session.sendMessage(new TextMessage("system_log:Seu Session ID é: " + sessionId));

           // Carrega configurações padrão ou salvas se disponíveis
           ChatConfigParams defaultConfig = sessionConfigurations.getOrDefault(sessionId, getDefaultChatConfig());
           session.sendMessage(new TextMessage(String.format("config_update:{\"model\":\"%s\", \"temperature\":%.1f, \"frequencyPenalty\":%.1f, \"user\":\"%s\", \"topP\":%.1f, \"maxCompletionTokens\":%d}",
                   defaultConfig.getModel(), defaultConfig.getTemperature(), defaultConfig.getFrequencyPenalty(),
                   defaultConfig.getUser(), defaultConfig.getTopP(), defaultConfig.getMaxCompletionTokens())));

       } catch (IOException e) {
           log.error("Erro ao enviar mensagem inicial para o cliente: {}", sessionId, e);
       }
   }

   @Override
   protected void handleTextMessage(WebSocketSession session, TextMessage message) {
       String userMessage = message.getPayload();
       String sessionId = session.getId();

       if (!StringUtils.hasText(userMessage)) {
           log.warn("Mensagem vazia recebida da sessão: {}", sessionId);
           return;
       }

       log.debug("Mensagem recebida da sessão {}: {}", sessionId, userMessage);

       // Recupera ou define configurações para esta sessão
       ChatConfigParams config = sessionConfigurations.computeIfAbsent(sessionId, k -> getDefaultChatConfig());

       // Cria a requisição para o DeepSeek
       ChatCompletionRequest request = buildDeepSeekRequest(userMessage, config);

       // Envia a requisição e processa o fluxo de respostas
       deepSeekClient.chatFluxCompletion(request)
               .doOnNext(response -> processStreamResponse(session, response))
               .doOnError(error -> handleStreamError(session, sessionId, error))
               .doOnComplete(() -> log.debug("Fluxo de resposta concluído para a sessão: {}", sessionId))
               .subscribe(); // Inicia o fluxo reativo
   }

   private void processStreamResponse(WebSocketSession session, StreamChatCompletionResponse response) {
       try {
           String content = response.choices().get(0).delta().content();
           String reasoningContent = response.choices().get(0).delta().reasoningContent();
           String finishReason = response.choices().get(0).finishReason();

           // Prioriza o conteúdo de raciocínio se presente
           if (StringUtils.hasText(reasoningContent)) {
               session.sendMessage(new TextMessage("reasoning_step:" + reasoningContent));
           } else if (StringUtils.hasText(content)) {
               session.sendMessage(new TextMessage("model_reply:" + content));
           }

           // Indica o fim da resposta
           if ("stop".equals(finishReason)) {
               session.sendMessage(new TextMessage("system_log:Final da resposta gerada."));
               log.info("Geração de texto concluída para a sessão: {}", session.getId());
           }
       } catch (IOException e) {
           log.error("Erro ao enviar resposta via WebSocket para a sessão: {}", session.getId(), e);
       }
   }

   private void handleStreamError(WebSocketSession session, String sessionId, Throwable error) {
       log.error("Erro durante o processamento do fluxo do DeepSeek para a sessão {}: {}", sessionId, error.getMessage(), error);
       try {
           session.sendMessage(new TextMessage("system_log:Ocorreu um erro ao processar sua solicitação. Por favor, tente novamente."));
       } catch (IOException e) {
           log.error("Erro ao enviar mensagem de erro para o cliente na sessão: {}", sessionId, e);
       }
   }

   @Override
   public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
       String sessionId = session.getId();
       activeSessions.remove(sessionId);
       sessionConfigurations.remove(sessionId); // Limpa configurações da sessão encerrada
       log.info("Conexão WebSocket encerrada. Session ID: {}, Status: {}", sessionId, status.getReason());
   }

   // Constrói a requisição para o DeepSeek usando os parâmetros da sessão
   private ChatCompletionRequest buildDeepSeekRequest(String prompt, ChatConfigParams config) {
       return ChatCompletionRequest.builder()
               .addUserMessage(prompt)
               .model(config.getModel())
               .stream(true) // Habilita o streaming de respostas
               .temperature(config.getTemperature())
               .frequencyPenalty(config.getFrequencyPenalty())
               .user(config.getUser())
               .topP(config.getTopP())
               .maxCompletionTokens(config.getMaxCompletionTokens())
               .responseFormat(ResponseFormatType.TEXT) // Formato de resposta de texto simples
               .build();
   }

   // Retorna um objeto de configuração padrão
   private ChatConfigParams getDefaultChatConfig() {
       return new ChatConfigParams("deepseek-ai/DeepSeek-R1", 0.7, 0.5, "default_user", 0.7, 1024);
   }
}
   

8. Modelo de DTO para Parâmetros de Configuração (ChatConfigParams.java)


package org.example.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatConfigParams implements Serializable {
   private static final long serialVersionUID = 1L;

   private String sessionId; // Associado à sessão WebSocket

   private String model = "deepseek-ai/DeepSeek-R1"; // Modelo padrão
   private Double temperature = 0.7;
   private Double frequencyPenalty = 0.5;
   private String user = "user";
   private Double topP = 0.7;
   private Integer maxCompletionTokens = 1024; // Limite de tokens na resposta

   // Construtor para configuração inicial/padrão
   public ChatConfigParams(String model, Double temperature, Double frequencyPenalty, String user, Double topP, Integer maxCompletionTokens) {
       this.model = model;
       this.temperature = temperature;
       this.frequencyPenalty = frequencyPenalty;
       this.user = user;
       this.topP = topP;
       this.maxCompletionTokens = maxCompletionTokens;
   }
}
   

Interface Frontend (index.html)

Este arquivo HTML cria uma interface simples para interagir com o backend via WebSocket.



<html lang="pt-BR">
<head>
   <meta charset="UTF-8">
   <title>DeepSeek Chat com WebSocket</title>
   <style>
       body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; margin: 20px; background-color: #f8f9fa; color: #333; }
       .container { max-width: 1100px; margin: 0 auto; background-color: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
       .chat-controls { display: flex; gap: 15px; margin-bottom: 25px; align-items: center; }
       #messageInput { flex-grow: 1; padding: 12px 15px; border: 1px solid #ccc; border-radius: 5px; font-size: 1rem; }
       .btn { padding: 12px 25px; border: none; border-radius: 5px; cursor: pointer; font-size: 1rem; transition: background-color 0.3s ease; }
       .btn-primary { background-color: #007bff; color: white; }
       .btn-primary:hover { background-color: #0056b3; }
       .btn-danger { background-color: #dc3545; color: white; }
       .btn-danger:hover { background-color: #c82333; }
       .btn-secondary { background-color: #6c757d; color: white; margin-left: 10px;}
       .btn-secondary:hover { background-color: #5a6268; }
       .chat-area { display: flex; gap: 20px; margin-bottom: 30px; }
       .message-box { flex: 1; height: 350px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 5px; padding: 15px; background-color: #fdfdfd; }
       .message-box h3 { margin-top: 0; color: #0056b3; border-bottom: 1px solid #eee; padding-bottom: 10px; }
       .message { margin-bottom: 15px; padding: 10px; border-radius: 5px; }
       .message p { margin: 0; }
       .user-message { background-color: #e7f3ff; text-align: right; margin-left: 50px; }
       .model-message { background-color: #f1f1f1; margin-right: 50px; }
       .reasoning-message { background-color: #fff3e0; margin-right: 50px; font-style: italic; border-left: 3px solid #ff9800; }
       .system-message { background-color: #f8d7da; color: #721c24; font-size: 0.9em; }
       .config-section { border: 1px dashed #ccc; padding: 20px; border-radius: 5px; background-color: #fcfcfc; }
       .config-form label { display: block; margin-top: 15px; font-weight: bold; }
       .config-form input[type="text"], .config-form input[type="number"] { width: calc(100% - 20px); padding: 10px; margin-top: 8px; border: 1px solid #ccc; border-radius: 4px; }
       .config-form button { margin-top: 20px; }
       #sysLogBox { height: 100px; background-color: #f0f0f0; font-size: 0.9em; }
   </style>
</head>
<body>
   <div class="container">
       <h1>DeepSeek Chat Interativo (WebSocket)</h1>

       <div class="chat-controls">
           <input type="text" id="messageInput" placeholder="Digite sua pergunta aqui..." />
           <button class="btn btn-primary" onclick="sendMessage()">Enviar</button>
           <button class="btn btn-secondary" onclick="connectWebSocket()">Conectar</button>
           <button class="btn btn-danger" onclick="disconnectWebSocket()">Desconectar</button>
       </div>

       <div class="chat-area">
           <div class="message-box">
               <h3>Raciocínio (Passos)</h3>
               <div id="reasoningBox"></div>
           </div>
           <div class="message-box">
               <h3>Resposta do Modelo</h3>
               <div id="replyBox"></div>
           </div>
       </div>

       <div class="message-box" id="sysLogBox">
           <h3>Logs do Sistema</h3>
           <div id="systemLogs"></div>
       </div>

       <div class="config-section">
           <h3>Configurações da Sessão</h3>
           <form id="configForm" class="config-form" onsubmit="return submitConfigForm(event)">
               <div style="display: flex; gap: 20px;">
                   <div style="flex: 1;">
                       <label for="sessionId">Session ID (Automático):</label>
                       <input type="text" id="sessionId" name="sessionId" readonly disabled />

                       <label for="model">Modelo:</label>
                       <input type="text" id="model" name="model" value="deepseek-ai/DeepSeek-R1" required />

                       <label for="temperature">Temperatura:</label>
                       <input type="number" id="temperature" name="temperature" step="0.1" min="0" max="1" value="0.7" required />

                       <label for="frequencyPenalty">Penalidade de Frequência:</label>
                       <input type="number" id="frequencyPenalty" name="frequencyPenalty" step="0.1" min="0" max="2" value="0.5" required />
                   </div>
                   <div style="flex: 1;">
                       <label for="user">Usuário:</label>
                       <input type="text" id="user" name="user" value="frontend_user" required />

                       <label for="topP">Top P:</label>
                       <input type="number" id="topP" name="topP" step="0.1" min="0" max="1" value="0.7" required />

                       <label for="maxCompletionTokens">Max Tokens Resposta:</label>
                       <input type="number" id="maxCompletionTokens" name="maxCompletionTokens" value="1024" required />

                       <button type="submit" class="btn btn-primary">Salvar Configurações</button>
                   </div>
               </div>
           </form>
       </div>
   </div>

   <script>
       let websocket;
       const wsUrl = `ws://${window.location.host}/ws/chat`; // Assume que o backend está na mesma origem
       const backendApiUrl = `http://${window.location.host}/api/deepseek`;

       function connectWebSocket() {
           if (websocket && websocket.readyState === WebSocket.OPEN) {
               logSystemMessage("Conexão já está aberta.");
               return;
           }
           if (websocket) {
               websocket.close();
           }

           websocket = new WebSocket(wsUrl);

           websocket.onopen = (event) => {
               logSystemMessage("Conectado ao servidor WebSocket.");
               document.querySelector('.btn-primary[onclick="connectWebSocket()"]').disabled = true;
               document.querySelector('.btn-danger[onclick="disconnectWebSocket()"]').disabled = false;
           };

           websocket.onmessage = (event) => {
               const message = event.data;
               console.log("Recebido:", message);

               if (message.startsWith("system_log:")) {
                   logSystemMessage(message.substring("system_log:".length));
               } else if (message.startsWith("reasoning_step:")) {
                   appendMessage(message.substring("reasoning_step:".length), "reasoningBox", "reasoning-message");
               } else if (message.startsWith("model_reply:")) {
                   appendMessage(message.substring("model_reply:".length), "replyBox", "model-message");
               } else if (message.startsWith("config_update:")) {
                   updateConfigForm(JSON.parse(message.substring("config_update:".length)));
               }
           };

           websocket.onclose = (event) => {
               logSystemMessage(`Desconectado. Código: ${event.code}, Motivo: ${event.reason || 'Sem motivo especificado'}`);
               document.querySelector('.btn-primary[onclick="connectWebSocket()"]').disabled = false;
               document.querySelector('.btn-danger[onclick="disconnectWebSocket()"]').disabled = true;
               // Limpa o Session ID se a conexão for fechada
               document.getElementById("sessionId").value = "";
           };

           websocket.onerror = (error) => {
               logSystemMessage(`Erro na conexão WebSocket: ${error.message || 'Erro desconhecido'}`);
               console.error("Erro WebSocket:", error);
           };
       }

       function disconnectWebSocket() {
           if (websocket && websocket.readyState === WebSocket.OPEN) {
               websocket.close();
           } else {
               logSystemMessage("Não há conexão ativa para desconectar.");
           }
       }

       function sendMessage() {
           if (!websocket || websocket.readyState !== WebSocket.OPEN) {
               logSystemMessage("Erro: Conexão WebSocket não está aberta.");
               return;
           }
           const messageInput = document.getElementById("messageInput");
           const message = messageInput.value.trim();
           if (message) {
               // Envia a mensagem do usuário
               appendMessage(`Você: ${message}`, "replyBox", "user-message"); // Exibe a mensagem do usuário localmente
               websocket.send(message);
               messageInput.value = ""; // Limpa o campo de entrada

               // Limpa as caixas de resposta e raciocínio para a nova pergunta
               document.getElementById("reasoningBox").innerHTML = "";
               document.getElementById("replyBox").innerHTML = "";
           }
       }

       function appendMessage(msg, boxId, className) {
           const box = document.getElementById(boxId);
           const msgDiv = document.createElement("div");
           msgDiv.className = `message ${className}`;
           const p = document.createElement("p");
           p.innerText = msg; // Usa innerText para evitar interpretação de HTML na mensagem
           msgDiv.appendChild(p);
           box.appendChild(msgDiv);
           box.scrollTop = box.scrollHeight; // Rola para a última mensagem
       }

       function logSystemMessage(msg) {
           const logBox = document.getElementById("systemLogs");
           const p = document.createElement("p");
           p.className = "system-message";
           p.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
           logBox.appendChild(p);
           logBox.scrollTop = logBox.scrollHeight;
       }

       function updateConfigForm(config) {
           document.getElementById("sessionId").value = config.sessionId || "N/A";
           document.getElementById("model").value = config.model;
           document.getElementById("temperature").value = config.temperature;
           document.getElementById("frequencyPenalty").value = config.frequencyPenalty;
           document.getElementById("user").value = config.user;
           document.getElementById("topP").value = config.topP;
           document.getElementById("maxCompletionTokens").value = config.maxCompletionTokens;
           logSystemMessage("Configurações da sessão recebidas e aplicadas.");
       }

       async function submitConfigForm(event) {
           event.preventDefault(); // Impede o envio padrão do formulário

           const form = document.getElementById("configForm");
           const formData = new FormData(form);
           const configData = {};
           formData.forEach((value, key) => {
               // Converte números que devem ser numéricos
               if (key === 'temperature' || key === 'frequencyPenalty' || key === 'topP') {
                   configData[key] = parseFloat(value);
               } else if (key === 'maxCompletionTokens') {
                   configData[key] = parseInt(value, 10);
               } else {
                   configData[key] = value;
               }
           });

           // Obtém o sessionId da conexão ativa, se houver
           if (websocket && websocket.readyState === WebSocket.OPEN && document.getElementById("sessionId").value) {
                configData.sessionId = document.getElementById("sessionId").value;
           } else {
               logSystemMessage("Aviso: Não é possível salvar configurações sem uma sessão WebSocket ativa e com Session ID.");
               // Poderia optar por enviar mesmo assim se o backend souber lidar com sessionId nulo
               // configData.sessionId = null;
               return; // Impede o envio se não houver sessionId válido
           }

           try {
               const response = await fetch(`${backendApiUrl}/configureSession`, {
                   method: 'POST',
                   headers: {
                       'Content-Type': 'application/json',
                   },
                   body: JSON.stringify(configData),
               });

               if (response.ok) {
                   logSystemMessage("Configurações salvas com sucesso no servidor!");
                   // Opcional: Atualizar o formulário com os dados enviados se o backend retornar algo
                   // Ou simplesmente confiar que o backend aplicou
                   // Para feedback imediato, podemos reenviar a configuração atual para o cliente via WS
                   // Mas o mais correto é o servidor reenviar ou o cliente pegar o state do input
               } else {
                   const errorText = await response.text();
                   logSystemMessage(`Falha ao salvar configurações: ${response.status} ${errorText}`);
               }
           } catch (error) {
               logSystemMessage(`Erro de rede ao salvar configurações: ${error.message}`);
               console.error("Erro no fetch:", error);
           }
       }

       // Inicializa a interface
       document.addEventListener('DOMContentLoaded', () => {
           // Desabilita o botão de desconectar inicialmente
           document.querySelector('.btn-danger[onclick="disconnectWebSocket()"]').disabled = true;
           logSystemMessage("Pronto para conectar. Clique em 'Conectar'.");
       });
   </script>
</body>
</html>
   

Depuração e Teste

  1. Inicialize o Servidor Spring Boot: Execute a classe DemoApplication.
  2. Acesse a Interface Frontend: Abra o arquivo index.html em seu navegador. Se o backend estiver rodando em localhost:8080, o frontend deve ser acessível diretamente via file://.../index.html ou servido pelo próprio backend.
  3. Conecte-se: Clique no botão "Conectar". Verifique os logs no console do navegador e na saída do terminal do Spring Boot para confirmar o estabelecimento da conexão WebSocket. O "Session ID" será exibido.
  4. Configure Parâmetros (Opcional): Ajuste os parâmetros como modelo, temperatura, etc., no formulário e clique em "Salvar Configurações". Isso anviará os dados para o endpoint /api/deepseek/configureSession do Spring Boot.
  5. Envie uma Mensagem: Digite uma pergunta no campo de entrada e clique em "Enviar". Observe as respostas divididas em "Raciocínio" e "Resposta do Modelo" na enterface, e os logs detalhados no console do navegador e no terminal.

Considerações

A interface frontend é básica e focada na funcionalidade. Aspectos como renderização de Markdown, tratamento de erros mais robusto e feedback visual avançado podem ser implementados posteriormente.

Tags: spring-boot WebSocket deepseek IA Generativa java

Publicado em 6-9 05:47 por Thomas