From 3844734794611c8dc7c552522969994d9e618b4c Mon Sep 17 00:00:00 2001 From: Pablo de la Torre Jamardo Date: Sun, 20 Jul 2025 10:47:16 +0200 Subject: [PATCH] Create ChatEntity to group chat messages under a parent entity --- .../controller/ChatRestController.java | 6 +- .../application/prompt/PromptTemplates.java | 6 +- .../session/ChatSessionManager.java | 15 +++- .../boot/application/usecase/ChatUseCase.java | 5 ++ .../chat/boot/domain/model/ChatIdentity.java | 2 +- .../chat/boot/domain/model/ChatMessage.java | 2 +- .../boot/domain/port/ChatMessageStore.java | 7 +- .../ia/chat/boot/persistence/ChatEntity.java | 74 +++++++++++++++++++ .../boot/persistence/ChatJpaRepository.java | 13 ++++ .../ia/chat/boot/persistence/ChatMapper.java | 15 ++++ .../boot/persistence/ChatMessageEntity.java | 7 +- .../persistence/ChatMessageJpaRepository.java | 17 +---- .../boot/persistence/ChatMessageMapper.java | 11 +-- .../persistence/SqliteChatMessageStore.java | 61 +++++++++++---- frontend/src/components/ChatLayout.vue | 28 +++---- frontend/src/components/ChatSidebar.vue | 8 +- frontend/src/services/chatService.ts | 2 +- pom.xml | 2 +- 18 files changed, 207 insertions(+), 74 deletions(-) create mode 100644 backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatEntity.java create mode 100644 backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatJpaRepository.java create mode 100644 backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMapper.java diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatRestController.java b/backend/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatRestController.java index dade381..baa830b 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatRestController.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/adapter/controller/ChatRestController.java @@ -46,14 +46,14 @@ public class ChatRestController { } @PostMapping(produces = "application/json") - public ResponseEntity newChat() { - return ResponseEntity.ok(UUID.randomUUID().toString()); + public ResponseEntity newChat() { + return ResponseEntity.ok(chatUseCase.createChat()); } @PutMapping(value = "{chatId}", consumes = "application/x-www-form-urlencoded", produces = "application/json") public ResponseEntity handleChat(@PathVariable("chatId") String chatId, @RequestParam("prompt") String prompt) { if (ObjectUtils.isEmpty(chatId)) { - throw new IllegalArgumentException("Chat id cannot be empty"); + throw new IllegalArgumentException("Chat uuid cannot be empty"); } ChatMessage reply; try { diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptTemplates.java b/backend/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptTemplates.java index 1eb4992..e78eda2 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptTemplates.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/application/prompt/PromptTemplates.java @@ -13,9 +13,9 @@ public class PromptTemplates { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); /** - * Loads and returns the full prompts string for the given profile name. + * Loads and returns the full prompts string for the given profile resume. * - * @param profileName name of the prompts profile, without extension (e.g. "default") + * @param profileName resume of the prompts profile, without extension (e.g. "default") * @return full system prompts as String */ public static String get(String profileName) { @@ -25,7 +25,7 @@ public class PromptTemplates { /** * Loads and returns the PromptDefinition for the given profile. * - * @param profileName prompts profile name (e.g. "developer", "minimal") + * @param profileName prompts profile resume (e.g. "developer", "minimal") * @return PromptDefinition object */ public static PromptDefinition load(String profileName) { diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/application/session/ChatSessionManager.java b/backend/src/main/java/com/pablotj/ia/chat/boot/application/session/ChatSessionManager.java index aea765c..22c3216 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/application/session/ChatSessionManager.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/application/session/ChatSessionManager.java @@ -1,5 +1,6 @@ package com.pablotj.ia.chat.boot.application.session; +import com.pablotj.ia.chat.boot.domain.model.ChatIdentity; import com.pablotj.ia.chat.boot.domain.model.ChatMessage; import com.pablotj.ia.chat.boot.domain.port.ChatMessageStore; import java.util.ArrayList; @@ -16,8 +17,12 @@ public class ChatSessionManager { this.chatMessageStore = chatMessageStore; } - public List getMessages(String chatId) { - List messages = chatMessageStore.getMessages(chatId); + public ChatIdentity createChat() { + return chatMessageStore.createChat(); + } + + public List getMessages(String chatUuid) { + List messages = chatMessageStore.getMessages(chatUuid); List filteredMessages = new ArrayList<>(messages); if (ObjectUtils.isEmpty(filteredMessages)) { @@ -28,8 +33,10 @@ public class ChatSessionManager { return filteredMessages; } - public void setMessages(String chatId, List messages) { + public void setMessages(String chatUuid, List messages) { messages.removeIf(m -> ObjectUtils.isEmpty(m.text())); - chatMessageStore.saveMessages(chatId, messages); + chatMessageStore.saveMessages(chatUuid, messages); } + + } \ No newline at end of file diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/application/usecase/ChatUseCase.java b/backend/src/main/java/com/pablotj/ia/chat/boot/application/usecase/ChatUseCase.java index 15f9fcf..c650f66 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/application/usecase/ChatUseCase.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/application/usecase/ChatUseCase.java @@ -3,6 +3,7 @@ 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.application.session.ChatSessionManager; +import com.pablotj.ia.chat.boot.domain.model.ChatIdentity; import com.pablotj.ia.chat.boot.domain.model.ChatMessage; import com.pablotj.ia.chat.boot.infraestructure.llm.LlmModelClient; import java.util.Date; @@ -24,6 +25,10 @@ public class ChatUseCase { this.sessionManager = sessionManager; } + public ChatIdentity createChat() { + return sessionManager.createChat(); + } + public List getMessages(String chatId) { return sessionManager.getMessages(chatId); } diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatIdentity.java b/backend/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatIdentity.java index 6a4d512..a962412 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatIdentity.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatIdentity.java @@ -2,5 +2,5 @@ package com.pablotj.ia.chat.boot.domain.model; import java.io.Serializable; -public record ChatIdentity(String id, String name) implements Serializable { +public record ChatIdentity(String uuid, String resume) implements Serializable { } \ No newline at end of file diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatMessage.java b/backend/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatMessage.java index 584c36a..948740e 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatMessage.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/domain/model/ChatMessage.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import java.io.Serializable; import java.util.Date; -public record ChatMessage(String chatId, String role, String text, Date date) implements Serializable { +public record ChatMessage(String chatUuid, String role, String text, Date date) implements Serializable { @Override @JsonFormat(pattern = "dd/MM/yyyy HH:mm", timezone = "Europe/Madrid") diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/domain/port/ChatMessageStore.java b/backend/src/main/java/com/pablotj/ia/chat/boot/domain/port/ChatMessageStore.java index 7553b2c..f6ad445 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/domain/port/ChatMessageStore.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/domain/port/ChatMessageStore.java @@ -3,10 +3,11 @@ package com.pablotj.ia.chat.boot.domain.port; import com.pablotj.ia.chat.boot.domain.model.ChatIdentity; import com.pablotj.ia.chat.boot.domain.model.ChatMessage; import java.util.List; +import org.springframework.transaction.annotation.Transactional; public interface ChatMessageStore { + ChatIdentity createChat(); List getChats(); - - List getMessages(String chatId); - void saveMessages(String sessionId, List messages); + List getMessages(String chatUuid); + void saveMessages(String chatUuid, List messages); } diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatEntity.java b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatEntity.java new file mode 100644 index 0000000..74044da --- /dev/null +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatEntity.java @@ -0,0 +1,74 @@ +package com.pablotj.ia.chat.boot.persistence; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.springframework.data.annotation.CreatedDate; + +@Entity +@Table(name = "chat") +public class ChatEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String uuid; + + private String resume; + + @CreatedDate + private Date createdDate; + + @OneToMany(mappedBy = "chat", cascade = CascadeType.ALL, orphanRemoval = true) + private List messages = new ArrayList<>(); + + // Getters y Setters + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getResume() { + return resume; + } + + public void setResume(String resume) { + this.resume = resume; + } + + public Date getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Date createdDate) { + this.createdDate = createdDate; + } + + public List getMessages() { + return messages; + } + + public void setMessages(List messages) { + this.messages = messages; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatJpaRepository.java b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatJpaRepository.java new file mode 100644 index 0000000..c80ef8b --- /dev/null +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatJpaRepository.java @@ -0,0 +1,13 @@ +package com.pablotj.ia.chat.boot.persistence; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChatJpaRepository extends JpaRepository { + + Optional findOneByUuid(String chatUuid); + + List findAllByOrderByCreatedDateDesc(); + +} \ No newline at end of file diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMapper.java b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMapper.java new file mode 100644 index 0000000..3891e42 --- /dev/null +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMapper.java @@ -0,0 +1,15 @@ +package com.pablotj.ia.chat.boot.persistence; + +import com.pablotj.ia.chat.boot.domain.model.ChatIdentity; +import com.pablotj.ia.chat.boot.domain.model.ChatMessage; + +public class ChatMapper { + + private ChatMapper() throws IllegalAccessException { + throw new IllegalAccessException("Private access to ChatMapper"); + } + + public static ChatIdentity toDomain(ChatEntity entity) { + return new ChatIdentity(entity.getUuid(), entity.getResume()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageEntity.java b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageEntity.java index 846d206..37e6f45 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageEntity.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageEntity.java @@ -11,7 +11,8 @@ public class ChatMessageEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String sessionId; + @ManyToOne(fetch = FetchType.LAZY) + private ChatEntity chat; @Column(length = 4000) private String text; @@ -25,8 +26,8 @@ public class ChatMessageEntity { public Long getId() { return id; } public void setId(Long id) { this.id = id; } - public String getSessionId() { return sessionId; } - public void setSessionId(String sessionId) { this.sessionId = sessionId; } + public ChatEntity getChat() { return chat; } + public void setChat(ChatEntity chat) { this.chat = chat; } public String getText() { return text; } public void setText(String text) { this.text = text; } diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageJpaRepository.java b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageJpaRepository.java index 2f88ef9..fc2cb9a 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageJpaRepository.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageJpaRepository.java @@ -2,23 +2,10 @@ package com.pablotj.ia.chat.boot.persistence; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - public interface ChatMessageJpaRepository extends JpaRepository { - @Query(value = """ - SELECT * - FROM ( - SELECT *, ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY id ASC) AS rn - FROM chat_messages - ) sub - WHERE rn = 1 - ORDER BY id desc - """, nativeQuery = true) - List findFirstMessagePerSession(); + List findByChatUuidOrderByIdAsc(String chatUuid); - List findBySessionIdOrderByIdAsc(String chatId); - - void deleteBySessionId(String chatId); + void deleteByChatUuid(String chatUuid); } \ No newline at end of file diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageMapper.java b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageMapper.java index ede6726..6924c50 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageMapper.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/ChatMessageMapper.java @@ -1,6 +1,5 @@ package com.pablotj.ia.chat.boot.persistence; -import com.pablotj.ia.chat.boot.domain.model.ChatIdentity; import com.pablotj.ia.chat.boot.domain.model.ChatMessage; public class ChatMessageMapper { @@ -9,9 +8,9 @@ public class ChatMessageMapper { throw new IllegalAccessException("Private access to ChatMessageMapper"); } - public static ChatMessageEntity toEntity(String chatId, ChatMessage message) { + public static ChatMessageEntity toEntity(ChatEntity chat, ChatMessage message) { ChatMessageEntity entity = new ChatMessageEntity(); - entity.setSessionId(chatId); + entity.setChat(chat); entity.setRole(message.role()); entity.setText(message.text()); entity.setDate(message.date()); @@ -19,10 +18,6 @@ public class ChatMessageMapper { } public static ChatMessage toDomain(ChatMessageEntity entity) { - return new ChatMessage(entity.getSessionId(), entity.getRole(), entity.getText(), entity.getDate()); - } - - public static ChatIdentity toEntityId(ChatMessageEntity m) { - return new ChatIdentity(m.getSessionId(), m.getText().length() > 35 ? m.getText().substring(0, 35).concat("...") : m.getText()); + return new ChatMessage(entity.getChat().getUuid(), entity.getRole(), entity.getText(), entity.getDate()); } } \ No newline at end of file diff --git a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/SqliteChatMessageStore.java b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/SqliteChatMessageStore.java index 6c6af7c..ccb5802 100644 --- a/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/SqliteChatMessageStore.java +++ b/backend/src/main/java/com/pablotj/ia/chat/boot/persistence/SqliteChatMessageStore.java @@ -3,29 +3,44 @@ package com.pablotj.ia.chat.boot.persistence; import com.pablotj.ia.chat.boot.domain.model.ChatIdentity; import com.pablotj.ia.chat.boot.domain.model.ChatMessage; import com.pablotj.ia.chat.boot.domain.port.ChatMessageStore; +import java.util.Date; import java.util.List; +import java.util.UUID; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @Repository public class SqliteChatMessageStore implements ChatMessageStore { - private final ChatMessageJpaRepository repository; + private final ChatJpaRepository chatJpaRepository; + private final ChatMessageJpaRepository chatMessageJpaRepository; - public SqliteChatMessageStore(ChatMessageJpaRepository repository) { - this.repository = repository; + public SqliteChatMessageStore(ChatJpaRepository chatJpaRepository, ChatMessageJpaRepository chatMessageJpaRepository) { + this.chatJpaRepository = chatJpaRepository; + this.chatMessageJpaRepository = chatMessageJpaRepository; + } + + @Override + @Transactional + public ChatIdentity createChat() { + ChatEntity chat = new ChatEntity(); + chat.setUuid(UUID.randomUUID().toString()); + chat.setResume("Nuevo chat"); + chat.setCreatedDate(new Date()); + chat = chatJpaRepository.save(chat); + return ChatMapper.toDomain(chat); } @Override public List getChats() { - return repository.findFirstMessagePerSession().stream() - .map(ChatMessageMapper::toEntityId) + return chatJpaRepository.findAllByOrderByCreatedDateDesc().stream() + .map(ChatMapper::toDomain) .toList(); } @Override - public List getMessages(String chatId) { - return repository.findBySessionIdOrderByIdAsc(chatId) + public List getMessages(String chatUuid) { + return chatMessageJpaRepository.findByChatUuidOrderByIdAsc(chatUuid) .stream() .map(ChatMessageMapper::toDomain) .toList(); @@ -33,11 +48,31 @@ public class SqliteChatMessageStore implements ChatMessageStore { @Override @Transactional - public void saveMessages(String chatId, List messages) { - repository.deleteBySessionId(chatId); - List entities = messages.stream() - .map(m -> ChatMessageMapper.toEntity(chatId, m)) - .toList(); - repository.saveAll(entities); + public void saveMessages(String chatUuid, List messages) { + if (messages == null || messages.isEmpty()) { + return; + } + + ChatEntity chat = chatJpaRepository.findOneByUuid(chatUuid).orElse(null); + + if (chat == null) { + chat = new ChatEntity(); + chat.setUuid(chatUuid != null ? chatUuid : UUID.randomUUID().toString()); + chat.setResume(createResume(messages.getFirst())); + chat.setCreatedDate(new Date()); + chat = chatJpaRepository.save(chat); + } else if (chat.getMessages().isEmpty()) { + chat.setResume(createResume(messages.getFirst())); + chat = chatJpaRepository.save(chat); + } + + ChatEntity finalChat = chat; + messages.forEach(msg -> finalChat.getMessages().add(ChatMessageMapper.toEntity(finalChat, msg))); + + chatJpaRepository.save(finalChat); + } + + private String createResume(ChatMessage chatMessage) { + return chatMessage.text().length() > 35 ? chatMessage.text().substring(0, 35).concat("...") : chatMessage.text(); } } \ No newline at end of file diff --git a/frontend/src/components/ChatLayout.vue b/frontend/src/components/ChatLayout.vue index 9921976..3036a5c 100644 --- a/frontend/src/components/ChatLayout.vue +++ b/frontend/src/components/ChatLayout.vue @@ -3,7 +3,7 @@ @@ -25,7 +25,7 @@ import { chatService } from '../services/chatService.ts' import { dateUtils } from '../utils/dateUtils.ts' // Estado reactivo -const chatId = ref('') +const chatUuid = ref('') const chats = ref([]) const messages = ref([]) const isLoading = ref(false) @@ -37,9 +37,9 @@ const loadHistory = async () => { chats.value = data // Autoabrir primer chat si no hay chatId activo - if (!chatId.value && data.length > 0) { - chatId.value = data[0].id - await loadMessages(chatId.value) + if (!chatUuid.value && data.length > 0) { + chatUuid.value = data[0].uuid + await loadMessages(chatUuid.value) } } catch (error) { console.error("Error cargando historial:", error) @@ -58,19 +58,19 @@ const loadMessages = async (selectedChatId) => { // Seleccionar un chat const selectChat = async (selectedId) => { - if (selectedId !== chatId.value) { - chatId.value = selectedId - await loadMessages(chatId.value) + if (selectedId !== chatUuid.value) { + chatUuid.value = selectedId + await loadMessages(chatUuid.value) } } // Crear nuevo chat const createNewChat = async () => { try { - const newChatId = await chatService.createChat() - chatId.value = newChatId + const response = await chatService.createChat() + chatUuid.value = response.uuid; await loadHistory() - await loadMessages(chatId.value) + await loadMessages(chatUuid.value) } catch (error) { console.error("Error al crear nuevo chat:", error) } @@ -79,7 +79,7 @@ const createNewChat = async () => { // Enviar mensaje const sendMessage = async (prompt) => { // Crear nuevo chat si no existe - if (!chatId.value) { + if (!chatUuid.value) { await createNewChat() } @@ -94,8 +94,8 @@ const sendMessage = async (prompt) => { isLoading.value = true try { - const data = await chatService.sendMessage(chatId.value, prompt) - chatId.value = data.chatId + const data = await chatService.sendMessage(chatUuid.value, prompt) + chatUuid.value = data.chatId // Agregar respuesta del bot const botMessage = { diff --git a/frontend/src/components/ChatSidebar.vue b/frontend/src/components/ChatSidebar.vue index 1106762..f162a92 100644 --- a/frontend/src/components/ChatSidebar.vue +++ b/frontend/src/components/ChatSidebar.vue @@ -7,11 +7,11 @@
  • - {{ chat.name }} + {{ chat.resume }}
diff --git a/frontend/src/services/chatService.ts b/frontend/src/services/chatService.ts index 624bbed..8043f13 100644 --- a/frontend/src/services/chatService.ts +++ b/frontend/src/services/chatService.ts @@ -31,7 +31,7 @@ class ChatService { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } - return await response.text() + return await response.json() } catch (error) { console.error("Error creating chat:", error) toast.error("Could not create chat") diff --git a/pom.xml b/pom.xml index b3c61a2..2df8089 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,6 @@ Project IA Chat Offline - 17 + 21