Ajout d'un mode "jeu" (possibilité de lancer des sessions dans une campagne). Cela permet de faire de prendre des notes en live au cours d'une partie et d'avoir plusieurs outils sous la main pour aider le mj :
All checks were successful
All checks were successful
- Possibilité de parler à une IA pour règle de jeu ou élément de lore / campagne au cours d'une partie comme aide mémoire - Onglet dédié aux personnages de la campagne - Onglet dédié aux scènes - Onglet avec dès pour ceux qui souhaitent ; Possibilité de rajouté une note en tant qu'évènement, jet de dès ou encore action du joueur par exemple. D'autres ajouts seront fait dans le futur (notamment des tables aléatoires pour PNJ en live).
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
|
||||
<groupId>com.loremind</groupId>
|
||||
<artifactId>loremind-core</artifactId>
|
||||
<version>0.8.7-beta</version>
|
||||
<version>0.9.0-beta</version>
|
||||
<name>LoreMind Core</name>
|
||||
<description>Backend Core - Architecture Hexagonale</description>
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.loremind.application.generationcontext;
|
||||
|
||||
import com.loremind.domain.generationcontext.SessionContext;
|
||||
import com.loremind.domain.generationcontext.SessionContext.JournalEntrySummary;
|
||||
import com.loremind.domain.playcontext.Session;
|
||||
import com.loremind.domain.playcontext.SessionEntry;
|
||||
import com.loremind.domain.playcontext.ports.SessionEntryRepository;
|
||||
import com.loremind.domain.playcontext.ports.SessionRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Construit le SessionContext injecté dans le prompt IA pendant une partie.
|
||||
*
|
||||
* <p>Charge la Session + les N dernières entrées du journal et les mappe vers
|
||||
* le Value Object {@link SessionContext}. La limite d'entrées évite de saturer
|
||||
* la fenêtre de contexte du LLM sur des sessions très longues.</p>
|
||||
*/
|
||||
@Component
|
||||
public class SessionStructuralContextBuilder {
|
||||
|
||||
/**
|
||||
* Plafond du nombre d'entrées remontées au LLM.
|
||||
* Choisi pour rester dans des limites raisonnables (≈ 5-10k tokens max
|
||||
* pour des entrées moyennes de 200 chars). Si la session déborde,
|
||||
* on garde les entrées les plus récentes (fin de chronologie).
|
||||
*/
|
||||
private static final int MAX_ENTRIES = 80;
|
||||
|
||||
private final SessionRepository sessionRepository;
|
||||
private final SessionEntryRepository entryRepository;
|
||||
|
||||
public SessionStructuralContextBuilder(SessionRepository sessionRepository,
|
||||
SessionEntryRepository entryRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.entryRepository = entryRepository;
|
||||
}
|
||||
|
||||
public Optional<SessionContext> buildOptional(String sessionId) {
|
||||
return sessionRepository.findById(sessionId).map(this::toContext);
|
||||
}
|
||||
|
||||
public SessionContext build(String sessionId) {
|
||||
Session session = sessionRepository.findById(sessionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Session introuvable : " + sessionId));
|
||||
return toContext(session);
|
||||
}
|
||||
|
||||
private SessionContext toContext(Session session) {
|
||||
List<SessionEntry> allEntries = entryRepository.findBySessionId(session.getId());
|
||||
// findBySessionId renvoie en ASC. On garde la fin si la liste dépasse le plafond
|
||||
// — c'est l'info récente qui aide le plus l'IA pendant la partie.
|
||||
List<SessionEntry> kept = allEntries.size() <= MAX_ENTRIES
|
||||
? allEntries
|
||||
: allEntries.subList(allEntries.size() - MAX_ENTRIES, allEntries.size());
|
||||
|
||||
List<JournalEntrySummary> summaries = kept.stream()
|
||||
.map(e -> new JournalEntrySummary(
|
||||
e.getType() != null ? e.getType().name() : "NOTE",
|
||||
e.getContent(),
|
||||
e.getOccurredAt()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new SessionContext(
|
||||
session.getName(),
|
||||
session.isActive(),
|
||||
session.getStartedAt(),
|
||||
summaries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.loremind.application.generationcontext;
|
||||
|
||||
import com.loremind.application.gamesystemcontext.GameSystemContextBuilder;
|
||||
import com.loremind.domain.campaigncontext.Campaign;
|
||||
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
|
||||
import com.loremind.domain.gamesystemcontext.GenerationIntent;
|
||||
import com.loremind.domain.generationcontext.CampaignStructuralContext;
|
||||
import com.loremind.domain.generationcontext.ChatMessage;
|
||||
import com.loremind.domain.generationcontext.ChatRequest;
|
||||
import com.loremind.domain.generationcontext.ChatUsage;
|
||||
import com.loremind.domain.generationcontext.GameSystemContext;
|
||||
import com.loremind.domain.generationcontext.LoreStructuralContext;
|
||||
import com.loremind.domain.generationcontext.SessionContext;
|
||||
import com.loremind.domain.generationcontext.ports.AiChatProvider;
|
||||
import com.loremind.domain.playcontext.Session;
|
||||
import com.loremind.domain.playcontext.ports.SessionRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Use case applicatif : chat IA pendant une Session de jeu.
|
||||
* <p>
|
||||
* Orchestre la composition des contextes :
|
||||
* 1. Charge la Session puis la Campagne associée (weak reference).
|
||||
* 2. Construit le CampaignStructuralContext (carte narrative + PJ/PNJ).
|
||||
* 3. Construit le LoreStructuralContext si la campagne est liée à un Lore.
|
||||
* 4. Construit le GameSystemContext si elle a un système de JDR.
|
||||
* 5. Construit le SessionContext (journal horodaté, statut).
|
||||
* 6. Délègue au port {@link AiChatProvider} pour le streaming.
|
||||
* </p>
|
||||
*
|
||||
* <p>La conversation est éphémère (pas de persistance) : pendant une partie,
|
||||
* l'utilité est d'avoir une assistance immédiate, pas de garder un historique.
|
||||
* Le journal de session joue déjà ce rôle de mémoire persistante.</p>
|
||||
*/
|
||||
@Service
|
||||
public class StreamChatForSessionUseCase {
|
||||
|
||||
private final SessionRepository sessionRepository;
|
||||
private final CampaignRepository campaignRepository;
|
||||
private final CampaignStructuralContextBuilder campaignContextBuilder;
|
||||
private final LoreStructuralContextBuilder loreContextBuilder;
|
||||
private final GameSystemContextBuilder gameSystemContextBuilder;
|
||||
private final SessionStructuralContextBuilder sessionContextBuilder;
|
||||
private final AiChatProvider aiChatProvider;
|
||||
|
||||
public StreamChatForSessionUseCase(
|
||||
SessionRepository sessionRepository,
|
||||
CampaignRepository campaignRepository,
|
||||
CampaignStructuralContextBuilder campaignContextBuilder,
|
||||
LoreStructuralContextBuilder loreContextBuilder,
|
||||
GameSystemContextBuilder gameSystemContextBuilder,
|
||||
SessionStructuralContextBuilder sessionContextBuilder,
|
||||
AiChatProvider aiChatProvider) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.campaignRepository = campaignRepository;
|
||||
this.campaignContextBuilder = campaignContextBuilder;
|
||||
this.loreContextBuilder = loreContextBuilder;
|
||||
this.gameSystemContextBuilder = gameSystemContextBuilder;
|
||||
this.sessionContextBuilder = sessionContextBuilder;
|
||||
this.aiChatProvider = aiChatProvider;
|
||||
}
|
||||
|
||||
public void execute(
|
||||
String sessionId,
|
||||
List<ChatMessage> messages,
|
||||
Consumer<ChatUsage> onUsage,
|
||||
Consumer<String> onToken,
|
||||
Runnable onComplete,
|
||||
Consumer<Throwable> onError) {
|
||||
|
||||
Session session = sessionRepository.findById(sessionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Session introuvable : " + sessionId));
|
||||
|
||||
Campaign campaign = campaignRepository.findById(session.getCampaignId())
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"Campagne associée à la session introuvable : " + session.getCampaignId()));
|
||||
|
||||
CampaignStructuralContext campaignContext = campaignContextBuilder.build(campaign.getId());
|
||||
LoreStructuralContext loreContext = loadLoreContextOrNull(campaign);
|
||||
GameSystemContext gameSystemContext = loadGameSystemContextOrNull(campaign);
|
||||
SessionContext sessionContext = sessionContextBuilder.build(sessionId);
|
||||
|
||||
ChatRequest request = ChatRequest.builder()
|
||||
.messages(messages)
|
||||
.loreContext(loreContext)
|
||||
.campaignContext(campaignContext)
|
||||
.gameSystemContext(gameSystemContext)
|
||||
.sessionContext(sessionContext)
|
||||
.build();
|
||||
|
||||
aiChatProvider.streamChat(request, onUsage, onToken, onComplete, onError);
|
||||
}
|
||||
|
||||
private LoreStructuralContext loadLoreContextOrNull(Campaign campaign) {
|
||||
if (!campaign.isLinkedToLore()) return null;
|
||||
return loreContextBuilder.buildOptional(campaign.getLoreId()).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pendant une session active, on injecte les sections les plus utiles en partie
|
||||
* (combats, PNJ, mécaniques) — intent SCENE est le plus proche de ce besoin.
|
||||
*/
|
||||
private GameSystemContext loadGameSystemContextOrNull(Campaign campaign) {
|
||||
if (!campaign.isLinkedToGameSystem()) return null;
|
||||
return gameSystemContextBuilder.buildOptional(campaign.getGameSystemId(), GenerationIntent.SCENE)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.loremind.application.playcontext;
|
||||
|
||||
import com.loremind.domain.playcontext.EntryType;
|
||||
import com.loremind.domain.playcontext.SessionEntry;
|
||||
import com.loremind.domain.playcontext.ports.SessionEntryRepository;
|
||||
import com.loremind.domain.playcontext.ports.SessionRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Service d'application pour le journal d'une Session.
|
||||
* Gère le cycle CRUD des entrées (note, évènement, jet, action joueur).
|
||||
*/
|
||||
@Service
|
||||
public class SessionEntryService {
|
||||
|
||||
private final SessionEntryRepository entryRepository;
|
||||
private final SessionRepository sessionRepository;
|
||||
|
||||
public SessionEntryService(SessionEntryRepository entryRepository,
|
||||
SessionRepository sessionRepository) {
|
||||
this.entryRepository = entryRepository;
|
||||
this.sessionRepository = sessionRepository;
|
||||
}
|
||||
|
||||
/** Données fournies par l'API pour créer ou éditer une entrée. */
|
||||
public record EntryData(EntryType type, String content, LocalDateTime occurredAt) {}
|
||||
|
||||
public SessionEntry createEntry(String sessionId, EntryData data) {
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
throw new IllegalArgumentException("sessionId est requis.");
|
||||
}
|
||||
if (!sessionRepository.existsById(sessionId)) {
|
||||
throw new IllegalArgumentException("Session introuvable : " + sessionId);
|
||||
}
|
||||
validateContent(data.content());
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
SessionEntry entry = SessionEntry.builder()
|
||||
.sessionId(sessionId)
|
||||
.type(data.type() != null ? data.type() : EntryType.NOTE)
|
||||
.content(data.content().trim())
|
||||
.occurredAt(data.occurredAt() != null ? data.occurredAt() : now)
|
||||
.build();
|
||||
return entryRepository.save(entry);
|
||||
}
|
||||
|
||||
public SessionEntry updateEntry(String id, EntryData data) {
|
||||
validateContent(data.content());
|
||||
SessionEntry existing = entryRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Entrée introuvable : " + id));
|
||||
if (data.type() != null) existing.setType(data.type());
|
||||
existing.setContent(data.content().trim());
|
||||
if (data.occurredAt() != null) existing.setOccurredAt(data.occurredAt());
|
||||
return entryRepository.save(existing);
|
||||
}
|
||||
|
||||
public Optional<SessionEntry> getById(String id) {
|
||||
return entryRepository.findById(id);
|
||||
}
|
||||
|
||||
public List<SessionEntry> getBySessionId(String sessionId) {
|
||||
return entryRepository.findBySessionId(sessionId);
|
||||
}
|
||||
|
||||
public void deleteEntry(String id) {
|
||||
if (!entryRepository.existsById(id)) {
|
||||
throw new IllegalArgumentException("Entrée introuvable : " + id);
|
||||
}
|
||||
entryRepository.deleteById(id);
|
||||
}
|
||||
|
||||
private void validateContent(String content) {
|
||||
if (content == null || content.isBlank()) {
|
||||
throw new IllegalArgumentException("Le contenu d'une entrée ne peut pas être vide.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.loremind.application.playcontext;
|
||||
|
||||
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
|
||||
import com.loremind.domain.playcontext.Session;
|
||||
import com.loremind.domain.playcontext.ports.SessionEntryRepository;
|
||||
import com.loremind.domain.playcontext.ports.SessionRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Service d'application pour le Play Context.
|
||||
* Orchestre le cycle de vie d'une Session (lancement, fin, renommage).
|
||||
* Fait partie de la couche Application de l'Architecture Hexagonale.
|
||||
*
|
||||
* <p>Règle métier : une seule Session peut être active (endedAt null) à la fois
|
||||
* dans l'application.</p>
|
||||
*/
|
||||
@Service
|
||||
public class SessionService {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
private final SessionRepository sessionRepository;
|
||||
private final SessionEntryRepository entryRepository;
|
||||
private final CampaignRepository campaignRepository;
|
||||
|
||||
public SessionService(SessionRepository sessionRepository,
|
||||
SessionEntryRepository entryRepository,
|
||||
CampaignRepository campaignRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.entryRepository = entryRepository;
|
||||
this.campaignRepository = campaignRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance une nouvelle session sur la campagne donnée.
|
||||
* Échoue si une session est déjà active ou si la campagne n'existe pas.
|
||||
*/
|
||||
public Session startSession(String campaignId) {
|
||||
if (campaignId == null || campaignId.isBlank()) {
|
||||
throw new IllegalArgumentException("campaignId est requis pour démarrer une session.");
|
||||
}
|
||||
if (!campaignRepository.existsById(campaignId)) {
|
||||
throw new IllegalArgumentException("Campagne introuvable : " + campaignId);
|
||||
}
|
||||
sessionRepository.findActive().ifPresent(s -> {
|
||||
throw new IllegalStateException("Une session est déjà en cours (id=" + s.getId() + "). Termine-la avant d'en lancer une nouvelle.");
|
||||
});
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Session session = Session.builder()
|
||||
.name(generateDefaultName(now))
|
||||
.campaignId(campaignId)
|
||||
.startedAt(now)
|
||||
.build();
|
||||
return sessionRepository.save(session);
|
||||
}
|
||||
|
||||
/** Termine la session active si elle correspond à l'id donné. */
|
||||
public Session endSession(String id) {
|
||||
Session session = sessionRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Session introuvable : " + id));
|
||||
if (!session.isActive()) {
|
||||
throw new IllegalStateException("Cette session est déjà terminée.");
|
||||
}
|
||||
session.setEndedAt(LocalDateTime.now());
|
||||
return sessionRepository.save(session);
|
||||
}
|
||||
|
||||
public Session renameSession(String id, String newName) {
|
||||
if (newName == null || newName.isBlank()) {
|
||||
throw new IllegalArgumentException("Le nom de la session ne peut pas être vide.");
|
||||
}
|
||||
Session session = sessionRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Session introuvable : " + id));
|
||||
session.setName(newName.trim());
|
||||
return sessionRepository.save(session);
|
||||
}
|
||||
|
||||
public Optional<Session> getById(String id) {
|
||||
return sessionRepository.findById(id);
|
||||
}
|
||||
|
||||
public Optional<Session> getActive() {
|
||||
return sessionRepository.findActive();
|
||||
}
|
||||
|
||||
public List<Session> getAll() {
|
||||
return sessionRepository.findAll();
|
||||
}
|
||||
|
||||
public List<Session> getByCampaignId(String campaignId) {
|
||||
return sessionRepository.findByCampaignId(campaignId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une session et toutes ses entrées de journal en cascade.
|
||||
* Transactionnel : soit tout disparaît, soit rien.
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteSession(String id) {
|
||||
if (!sessionRepository.existsById(id)) {
|
||||
throw new IllegalArgumentException("Session introuvable : " + id);
|
||||
}
|
||||
entryRepository.deleteBySessionId(id);
|
||||
sessionRepository.deleteById(id);
|
||||
}
|
||||
|
||||
private String generateDefaultName(LocalDateTime startedAt) {
|
||||
return "Session du " + startedAt.format(DATE_FORMATTER);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,8 @@ public record ChatRequest(
|
||||
PageContext pageContext,
|
||||
CampaignStructuralContext campaignContext,
|
||||
NarrativeEntityContext narrativeEntity,
|
||||
GameSystemContext gameSystemContext) {
|
||||
GameSystemContext gameSystemContext,
|
||||
SessionContext sessionContext) {
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
@@ -50,6 +51,7 @@ public record ChatRequest(
|
||||
private CampaignStructuralContext campaignContext;
|
||||
private NarrativeEntityContext narrativeEntity;
|
||||
private GameSystemContext gameSystemContext;
|
||||
private SessionContext sessionContext;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@@ -83,9 +85,14 @@ public record ChatRequest(
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder sessionContext(SessionContext sessionContext) {
|
||||
this.sessionContext = sessionContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatRequest build() {
|
||||
return new ChatRequest(messages, loreContext, pageContext,
|
||||
campaignContext, narrativeEntity, gameSystemContext);
|
||||
campaignContext, narrativeEntity, gameSystemContext, sessionContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.loremind.domain.generationcontext;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Contexte structurel d'une Session de jeu — injecté dans le system prompt
|
||||
* de l'IA pour qu'elle ait conscience de la partie en cours et de son journal.
|
||||
*
|
||||
* <p>Pendant qu'une session se joue, l'IA reçoit en plus du Lore/Campagne/GameSystem :
|
||||
* le nom de la session, son statut (en cours / terminée) et un résumé chronologique
|
||||
* des entrées du journal (notes, évènements, jets, actions joueurs).</p>
|
||||
*
|
||||
* <p>Value Object du Generation Context — record Java immutable.</p>
|
||||
*
|
||||
* @param sessionName Nom de la session telle qu'affichée au MJ.
|
||||
* @param active True si la session est en cours, false si terminée.
|
||||
* @param startedAt Horodatage de démarrage.
|
||||
* @param entries Entrées du journal triées chronologiquement (anciennes → récentes).
|
||||
* Limité côté builder pour éviter de saturer le contexte LLM.
|
||||
*/
|
||||
public record SessionContext(
|
||||
String sessionName,
|
||||
boolean active,
|
||||
LocalDateTime startedAt,
|
||||
List<JournalEntrySummary> entries) {
|
||||
|
||||
/** Résumé d'une entrée de journal — type + contenu + horodatage. */
|
||||
public record JournalEntrySummary(
|
||||
String type,
|
||||
String content,
|
||||
LocalDateTime occurredAt) {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.loremind.domain.playcontext;
|
||||
|
||||
/**
|
||||
* Type d'entrée du journal de session.
|
||||
* Permet à l'UI de catégoriser visuellement la timeline (icône, couleur).
|
||||
*/
|
||||
public enum EntryType {
|
||||
/** Note libre du MJ (défaut). */
|
||||
NOTE,
|
||||
/** Moment marquant du scénario (combat gagné, décision majeure...). */
|
||||
EVENT,
|
||||
/** Jet de dés / test de caractéristique. */
|
||||
DICE_ROLL,
|
||||
/** Action déclarée par un joueur. */
|
||||
PLAYER_ACTION
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.loremind.domain.playcontext;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité de domaine représentant une Session de jeu en cours ou passée.
|
||||
*
|
||||
* <p>Une Session est une instance jouée d'une Campaign. La Campaign reste
|
||||
* un scénario générique réutilisable ; la Session capture une partie réelle
|
||||
* (date, journal, etc.) sans polluer le scénario d'origine.</p>
|
||||
*
|
||||
* <p>Fait partie du Play Context. Référence la Campaign par weak reference
|
||||
* (campaignId) pour respecter la séparation des Bounded Contexts.</p>
|
||||
*
|
||||
* <p>{@code endedAt == null} signifie que la session est en cours.
|
||||
* Une seule session peut être en cours dans l'application à la fois.</p>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class Session {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
|
||||
/** Weak reference vers Campaign — pas de dépendance directe inter-contexte. */
|
||||
private String campaignId;
|
||||
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
/** Null = session en cours ; renseigné = session terminée. */
|
||||
private LocalDateTime endedAt;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public boolean isActive() {
|
||||
return this.endedAt == null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.loremind.domain.playcontext;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entrée du journal d'une Session.
|
||||
* Représente un évènement horodaté capturé pendant ou après une partie :
|
||||
* note libre du MJ, évènement marquant, jet de dés, action de joueur.
|
||||
*
|
||||
* <p>Fait partie du Play Context. Référence la Session par weak reference
|
||||
* (sessionId) — l'orchestration en cascade est gérée par le service applicatif.</p>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class SessionEntry {
|
||||
|
||||
private String id;
|
||||
|
||||
/** Weak reference vers Session (intra-contexte mais reste découplée). */
|
||||
private String sessionId;
|
||||
|
||||
private EntryType type;
|
||||
|
||||
/** Contenu texte brut saisi par le MJ. */
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* Horodatage métier de l'évènement.
|
||||
* Distinct de {@code createdAt} : utile si le MJ rédige a posteriori
|
||||
* une note rétroactive sur quelque chose qui s'est passé plus tôt.
|
||||
*/
|
||||
private LocalDateTime occurredAt;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.loremind.domain.playcontext.ports;
|
||||
|
||||
import com.loremind.domain.playcontext.SessionEntry;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Port de sortie pour la persistance des entrées de journal de session.
|
||||
*/
|
||||
public interface SessionEntryRepository {
|
||||
|
||||
SessionEntry save(SessionEntry entry);
|
||||
|
||||
Optional<SessionEntry> findById(String id);
|
||||
|
||||
/** Renvoie les entrées d'une session, triées par occurredAt croissant (chronologique). */
|
||||
List<SessionEntry> findBySessionId(String sessionId);
|
||||
|
||||
void deleteById(String id);
|
||||
|
||||
/** Supprime toutes les entrées d'une session — utilisé pour la cascade à la suppression. */
|
||||
void deleteBySessionId(String sessionId);
|
||||
|
||||
boolean existsById(String id);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.loremind.domain.playcontext.ports;
|
||||
|
||||
import com.loremind.domain.playcontext.Session;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Port de sortie pour la persistance des Sessions.
|
||||
* Interface définie dans le domaine, implémentée par l'infrastructure.
|
||||
*/
|
||||
public interface SessionRepository {
|
||||
|
||||
Session save(Session session);
|
||||
|
||||
Optional<Session> findById(String id);
|
||||
|
||||
List<Session> findAll();
|
||||
|
||||
List<Session> findByCampaignId(String campaignId);
|
||||
|
||||
/** Retourne la session en cours (endedAt null) s'il y en a une. */
|
||||
Optional<Session> findActive();
|
||||
|
||||
void deleteById(String id);
|
||||
|
||||
boolean existsById(String id);
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import com.loremind.domain.generationcontext.LoreStructuralContext;
|
||||
import com.loremind.domain.generationcontext.LoreStructuralContext.PageSummary;
|
||||
import com.loremind.domain.generationcontext.NarrativeEntityContext;
|
||||
import com.loremind.domain.generationcontext.PageContext;
|
||||
import com.loremind.domain.generationcontext.SessionContext;
|
||||
import com.loremind.domain.generationcontext.SessionContext.JournalEntrySummary;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -58,9 +60,35 @@ public class BrainChatPayloadBuilder {
|
||||
if (request.gameSystemContext() != null) {
|
||||
root.put("game_system_context", gameSystemContextToMap(request.gameSystemContext()));
|
||||
}
|
||||
if (request.sessionContext() != null) {
|
||||
root.put("session_context", sessionContextToMap(request.sessionContext()));
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private Map<String, Object> sessionContextToMap(SessionContext sc) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("session_name", sc.sessionName());
|
||||
map.put("active", sc.active());
|
||||
if (sc.startedAt() != null) {
|
||||
map.put("started_at", sc.startedAt().toString());
|
||||
}
|
||||
map.put("entries", sc.entries() != null
|
||||
? sc.entries().stream().map(this::journalEntryToMap).collect(Collectors.toList())
|
||||
: List.of());
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> journalEntryToMap(JournalEntrySummary e) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("type", e.type());
|
||||
map.put("content", e.content());
|
||||
if (e.occurredAt() != null) {
|
||||
map.put("occurred_at", e.occurredAt().toString());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> gameSystemContextToMap(GameSystemContext gs) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("system_name", gs.systemName());
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.loremind.infrastructure.persistence.entity;
|
||||
|
||||
import com.loremind.domain.playcontext.EntryType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité JPA pour la persistance des entrées de journal de session.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "session_entries", indexes = {
|
||||
@Index(name = "idx_session_entries_session_id", columnList = "session_id")
|
||||
})
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SessionEntryJpaEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** Weak reference — pas de FK DB pour rester cohérent avec le reste du projet. */
|
||||
@Column(name = "session_id", nullable = false)
|
||||
private String sessionId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 32)
|
||||
private EntryType type;
|
||||
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
private String content;
|
||||
|
||||
@Column(name = "occurred_at", nullable = false)
|
||||
private LocalDateTime occurredAt;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
createdAt = now;
|
||||
updatedAt = now;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.loremind.infrastructure.persistence.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité JPA pour la persistance des Sessions en PostgreSQL.
|
||||
* Adaptateur d'infrastructure — n'est PAS dans le domaine.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sessions")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SessionJpaEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* ID de la Campaign associée. Pas de @ManyToOne / pas de FK : c'est une
|
||||
* weak reference inter-contexte (Play Context ↔ Campaign Context).
|
||||
*/
|
||||
@Column(name = "campaign_id", nullable = false)
|
||||
private String campaignId;
|
||||
|
||||
@Column(name = "started_at", nullable = false)
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
/** Null = session en cours. */
|
||||
@Column(name = "ended_at")
|
||||
private LocalDateTime endedAt;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
createdAt = now;
|
||||
updatedAt = now;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.loremind.infrastructure.persistence.jpa;
|
||||
|
||||
import com.loremind.infrastructure.persistence.entity.SessionEntryJpaEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface SessionEntryJpaRepository extends JpaRepository<SessionEntryJpaEntity, Long> {
|
||||
|
||||
List<SessionEntryJpaEntity> findBySessionIdOrderByOccurredAtAsc(String sessionId);
|
||||
|
||||
void deleteBySessionId(String sessionId);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.loremind.infrastructure.persistence.jpa;
|
||||
|
||||
import com.loremind.infrastructure.persistence.entity.SessionJpaEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Repository Spring Data JPA pour SessionJpaEntity.
|
||||
*/
|
||||
@Repository
|
||||
public interface SessionJpaRepository extends JpaRepository<SessionJpaEntity, Long> {
|
||||
|
||||
List<SessionJpaEntity> findByCampaignIdOrderByStartedAtDesc(String campaignId);
|
||||
|
||||
Optional<SessionJpaEntity> findFirstByEndedAtIsNull();
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.loremind.infrastructure.persistence.postgres;
|
||||
|
||||
import com.loremind.domain.playcontext.SessionEntry;
|
||||
import com.loremind.domain.playcontext.ports.SessionEntryRepository;
|
||||
import com.loremind.infrastructure.persistence.entity.SessionEntryJpaEntity;
|
||||
import com.loremind.infrastructure.persistence.jpa.SessionEntryJpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Adaptateur d'infrastructure : implémente le Port SessionEntryRepository.
|
||||
*/
|
||||
@Repository
|
||||
public class PostgresSessionEntryRepository implements SessionEntryRepository {
|
||||
|
||||
private final SessionEntryJpaRepository jpaRepository;
|
||||
|
||||
public PostgresSessionEntryRepository(SessionEntryJpaRepository jpaRepository) {
|
||||
this.jpaRepository = jpaRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionEntry save(SessionEntry entry) {
|
||||
SessionEntryJpaEntity saved = jpaRepository.save(toJpaEntity(entry));
|
||||
return toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<SessionEntry> findById(String id) {
|
||||
return jpaRepository.findById(Long.parseLong(id)).map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionEntry> findBySessionId(String sessionId) {
|
||||
return jpaRepository.findBySessionIdOrderByOccurredAtAsc(sessionId).stream()
|
||||
.map(this::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(String id) {
|
||||
jpaRepository.deleteById(Long.parseLong(id));
|
||||
}
|
||||
|
||||
/** {@code @Transactional} requis : Spring Data exige une transaction pour les deleteByXxx dérivés. */
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteBySessionId(String sessionId) {
|
||||
jpaRepository.deleteBySessionId(sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsById(String id) {
|
||||
return jpaRepository.existsById(Long.parseLong(id));
|
||||
}
|
||||
|
||||
private SessionEntry toDomain(SessionEntryJpaEntity jpa) {
|
||||
return SessionEntry.builder()
|
||||
.id(jpa.getId().toString())
|
||||
.sessionId(jpa.getSessionId())
|
||||
.type(jpa.getType())
|
||||
.content(jpa.getContent())
|
||||
.occurredAt(jpa.getOccurredAt())
|
||||
.createdAt(jpa.getCreatedAt())
|
||||
.updatedAt(jpa.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private SessionEntryJpaEntity toJpaEntity(SessionEntry entry) {
|
||||
Long id = entry.getId() != null ? Long.parseLong(entry.getId()) : null;
|
||||
return SessionEntryJpaEntity.builder()
|
||||
.id(id)
|
||||
.sessionId(entry.getSessionId())
|
||||
.type(entry.getType())
|
||||
.content(entry.getContent())
|
||||
.occurredAt(entry.getOccurredAt())
|
||||
.createdAt(entry.getCreatedAt())
|
||||
.updatedAt(entry.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.loremind.infrastructure.persistence.postgres;
|
||||
|
||||
import com.loremind.domain.playcontext.Session;
|
||||
import com.loremind.domain.playcontext.ports.SessionRepository;
|
||||
import com.loremind.infrastructure.persistence.entity.SessionJpaEntity;
|
||||
import com.loremind.infrastructure.persistence.jpa.SessionJpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Adaptateur d'infrastructure qui implémente le Port SessionRepository.
|
||||
* Convertit Session (domaine pur) ↔ SessionJpaEntity (persistance).
|
||||
*/
|
||||
@Repository
|
||||
public class PostgresSessionRepository implements SessionRepository {
|
||||
|
||||
private final SessionJpaRepository jpaRepository;
|
||||
|
||||
public PostgresSessionRepository(SessionJpaRepository jpaRepository) {
|
||||
this.jpaRepository = jpaRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session save(Session session) {
|
||||
SessionJpaEntity saved = jpaRepository.save(toJpaEntity(session));
|
||||
return toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Session> findById(String id) {
|
||||
return jpaRepository.findById(Long.parseLong(id)).map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Session> findAll() {
|
||||
return jpaRepository.findAll().stream()
|
||||
.map(this::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Session> findByCampaignId(String campaignId) {
|
||||
return jpaRepository.findByCampaignIdOrderByStartedAtDesc(campaignId).stream()
|
||||
.map(this::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Session> findActive() {
|
||||
return jpaRepository.findFirstByEndedAtIsNull().map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(String id) {
|
||||
jpaRepository.deleteById(Long.parseLong(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsById(String id) {
|
||||
return jpaRepository.existsById(Long.parseLong(id));
|
||||
}
|
||||
|
||||
private Session toDomain(SessionJpaEntity jpa) {
|
||||
return Session.builder()
|
||||
.id(jpa.getId().toString())
|
||||
.name(jpa.getName())
|
||||
.campaignId(jpa.getCampaignId())
|
||||
.startedAt(jpa.getStartedAt())
|
||||
.endedAt(jpa.getEndedAt())
|
||||
.createdAt(jpa.getCreatedAt())
|
||||
.updatedAt(jpa.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private SessionJpaEntity toJpaEntity(Session session) {
|
||||
Long id = session.getId() != null ? Long.parseLong(session.getId()) : null;
|
||||
return SessionJpaEntity.builder()
|
||||
.id(id)
|
||||
.name(session.getName())
|
||||
.campaignId(session.getCampaignId())
|
||||
.startedAt(session.getStartedAt())
|
||||
.endedAt(session.getEndedAt())
|
||||
.createdAt(session.getCreatedAt())
|
||||
.updatedAt(session.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package com.loremind.infrastructure.web.controller;
|
||||
|
||||
import com.loremind.application.generationcontext.StreamChatForCampaignUseCase;
|
||||
import com.loremind.application.generationcontext.StreamChatForLoreUseCase;
|
||||
import com.loremind.application.generationcontext.StreamChatForSessionUseCase;
|
||||
import com.loremind.domain.generationcontext.ChatMessage;
|
||||
import com.loremind.domain.generationcontext.ChatUsage;
|
||||
import com.loremind.infrastructure.web.dto.generationcontext.ChatMessageDTO;
|
||||
import com.loremind.infrastructure.web.dto.generationcontext.ChatStreamCampaignRequestDTO;
|
||||
import com.loremind.infrastructure.web.dto.generationcontext.ChatStreamRequestDTO;
|
||||
import com.loremind.infrastructure.web.dto.generationcontext.ChatStreamSessionRequestDTO;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -42,14 +44,17 @@ public class AiChatController {
|
||||
|
||||
private final StreamChatForLoreUseCase streamChatForLoreUseCase;
|
||||
private final StreamChatForCampaignUseCase streamChatForCampaignUseCase;
|
||||
private final StreamChatForSessionUseCase streamChatForSessionUseCase;
|
||||
private final TaskExecutor taskExecutor;
|
||||
|
||||
public AiChatController(
|
||||
StreamChatForLoreUseCase streamChatForLoreUseCase,
|
||||
StreamChatForCampaignUseCase streamChatForCampaignUseCase,
|
||||
StreamChatForSessionUseCase streamChatForSessionUseCase,
|
||||
@Qualifier("applicationTaskExecutor") TaskExecutor taskExecutor) {
|
||||
this.streamChatForLoreUseCase = streamChatForLoreUseCase;
|
||||
this.streamChatForCampaignUseCase = streamChatForCampaignUseCase;
|
||||
this.streamChatForSessionUseCase = streamChatForSessionUseCase;
|
||||
this.taskExecutor = taskExecutor;
|
||||
}
|
||||
|
||||
@@ -74,6 +79,19 @@ public class AiChatController {
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat IA ancré sur une Session de jeu : récupère automatiquement la
|
||||
* Campagne / Lore / GameSystem associés + injecte le journal horodaté.
|
||||
*/
|
||||
@PostMapping(value = "/chat/stream-session", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter chatStreamSession(@RequestBody ChatStreamSessionRequestDTO body) {
|
||||
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MS);
|
||||
List<ChatMessage> messages = toDomainMessages(body.getMessages());
|
||||
|
||||
taskExecutor.execute(() -> runSessionStreaming(emitter, body.getSessionId(), messages));
|
||||
return emitter;
|
||||
}
|
||||
|
||||
// --- Exécution du streaming dans un thread dédié ------------------------
|
||||
|
||||
private void runLoreStreaming(
|
||||
@@ -111,6 +129,22 @@ public class AiChatController {
|
||||
}
|
||||
}
|
||||
|
||||
private void runSessionStreaming(
|
||||
SseEmitter emitter,
|
||||
String sessionId,
|
||||
List<ChatMessage> messages) {
|
||||
try {
|
||||
streamChatForSessionUseCase.execute(
|
||||
sessionId, messages,
|
||||
usage -> sendUsage(emitter, usage),
|
||||
token -> sendToken(emitter, token),
|
||||
() -> complete(emitter),
|
||||
error -> fail(emitter, error));
|
||||
} catch (Exception e) {
|
||||
fail(emitter, e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers SSE (un seul point d'écriture par type d'événement) --------
|
||||
|
||||
private void sendUsage(SseEmitter emitter, ChatUsage usage) {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.loremind.infrastructure.web.controller;
|
||||
|
||||
import com.loremind.application.playcontext.SessionService;
|
||||
import com.loremind.domain.playcontext.Session;
|
||||
import com.loremind.infrastructure.web.dto.playcontext.SessionDTO;
|
||||
import com.loremind.infrastructure.web.mapper.SessionMapper;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* REST Controller pour le Play Context.
|
||||
* Adaptateur d'infrastructure qui expose l'API REST des Sessions.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/sessions")
|
||||
public class SessionController {
|
||||
|
||||
private final SessionService sessionService;
|
||||
private final SessionMapper sessionMapper;
|
||||
|
||||
public SessionController(SessionService sessionService, SessionMapper sessionMapper) {
|
||||
this.sessionService = sessionService;
|
||||
this.sessionMapper = sessionMapper;
|
||||
}
|
||||
|
||||
public record StartSessionRequest(String campaignId) {}
|
||||
|
||||
public record RenameSessionRequest(String name) {}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<SessionDTO> startSession(@RequestBody StartSessionRequest request) {
|
||||
Session session = sessionService.startSession(request.campaignId());
|
||||
return ResponseEntity.ok(sessionMapper.toDTO(session));
|
||||
}
|
||||
|
||||
@GetMapping("/active")
|
||||
public ResponseEntity<SessionDTO> getActiveSession() {
|
||||
return sessionService.getActive()
|
||||
.map(s -> ResponseEntity.ok(sessionMapper.toDTO(s)))
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<SessionDTO>> getSessions(@RequestParam(value = "campaignId", required = false) String campaignId) {
|
||||
List<Session> sessions = (campaignId == null || campaignId.isBlank())
|
||||
? sessionService.getAll()
|
||||
: sessionService.getByCampaignId(campaignId);
|
||||
List<SessionDTO> dtos = sessions.stream()
|
||||
.map(sessionMapper::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<SessionDTO> getSessionById(@PathVariable String id) {
|
||||
return sessionService.getById(id)
|
||||
.map(s -> ResponseEntity.ok(sessionMapper.toDTO(s)))
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/end")
|
||||
public ResponseEntity<SessionDTO> endSession(@PathVariable String id) {
|
||||
Session ended = sessionService.endSession(id);
|
||||
return ResponseEntity.ok(sessionMapper.toDTO(ended));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
public ResponseEntity<SessionDTO> renameSession(@PathVariable String id,
|
||||
@RequestBody RenameSessionRequest request) {
|
||||
Session renamed = sessionService.renameSession(id, request.name());
|
||||
return ResponseEntity.ok(sessionMapper.toDTO(renamed));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteSession(@PathVariable String id) {
|
||||
sessionService.deleteSession(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.loremind.infrastructure.web.controller;
|
||||
|
||||
import com.loremind.application.playcontext.SessionEntryService;
|
||||
import com.loremind.domain.playcontext.EntryType;
|
||||
import com.loremind.domain.playcontext.SessionEntry;
|
||||
import com.loremind.infrastructure.web.dto.playcontext.SessionEntryDTO;
|
||||
import com.loremind.infrastructure.web.mapper.SessionEntryMapper;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* REST Controller pour les entrées de journal d'une Session.
|
||||
* Endpoints imbriqués sous /api/sessions/{sessionId}/entries.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/sessions/{sessionId}/entries")
|
||||
public class SessionEntryController {
|
||||
|
||||
private final SessionEntryService entryService;
|
||||
private final SessionEntryMapper entryMapper;
|
||||
|
||||
public SessionEntryController(SessionEntryService entryService, SessionEntryMapper entryMapper) {
|
||||
this.entryService = entryService;
|
||||
this.entryMapper = entryMapper;
|
||||
}
|
||||
|
||||
public record EntryRequest(EntryType type, String content, LocalDateTime occurredAt) {}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<SessionEntryDTO>> getEntries(@PathVariable String sessionId) {
|
||||
List<SessionEntryDTO> dtos = entryService.getBySessionId(sessionId).stream()
|
||||
.map(entryMapper::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<SessionEntryDTO> createEntry(@PathVariable String sessionId,
|
||||
@RequestBody EntryRequest request) {
|
||||
SessionEntry created = entryService.createEntry(
|
||||
sessionId,
|
||||
new SessionEntryService.EntryData(request.type(), request.content(), request.occurredAt())
|
||||
);
|
||||
return ResponseEntity.ok(entryMapper.toDTO(created));
|
||||
}
|
||||
|
||||
@PutMapping("/{entryId}")
|
||||
public ResponseEntity<SessionEntryDTO> updateEntry(@PathVariable String sessionId,
|
||||
@PathVariable String entryId,
|
||||
@RequestBody EntryRequest request) {
|
||||
SessionEntry updated = entryService.updateEntry(
|
||||
entryId,
|
||||
new SessionEntryService.EntryData(request.type(), request.content(), request.occurredAt())
|
||||
);
|
||||
return ResponseEntity.ok(entryMapper.toDTO(updated));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{entryId}")
|
||||
public ResponseEntity<Void> deleteEntry(@PathVariable String sessionId,
|
||||
@PathVariable String entryId) {
|
||||
entryService.deleteEntry(entryId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.loremind.infrastructure.web.dto.generationcontext;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DTO de requête pour le chat IA d'une Session de jeu.
|
||||
* Le contexte (lore, campagne, gamesystem, journal) est dérivé du sessionId
|
||||
* côté serveur — l'appelant n'a qu'à fournir l'id et les messages.
|
||||
*/
|
||||
@Data
|
||||
public class ChatStreamSessionRequestDTO {
|
||||
private String sessionId;
|
||||
private List<ChatMessageDTO> messages;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.loremind.infrastructure.web.dto.playcontext;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO pour l'entité Session — objet de transfert de l'API REST.
|
||||
*/
|
||||
@Data
|
||||
public class SessionDTO {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String campaignId;
|
||||
private LocalDateTime startedAt;
|
||||
/** Null = session en cours. */
|
||||
private LocalDateTime endedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private boolean active;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.loremind.infrastructure.web.dto.playcontext;
|
||||
|
||||
import com.loremind.domain.playcontext.EntryType;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO d'une entrée de journal de session.
|
||||
*/
|
||||
@Data
|
||||
public class SessionEntryDTO {
|
||||
|
||||
private String id;
|
||||
private String sessionId;
|
||||
private EntryType type;
|
||||
private String content;
|
||||
private LocalDateTime occurredAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.loremind.infrastructure.web.mapper;
|
||||
|
||||
import com.loremind.domain.playcontext.SessionEntry;
|
||||
import com.loremind.infrastructure.web.dto.playcontext.SessionEntryDTO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SessionEntryMapper {
|
||||
|
||||
public SessionEntryDTO toDTO(SessionEntry entry) {
|
||||
if (entry == null) return null;
|
||||
SessionEntryDTO dto = new SessionEntryDTO();
|
||||
dto.setId(entry.getId());
|
||||
dto.setSessionId(entry.getSessionId());
|
||||
dto.setType(entry.getType());
|
||||
dto.setContent(entry.getContent());
|
||||
dto.setOccurredAt(entry.getOccurredAt());
|
||||
dto.setCreatedAt(entry.getCreatedAt());
|
||||
dto.setUpdatedAt(entry.getUpdatedAt());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.loremind.infrastructure.web.mapper;
|
||||
|
||||
import com.loremind.domain.playcontext.Session;
|
||||
import com.loremind.infrastructure.web.dto.playcontext.SessionDTO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Mapper Session (domaine) ↔ SessionDTO (transport REST).
|
||||
*/
|
||||
@Component
|
||||
public class SessionMapper {
|
||||
|
||||
public SessionDTO toDTO(Session session) {
|
||||
if (session == null) return null;
|
||||
SessionDTO dto = new SessionDTO();
|
||||
dto.setId(session.getId());
|
||||
dto.setName(session.getName());
|
||||
dto.setCampaignId(session.getCampaignId());
|
||||
dto.setStartedAt(session.getStartedAt());
|
||||
dto.setEndedAt(session.getEndedAt());
|
||||
dto.setCreatedAt(session.getCreatedAt());
|
||||
dto.setUpdatedAt(session.getUpdatedAt());
|
||||
dto.setActive(session.isActive());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user