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:
- Implementação com SSE: SpringBoot com DeepSeek (Siliconflow) + Frontend (Modo SSE)
- Página Siliconflow DeepSeek: https://m.siliconflow.cn/playground/chat
- Documentação da API Siliconflow: https://docs.siliconflow.cn/cn/userguide/capabilities/reasoning
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
- Inicialize o Servidor Spring Boot: Execute a classe
DemoApplication. - Acesse a Interface Frontend: Abra o arquivo
index.htmlem seu navegador. Se o backend estiver rodando emlocalhost:8080, o frontend deve ser acessível diretamente viafile://.../index.htmlou servido pelo próprio backend. - 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.
- 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/configureSessiondo Spring Boot. - 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.