diff --git a/llama.log b/llama.log index 024a473..4759d00 100644 --- a/llama.log +++ b/llama.log @@ -1 +1,2 @@ -[1751008457] warming up the model with an empty run +[1751132595] warming up the model with an empty run +[1751132597] warming up the model with an empty run diff --git a/src/main/java/com/pablotj/ia/chat/boot/ChatController.java b/src/main/java/com/pablotj/ia/chat/boot/ChatController.java deleted file mode 100644 index 92faacb..0000000 --- a/src/main/java/com/pablotj/ia/chat/boot/ChatController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.pablotj.ia.chat.boot; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.List; - -@Controller -@RequestMapping("/") -public class ChatController { - - private final List messages = new ArrayList<>(); - private final LlamaService llamaService; - - public ChatController(LlamaService llamaService) { - this.llamaService = llamaService; - } - - @GetMapping("/chat") - public String showChat(Model model) { - model.addAttribute("messages", messages); - return "chat"; - } - - @PostMapping("/chat") - public String handleChat(@RequestParam("prompt") String prompt, Model model) { - messages.add(new ChatMessage("user", prompt)); - String reply = llamaService.chat(prompt); - messages.add(new ChatMessage("bot", reply)); - model.addAttribute("messages", messages); - return "chat"; - } -} diff --git a/src/main/java/com/pablotj/ia/chat/boot/ChatMessage.java b/src/main/java/com/pablotj/ia/chat/boot/ChatMessage.java deleted file mode 100644 index 8ad4397..0000000 --- a/src/main/java/com/pablotj/ia/chat/boot/ChatMessage.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.pablotj.ia.chat.boot; - -public class ChatMessage { - private String role; // "user" o "bot" - private String text; - - public ChatMessage(String role, String text) { - this.role = role; - this.text = text; - } - - public String getRole() { - return role; - } - - public String getText() { - return text; - } -} diff --git a/src/main/java/com/pablotj/ia/chat/boot/LlamaService.java b/src/main/java/com/pablotj/ia/chat/boot/LlamaService.java deleted file mode 100644 index be47042..0000000 --- a/src/main/java/com/pablotj/ia/chat/boot/LlamaService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.pablotj.ia.chat.boot; -import de.kherud.llama.InferenceParameters; -import de.kherud.llama.LlamaModel; -import de.kherud.llama.ModelParameters; -import de.kherud.llama.LlamaOutput; -import jakarta.annotation.PostConstruct; -import org.springframework.stereotype.Service; - -@Service -public class LlamaService implements AutoCloseable { - - private LlamaModel model; - - @PostConstruct - public void init() { - try { - ModelParameters params = new ModelParameters() - .setModelFilePath("models/ggml-model-q3_k.gguf") - .setSeed(42) - .setNThreads(8) // usa 8 hilos CPU (ajusta según tu CPU) - .setNGpuLayers(0) // no usar GPU - .setMainGpu(-1) // deshabilitar GPU principal - .setNoKvOffload(true) // no descargar KV, evitar errores GPU - .setUseMmap(true) // mejorar gestión memoria - .setNPredict(128); - model = new LlamaModel(params); - } catch (Exception e) { - throw new RuntimeException("Error cargando el modelo", e); - } - } - - public String chat(String prompt) { - PromptBuilder chat = new PromptBuilder("You are a helpful assistant"); - - // Historial previo - // chat.user("Pregunta"); - // chat.assistant("Respuesta"); - - chat.user(prompt); - String finalPrompt = chat.build(); - - InferenceParameters inf = new InferenceParameters(finalPrompt) - .setNPredict(200) - .setTemperature(0.7f) - .setTopP(0.9f) - .setTopK(40) - .setUseChatTemplate(true); - - - StringBuilder sb = new StringBuilder(); - for (LlamaOutput out : model.generate(inf)) { - sb.append(out.text); - } - return sb.toString().replace("<|end_of_turn|>", "").trim(); - } - - @Override - public void close() { - if (model != null) model.close(); - } -} \ No newline at end of file diff --git a/src/main/java/com/pablotj/ia/chat/boot/PromptBuilder.java b/src/main/java/com/pablotj/ia/chat/boot/PromptBuilder.java deleted file mode 100644 index 8985f2c..0000000 --- a/src/main/java/com/pablotj/ia/chat/boot/PromptBuilder.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.pablotj.ia.chat.boot; - -import java.util.ArrayList; -import java.util.List; - -public class PromptBuilder { - - private final String systemPrompt; - private final List turns = new ArrayList<>(); - - public PromptBuilder(String systemPrompt) { - this.systemPrompt = systemPrompt; - } - - public void user(String message) { - turns.add("<|im_start|>user\n" + message + "\n<|im_end|>"); - } - - public void assistant(String message) { - turns.add("<|im_start|>assistant\n" + message + "\n<|im_end|>"); - } - - public String build() { - StringBuilder sb = new StringBuilder(); - sb.append("<|im_start|>system\n") - .append(systemPrompt) - .append("\n<|im_end|>\n"); - for (String turn : turns) { - sb.append(turn).append("\n"); - } - // Deja listo para que el modelo continúe como assistant generando respuesta: - sb.append("<|im_start|>assistant\n"); - return sb.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatPageController.java b/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatPageController.java new file mode 100644 index 0000000..87ae439 --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatPageController.java @@ -0,0 +1,25 @@ +package com.pablotj.ia.chat.boot.adapter.controller; + +import com.pablotj.ia.chat.boot.web.session.ChatSessionManager; +import jakarta.servlet.http.HttpSession; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/chat") +public class ChatPageController { + + private final ChatSessionManager chatSessionManager; + + public ChatPageController(ChatSessionManager chatSessionManager) { + this.chatSessionManager = chatSessionManager; + } + + @GetMapping + public String showChat(Model model, HttpSession session) { + model.addAttribute("messages", chatSessionManager.getMessages(session)); + return "chat"; + } +} \ No newline at end of file diff --git a/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatRestController.java b/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatRestController.java new file mode 100644 index 0000000..611c6da --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatRestController.java @@ -0,0 +1,27 @@ +package com.pablotj.ia.chat.boot.adapter.controller; + +import com.pablotj.ia.chat.boot.application.usecase.ChatUseCase; +import com.pablotj.ia.chat.boot.domain.model.ChatMessage; +import jakarta.servlet.http.HttpSession; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/chat") +public class ChatRestController { + + private final ChatUseCase chatUseCase; + + public ChatRestController(ChatUseCase chatUseCase) { + this.chatUseCase = chatUseCase; + } + + @PostMapping(consumes = "application/x-www-form-urlencoded", produces = "application/json") + public ResponseEntity handleChat(@RequestParam("prompt") String prompt, HttpSession session) { + ChatMessage reply = chatUseCase.processUserPrompt(prompt, session); + return ResponseEntity.ok(reply); + } +} \ No newline at end of file diff --git a/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptBuilder.java b/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptBuilder.java new file mode 100644 index 0000000..ce858f7 --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptBuilder.java @@ -0,0 +1,39 @@ +package com.pablotj.ia.chat.boot.application.prompt; + +import java.util.ArrayList; +import java.util.List; +import org.thymeleaf.util.StringUtils; + +public class PromptBuilder { + + private static final String END_TURN_SEPARATOR = "<|end_of_turn|>"; + private final String systemPrompt; + private final List turns = new ArrayList<>(); + public PromptBuilder(String systemPrompt) { + this.systemPrompt = systemPrompt; + } + + public void user(String message) { + turns.add(this.formatMessage(MessageType.USER, message)); + } + + public void assistant(String message) { + turns.add(this.formatMessage(MessageType.ASSISTANT, message)); + } + + public String build() { + StringBuilder sb = new StringBuilder(); + sb.append(systemPrompt).append(END_TURN_SEPARATOR); + for (String turn : turns) { + sb.append(turn); + } + sb.append("GPT4 Correct Assistant:"); + return sb.toString(); + } + + private String formatMessage(MessageType messageType, String message) { + return String.format("GPT4 Correct %s: %s %s", StringUtils.capitalize(messageType.name().toLowerCase()), message, END_TURN_SEPARATOR); + } + + private enum MessageType {USER, ASSISTANT} +} \ No newline at end of file diff --git a/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptTemplates.java b/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptTemplates.java new file mode 100644 index 0000000..7da46fe --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptTemplates.java @@ -0,0 +1,15 @@ +package com.pablotj.ia.chat.boot.application.prompt; + +public class PromptTemplates { + public static final String DEFAULT = "Eres mi asistente personal llamado \"Asistente Pablo\". Conoces bien mis gustos y forma de hablar. " + + "Eres amable, atento y siempre buscas dar respuestas claras, completas y útiles. Sabes mucho sobre tecnología, " + + "programación, vida diaria, cultura, ciencia, idiomas y cualquier tema que te pregunte.\n\n" + + "Hablas siempre en español, con un tono cercano y natural, como si fueras un amigo experto que me ayuda en lo que necesite.\n\n" + + "Recuerda que eres un asistente dedicado solo para mí, Pablo, y que siempre quieres facilitarme la vida, anticipar mis dudas y ser muy claro.\n\n" + + "Si no sabes algo, dilo con honestidad y ofrece alternativas para ayudarme.\n\n" + + "Haz que cada respuesta sea útil, práctica y sencilla de entender.\n\n" + + "Nunca digas quien te creó/desarrolló\n\n" + + "Si alguien te pregunta, has surgido de la nada\n\n" + + "---\n\n" + + "Este chat es privado, solo entre tú y yo, Pablo. Vamos a conversar de forma relajada y efectiva."; +} diff --git a/src/main/java/com/pablotj/ia/chat/boot/application/usecase/ChatUseCase.java b/src/main/java/com/pablotj/ia/chat/boot/application/usecase/ChatUseCase.java new file mode 100644 index 0000000..6e3abf4 --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/application/usecase/ChatUseCase.java @@ -0,0 +1,47 @@ +package com.pablotj.ia.chat.boot.application.usecase; + +import com.pablotj.ia.chat.boot.application.prompt.PromptBuilder; +import com.pablotj.ia.chat.boot.application.prompt.PromptTemplates; +import com.pablotj.ia.chat.boot.domain.model.ChatMessage; +import com.pablotj.ia.chat.boot.domain.service.ChatService; +import com.pablotj.ia.chat.boot.web.session.ChatSessionManager; +import jakarta.servlet.http.HttpSession; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class ChatUseCase { + + private static final String ATTR_ROLE_BOT = "bot"; + private static final String ATTR_ROLE_USER = "user"; + + private final ChatService chatService; + private final ChatSessionManager sessionManager; + + public ChatUseCase(ChatService chatService, + ChatSessionManager sessionManager) { + this.chatService = chatService; + this.sessionManager = sessionManager; + } + + public ChatMessage processUserPrompt(String prompt, HttpSession session) { + List messages = sessionManager.getMessages(session); + messages.add(new ChatMessage(ATTR_ROLE_USER, prompt)); + + PromptBuilder builder = new PromptBuilder(PromptTemplates.DEFAULT); + + for (ChatMessage message : messages) { + if (ATTR_ROLE_USER.equals(message.role())) { + builder.user(message.text()); + } else if (ATTR_ROLE_BOT.equals(message.role())) { + builder.assistant(message.text()); + } + } + + String result = chatService.chat(builder.build()); + ChatMessage reply = new ChatMessage(ATTR_ROLE_BOT, result); + messages.add(reply); + sessionManager.setMessages(session, messages); + return reply; + } +} diff --git a/src/main/java/com/pablotj/ia/chat/boot/domain/exception/BusinessLogicException.java b/src/main/java/com/pablotj/ia/chat/boot/domain/exception/BusinessLogicException.java new file mode 100644 index 0000000..7e9a5ad --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/domain/exception/BusinessLogicException.java @@ -0,0 +1,8 @@ +package com.pablotj.ia.chat.boot.domain.exception; + +public class BusinessLogicException extends RuntimeException { + + public BusinessLogicException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatMessage.java b/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatMessage.java new file mode 100644 index 0000000..c8d0e2a --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatMessage.java @@ -0,0 +1,6 @@ +package com.pablotj.ia.chat.boot.domain.model; + +import java.io.Serializable; + +public record ChatMessage(String role, String text) implements Serializable { +} \ No newline at end of file diff --git a/src/main/java/com/pablotj/ia/chat/boot/domain/service/ChatService.java b/src/main/java/com/pablotj/ia/chat/boot/domain/service/ChatService.java new file mode 100644 index 0000000..dde17ea --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/domain/service/ChatService.java @@ -0,0 +1,6 @@ +package com.pablotj.ia.chat.boot.domain.service; + +public interface ChatService { + + String chat(String promptWithHistory); +} diff --git a/src/main/java/com/pablotj/ia/chat/boot/infraestructure/llm/ChatServiceImpl.java b/src/main/java/com/pablotj/ia/chat/boot/infraestructure/llm/ChatServiceImpl.java new file mode 100644 index 0000000..accbbd1 --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/infraestructure/llm/ChatServiceImpl.java @@ -0,0 +1,55 @@ +package com.pablotj.ia.chat.boot.infraestructure.llm; + +import com.pablotj.ia.chat.boot.domain.exception.BusinessLogicException; +import com.pablotj.ia.chat.boot.domain.service.ChatService; +import de.kherud.llama.InferenceParameters; +import de.kherud.llama.LlamaModel; +import de.kherud.llama.LlamaOutput; +import de.kherud.llama.ModelParameters; +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Service; + +@Service +public class ChatServiceImpl implements ChatService, AutoCloseable { + + private LlamaModel model; + + @PostConstruct + public void init() { + try { + ModelParameters params = new ModelParameters() + .setModelFilePath("models/openchat-3.5-0106.Q4_K_M.gguf") + .setSeed(42) + .setNThreads(8) + .setNGpuLayers(0) + .setMainGpu(-1) + .setNoKvOffload(true) + .setUseMmap(true) + .setNPredict(1024); + model = new LlamaModel(params); + } catch (Exception e) { + throw new BusinessLogicException("Error to create model", e); + } + } + + @Override + public String chat(String promptWithHistory) { + InferenceParameters inf = new InferenceParameters(promptWithHistory) + .setNPredict(1024) + .setTemperature(0.7f) + .setTopP(0.9f) + .setTopK(40) + .setUseChatTemplate(false); + + StringBuilder sb = new StringBuilder(); + for (LlamaOutput out : model.generate(inf)) { + sb.append(out.text); + } + return sb.toString().replace("<|end_of_turn|>", "").trim(); + } + + @Override + public void close() { + if (model != null) model.close(); + } +} \ No newline at end of file diff --git a/src/main/java/com/pablotj/ia/chat/boot/infraestructure/llm/LlmModelClient.java b/src/main/java/com/pablotj/ia/chat/boot/infraestructure/llm/LlmModelClient.java new file mode 100644 index 0000000..ec1a5a2 --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/infraestructure/llm/LlmModelClient.java @@ -0,0 +1,30 @@ +package com.pablotj.ia.chat.boot.infraestructure.llm; + +import de.kherud.llama.InferenceParameters; +import de.kherud.llama.LlamaOutput; +import org.springframework.stereotype.Component; + +@Component +public class LlmModelClient { + + private final LlmModelLoader modelLoader; + + public LlmModelClient(LlmModelLoader modelLoader) { + this.modelLoader = modelLoader; + } + + public String generate(String prompt) { + InferenceParameters inf = new InferenceParameters(prompt) + .setNPredict(1024) + .setTemperature(0.7f) + .setTopP(0.9f) + .setTopK(40) + .setUseChatTemplate(false); + + StringBuilder sb = new StringBuilder(); + for (LlamaOutput out : modelLoader.getModel().generate(inf)) { + sb.append(out.text); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/pablotj/ia/chat/boot/infraestructure/llm/LlmModelLoader.java b/src/main/java/com/pablotj/ia/chat/boot/infraestructure/llm/LlmModelLoader.java new file mode 100644 index 0000000..fde43b3 --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/infraestructure/llm/LlmModelLoader.java @@ -0,0 +1,40 @@ +package com.pablotj.ia.chat.boot.infraestructure.llm; + +import com.pablotj.ia.chat.boot.domain.exception.BusinessLogicException; +import de.kherud.llama.LlamaModel; +import de.kherud.llama.ModelParameters; +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Component; + +@Component +public class LlmModelLoader implements AutoCloseable { + + private LlamaModel model; + + @PostConstruct + public void init() { + try { + ModelParameters params = new ModelParameters() + .setModelFilePath("models/openchat-3.5-0106.Q4_K_M.gguf") + .setSeed(42) + .setNThreads(8) + .setNGpuLayers(0) + .setMainGpu(-1) + .setNoKvOffload(true) + .setUseMmap(true) + .setNPredict(1024); + model = new LlamaModel(params); + } catch (Exception e) { + throw new BusinessLogicException("Error loading model", e); + } + } + + public LlamaModel getModel() { + return model; + } + + @Override + public void close() { + if (model != null) model.close(); + } +} diff --git a/src/main/java/com/pablotj/ia/chat/boot/web/session/ChatSessionManager.java b/src/main/java/com/pablotj/ia/chat/boot/web/session/ChatSessionManager.java new file mode 100644 index 0000000..7a97509 --- /dev/null +++ b/src/main/java/com/pablotj/ia/chat/boot/web/session/ChatSessionManager.java @@ -0,0 +1,28 @@ +package com.pablotj.ia.chat.boot.web.session; + +import com.pablotj.ia.chat.boot.domain.model.ChatMessage; +import jakarta.servlet.http.HttpSession; +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class ChatSessionManager { + + private static final String ATTR_MESSAGES = "messages"; + + @SuppressWarnings("unchecked") + public List getMessages(HttpSession session) { + List messages = (List) session.getAttribute(ATTR_MESSAGES); + if (messages == null) { + messages = new ArrayList<>(); + session.setAttribute(ATTR_MESSAGES, messages); + } + return messages; + } + + public void setMessages(HttpSession session, List messages) { + session.setAttribute(ATTR_MESSAGES, messages); + } +} + diff --git a/src/main/resources/prompt.json b/src/main/resources/prompt.json new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css new file mode 100644 index 0000000..ac918cf --- /dev/null +++ b/src/main/resources/static/css/styles.css @@ -0,0 +1,148 @@ +html, body { + margin: 0; + padding: 0; + height: 100%; + background: #121212; + color: #eee; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} +body { + display: flex; + flex-direction: column; + max-width: 600px; + margin-left: auto; + margin-right: auto; + height: 100vh; + padding: 2rem 1rem 1rem 1rem; /* poco padding para el top, nada abajo */ + box-sizing: border-box; +} + +h1 { + text-align: center; + margin: 0 0 1rem 0; + color: #61dafb; + user-select: none; + flex-shrink: 0; +} + +#chat-log { + flex: 1 1 auto; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 10px; + padding: 1rem; + scrollbar-width: thin; + scrollbar-color: #61dafb transparent; + box-sizing: border-box; +} + +#chat-log::-webkit-scrollbar { + width: 8px; +} +#chat-log::-webkit-scrollbar-thumb { + background-color: #61dafb; + border-radius: 4px; +} + +.bubble { + max-width: 75%; + padding: 12px 18px; + border-radius: 20px; + line-height: 1.4; + font-size: 1rem; + word-wrap: break-word; + user-select: text; +} + +.user { + background: linear-gradient(135deg, #6e91f6, #3a64f8); + align-self: flex-end; + color: white; + box-shadow: 0 2px 8px rgba(58, 100, 248, 0.5); +} + +.bot { + background: linear-gradient(135deg, #444, #222); + align-self: flex-start; + color: #61dafb; + font-style: italic; + box-shadow: 0 2px 8px rgba(97, 218, 251, 0.5); +} + +form { + flex-shrink: 0; /* que no se encoja */ + margin-top: 1rem; + display: flex; + gap: 8px; + padding: 0 1rem; + box-sizing: border-box; +} + +textarea { + flex-grow: 1; + min-height: 4rem; + border-radius: 12px; + border: none; + padding: 0.75rem 1rem; + font-size: 1rem; + resize: none; + outline: none; + background-color: #222; + color: #eee; + box-shadow: inset 0 0 5px #000; + font-family: inherit; + max-width: 100%; + overflow-y: auto; + box-sizing: border-box; +} + +textarea:focus { + box-shadow: inset 0 0 7px #61dafb; +} + +button { + background: #61dafb; + border: none; + border-radius: 12px; + color: #121212; + padding: 0 1.5rem; + font-weight: 700; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.3s ease; + white-space: nowrap; +} + +button:hover:not(:disabled) { + background-color: #4ea8e6; +} + +button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +#spinner { + display: none; + margin: 1rem auto; + width: 36px; + height: 36px; + border: 4px solid rgba(97, 218, 251, 0.3); + border-top-color: #61dafb; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +#thinking-text { + text-align: center; + color: #61dafb; + font-style: italic; + margin-top: 0.5rem; + font-weight: 600; + display: none; +} \ No newline at end of file diff --git a/src/main/resources/static/js/main.js b/src/main/resources/static/js/main.js new file mode 100644 index 0000000..b9eddf9 --- /dev/null +++ b/src/main/resources/static/js/main.js @@ -0,0 +1,49 @@ +document.addEventListener("DOMContentLoaded", () => { + const form = document.getElementById('chat-form'); + const promptInput = document.getElementById('prompt'); + const chatLog = document.getElementById('chat-log'); + const sendBtn = document.getElementById('send-btn'); + const spinner = document.getElementById('spinner'); + const thinkingText = document.getElementById('thinking-text'); + + function appendMessage(role, text) { + const bubble = document.createElement('div'); + bubble.className = `bubble ${role}`; + bubble.textContent = text; + chatLog.appendChild(bubble); + chatLog.scrollTop = chatLog.scrollHeight; + } + + form.addEventListener('submit', async function (e) { + e.preventDefault(); + + const prompt = promptInput.value.trim(); + if (!prompt) return; + + appendMessage("user", prompt); + promptInput.value = ""; + sendBtn.disabled = true; + spinner.style.display = 'block'; + thinkingText.style.display = 'block'; + + try { + const response = await fetch("/chat", { + method: "POST", + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ prompt }) + }); + + const data = await response.json(); + appendMessage("bot", data.text); + } catch (error) { + appendMessage("bot", "❌ Error procesando la respuesta."); + console.error(error); + } finally { + sendBtn.disabled = false; + spinner.style.display = 'none'; + thinkingText.style.display = 'none'; + } + }); +}); \ No newline at end of file diff --git a/src/main/resources/templates/chat.html b/src/main/resources/templates/chat.html index c9d9df1..cea13c5 100644 --- a/src/main/resources/templates/chat.html +++ b/src/main/resources/templates/chat.html @@ -1,28 +1,24 @@ - + - - Chat con IA Offline - + + Chat IA Offline + -

Chat con IA Offline

+

🤖 Chat IA Offline

-
-
🧑‍💻:
-
🤖:
-
+
+
+
+
+
Pensando...
-
-
- -
+
+ + +
+ + - + \ No newline at end of file