diff --git a/pom.xml b/pom.xml
index 1ead6fe..70f4b02 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,7 +39,7 @@
de.kherud
llama
- 3.4.1
+ 4.2.0
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
index d66ecb0..4a12324 100644
--- 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
@@ -3,6 +3,7 @@ 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 java.util.Date;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.ResponseEntity;
@@ -30,7 +31,7 @@ public class ChatRestController {
reply = chatUseCase.processUserPrompt(prompt, session);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
- reply = new ChatMessage("bot", e.getMessage());
+ reply = new ChatMessage("bot", e.getMessage(), new Date());
}
return ResponseEntity.ok(reply);
}
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
index fdb6457..0844620 100644
--- 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
@@ -7,6 +7,7 @@ import com.pablotj.ia.chat.boot.domain.service.ChatService;
import com.pablotj.ia.chat.boot.infraestructure.llm.LlmModelClient;
import com.pablotj.ia.chat.boot.web.session.ChatSessionManager;
import jakarta.servlet.http.HttpSession;
+import java.util.Date;
import java.util.List;
import org.springframework.stereotype.Component;
@@ -27,7 +28,7 @@ public class ChatUseCase {
public ChatMessage processUserPrompt(String prompt, HttpSession session) {
List messages = sessionManager.getMessages(session);
- messages.add(new ChatMessage(ATTR_ROLE_USER, prompt));
+ messages.add(new ChatMessage(ATTR_ROLE_USER, prompt, new Date()));
PromptBuilder builder = new PromptBuilder(PromptTemplates.getDefault());
@@ -40,7 +41,7 @@ public class ChatUseCase {
}
String result = llmModelClient.generate(builder.build());
- ChatMessage reply = new ChatMessage(ATTR_ROLE_BOT, result);
+ ChatMessage reply = new ChatMessage(ATTR_ROLE_BOT, result, new Date());
messages.add(reply);
sessionManager.setMessages(session, messages);
return reply;
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
index c8d0e2a..e1889b8 100644
--- 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
@@ -1,6 +1,14 @@
package com.pablotj.ia.chat.boot.domain.model;
+import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable;
+import java.util.Date;
-public record ChatMessage(String role, String text) implements Serializable {
+public record ChatMessage(String role, String text, Date date) implements Serializable {
+
+ @Override
+ @JsonFormat(pattern = "dd/MM/yyyy HH:mm", timezone = "Europe/Madrid")
+ public Date date() {
+ return date;
+ }
}
\ No newline at end of file
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
index a56e55e..b159483 100644
--- 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
@@ -10,23 +10,30 @@ import org.springframework.stereotype.Component;
@Component
public class LlmModelLoader implements AutoCloseable {
- @Value(value = "${model.gguf.name}")
+ @Value(value = "${llama.model.name}")
private String modelName;
+ @Value(value = "${llama.model.gpu.enabled}")
+ private boolean gpuEnabled;
+
+ @Value(value = "${llama.model.gpu.layers}")
+ private int gpuLayers;
+
+ @Value(value = "${llama.model.tokens}")
+ private int tokens;
+
private LlamaModel model;
@PostConstruct
public void init() {
try {
ModelParameters params = new ModelParameters()
- .setModelFilePath(String.format("models/%s.gguf", modelName))
+ .setModel(String.format("models/%s.gguf", modelName))
.setSeed(42)
- .setNThreads(8)
- .setNGpuLayers(0)
- .setMainGpu(-1)
- .setNoKvOffload(true)
- .setUseMmap(true)
- .setNPredict(1024);
+ .setThreads(8)
+ .setMainGpu(gpuEnabled ? 0 : -1)
+ .setGpuLayers(gpuEnabled ? gpuLayers : -1)
+ .setPredict(tokens);
model = new LlamaModel(params);
} catch (Exception e) {
throw new BusinessLogicException("Error loading model", e);
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 99e9cca..f794079 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -2,4 +2,8 @@ spring.application.name = ia-chat-boot
server.port = 8080
! Model
-model.gguf.name = openchat-3.5-0106.Q4_K_M
\ No newline at end of file
+llama.model.name = openchat-3.5-0106.Q4_K_M
+llama.model.gpu.enabled = true
+llama.model.gpu.layers = 35
+llama.model.tokens = 1024
+
diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css
index ac918cf..03da5c7 100644
--- a/src/main/resources/static/css/styles.css
+++ b/src/main/resources/static/css/styles.css
@@ -1,126 +1,169 @@
+/* Base y fuente */
html, body {
margin: 0;
padding: 0;
height: 100%;
- background: #121212;
- color: #eee;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: #121217;
+ color: #e0e4e8;
+ font-family: 'Segoe UI Variable', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
}
+
body {
display: flex;
flex-direction: column;
max-width: 600px;
- margin-left: auto;
- margin-right: auto;
+ margin: 0 auto;
height: 100vh;
- padding: 2rem 1rem 1rem 1rem; /* poco padding para el top, nada abajo */
+ padding: 2rem 1.25rem 1rem;
box-sizing: border-box;
+ background-color: #121217;
}
+/* Título */
h1 {
text-align: center;
- margin: 0 0 1rem 0;
- color: #61dafb;
+ margin-bottom: 1.25rem;
+ color: #5a90ff;
+ font-weight: 700;
+ font-size: 1.75rem;
+ letter-spacing: 0.05em;
user-select: none;
- flex-shrink: 0;
}
+/* Contenedor de mensajes */
#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;
+ gap: 12px;
+ padding: 1.25rem 1.5rem;
box-sizing: border-box;
+ background-color: #1a1c22;
+ border-radius: 16px;
+ scrollbar-width: thin;
+ scrollbar-color: #5a90ff33 transparent;
}
#chat-log::-webkit-scrollbar {
- width: 8px;
+ width: 6px;
}
#chat-log::-webkit-scrollbar-thumb {
- background-color: #61dafb;
- border-radius: 4px;
+ background-color: #5a90ff55;
+ border-radius: 3px;
}
+/* Burbujas comunes */
.bubble {
max-width: 75%;
- padding: 12px 18px;
+ padding: 14px 18px;
border-radius: 20px;
- line-height: 1.4;
+ line-height: 1.5;
font-size: 1rem;
word-wrap: break-word;
user-select: text;
+ transition: background-color 0.3s ease, color 0.3s ease;
+ opacity: 0;
+ transform: translateY(10px);
+ animation: slideFadeIn 0.3s forwards;
}
+/* Animación de entrada */
+@keyframes slideFadeIn {
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Burbuja del usuario */
.user {
- background: linear-gradient(135deg, #6e91f6, #3a64f8);
+ background-color: #3451d1;
align-self: flex-end;
- color: white;
- box-shadow: 0 2px 8px rgba(58, 100, 248, 0.5);
+ color: #ffffff;
+ border-radius: 20px 20px 4px 20px;
}
+/* Burbuja del bot */
.bot {
- background: linear-gradient(135deg, #444, #222);
+ background-color: #2a2d35;
align-self: flex-start;
- color: #61dafb;
+ color: #a4c8ff;
font-style: italic;
- box-shadow: 0 2px 8px rgba(97, 218, 251, 0.5);
+ border-radius: 20px 20px 20px 4px;
}
+/* Formulario de entrada */
form {
- flex-shrink: 0; /* que no se encoja */
- margin-top: 1rem;
+ flex-shrink: 0;
+ margin-top: 1.25rem;
display: flex;
- gap: 8px;
- padding: 0 1rem;
+ gap: 12px;
+ padding: 0;
box-sizing: border-box;
+ background: transparent;
+ border-radius: 0;
}
+/* Textarea */
textarea {
flex-grow: 1;
- min-height: 4rem;
- border-radius: 12px;
- border: none;
- padding: 0.75rem 1rem;
+ min-height: 4.25rem;
+ border-radius: 16px;
+ border: 1px solid #2d2f36;
+ padding: 1rem 1.25rem;
font-size: 1rem;
resize: none;
outline: none;
- background-color: #222;
- color: #eee;
- box-shadow: inset 0 0 5px #000;
+ background-color: #1c1e24;
+ color: #e6e9ef;
font-family: inherit;
max-width: 100%;
overflow-y: auto;
box-sizing: border-box;
+ transition: border-color 0.25s ease, background-color 0.25s ease;
+}
+
+textarea::placeholder {
+ color: #6a6e7c;
+ font-style: italic;
+ opacity: 0.9;
}
textarea:focus {
- box-shadow: inset 0 0 7px #61dafb;
+ border-color: #5a90ff;
+ background-color: #20232a;
}
+/* Botón */
button {
- background: #61dafb;
+ background-color: #5a90ff;
border: none;
- border-radius: 12px;
- color: #121212;
- padding: 0 1.5rem;
- font-weight: 700;
+ border-radius: 14px;
+ color: #ffffff;
+ padding: 0 1.75rem;
+ font-weight: 600;
font-size: 1rem;
cursor: pointer;
- transition: background-color 0.3s ease;
+ transition: background-color 0.25s ease, transform 0.1s ease;
white-space: nowrap;
}
button:hover:not(:disabled) {
- background-color: #4ea8e6;
+ background-color: #4076e0;
+}
+
+button:active:not(:disabled) {
+ transform: translateY(1px);
+ background-color: #305dc0;
}
button:disabled {
- opacity: 0.6;
+ opacity: 0.5;
cursor: not-allowed;
+ background-color: #3a4a6a;
}
#spinner {
@@ -128,21 +171,64 @@ button:disabled {
margin: 1rem auto;
width: 36px;
height: 36px;
- border: 4px solid rgba(97, 218, 251, 0.3);
- border-top-color: #61dafb;
+ border: 4px solid rgba(90, 144, 255, 0.2);
+ border-top-color: #5a90ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
- to { transform: rotate(360deg); }
+ to {
+ transform: rotate(360deg);
+ }
}
+/* Texto "Pensando..." */
#thinking-text {
text-align: center;
- color: #61dafb;
+ color: #5a90ff;
font-style: italic;
margin-top: 0.5rem;
font-weight: 600;
display: none;
+ user-select: none;
+}
+
+.timestamp {
+ display: block;
+ text-align: right;
+ font-size: 0.75rem;
+ color: #999;
+ margin-top: 6px;
+ font-style: normal;
+ opacity: 0.7;
+ user-select: none;
+}
+
+/* Responsive */
+@media (max-width: 480px) {
+ body {
+ max-width: 100%;
+ padding: 1.5rem 1rem 1rem;
+ }
+
+ #chat-log {
+ padding: 1rem 1.25rem;
+ border-radius: 12px;
+ }
+
+ form {
+ gap: 8px;
+ }
+
+ textarea {
+ min-height: 3.5rem;
+ font-size: 0.95rem;
+ padding: 0.75rem 1rem;
+ }
+
+ button {
+ padding: 0 1.25rem;
+ font-size: 0.95rem;
+ }
}
\ No newline at end of file
diff --git a/src/main/resources/static/js/main.js b/src/main/resources/static/js/main.js
index b9eddf9..ccb3bfd 100644
--- a/src/main/resources/static/js/main.js
+++ b/src/main/resources/static/js/main.js
@@ -6,11 +6,15 @@ document.addEventListener("DOMContentLoaded", () => {
const spinner = document.getElementById('spinner');
const thinkingText = document.getElementById('thinking-text');
- function appendMessage(role, text) {
+ function appendMessage(role, text, date) {
const bubble = document.createElement('div');
bubble.className = `bubble ${role}`;
bubble.textContent = text;
chatLog.appendChild(bubble);
+ const timestamp = document.createElement('em');
+ timestamp.className = 'timestamp';
+ timestamp.textContent = date;
+ bubble.appendChild(timestamp);
chatLog.scrollTop = chatLog.scrollHeight;
}
@@ -20,7 +24,7 @@ document.addEventListener("DOMContentLoaded", () => {
const prompt = promptInput.value.trim();
if (!prompt) return;
- appendMessage("user", prompt);
+ appendMessage("user", prompt, formatDate(new Date()));
promptInput.value = "";
sendBtn.disabled = true;
spinner.style.display = 'block';
@@ -36,7 +40,7 @@ document.addEventListener("DOMContentLoaded", () => {
});
const data = await response.json();
- appendMessage("bot", data.text);
+ appendMessage("bot", data.text, data.date);
} catch (error) {
appendMessage("bot", "❌ Error procesando la respuesta.");
console.error(error);
@@ -46,4 +50,14 @@ document.addEventListener("DOMContentLoaded", () => {
thinkingText.style.display = 'none';
}
});
+
+ function formatDate(date) {
+ const day = date.getDate().toString().padStart(2, '0');
+ const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Enero = 0
+ const year = date.getFullYear();
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+
+ return `${day}/${month}/${year} ${hours}:${minutes}`;
+ }
});
\ No newline at end of file
diff --git a/src/main/resources/templates/chat.html b/src/main/resources/templates/chat.html
index cea13c5..224c03b 100644
--- a/src/main/resources/templates/chat.html
+++ b/src/main/resources/templates/chat.html
@@ -9,7 +9,11 @@
🤖 Chat IA Offline
Pensando...