diff --git a/docs/Documentation_projet.docx b/docs/Documentation_projet.docx deleted file mode 100644 index fee4d34..0000000 Binary files a/docs/Documentation_projet.docx and /dev/null differ diff --git a/docs/plan.md b/docs/plan.md deleted file mode 100644 index d00e942..0000000 --- a/docs/plan.md +++ /dev/null @@ -1,881 +0,0 @@ -# Plan de développement LoreMind - -## Contexte du projet -LoreMind est une application d'aide aux Maîtres de Jeu (JDR) permettant de gérer le Lore, les campagnes, et intègre un moteur IA pour générer du contenu structuré à partir de templates. À terme, les données seront exportables vers FoundryVTT. - -## Stack technique -- **Frontend** : Angular -- **Backend Core (Données & Métier)** : Java (Spring Boot, DDD, Architecture Hexagonale) -- **Backend AI (Génération & LLM)** : Python (FastAPI, Ollama en local) -- **Base de données** : PostgreSQL - -## Architecture Backend Java -- Domain-Driven Design (DDD) strict -- Architecture Hexagonale (Ports et Adaptateurs) -- Bounded Contexts : LoreContext, CampaignContext, GenerationContext -- Cœur du domaine sans dépendances techniques (ni framework, ni base de données) - -## État actuel du projet - -### 🆕 Projet initialisé (14 avril 2026) -- [x] Création des dossiers structurels (core/, web/, brain/, docs/) -- [x] Documentation de contexte et règles (.windsurfrules, loremind-contexte.md) - -### 🚀 Backend Java - Phase 1 (16 avril 2026) -- [x] Initialisation du projet Spring Boot (pom.xml) -- [x] Configuration de la base de données PostgreSQL -- [x] Création des entités de domaine : - - LoreContext : Lore, LoreNode, Page, Template - - CampaignContext : Campaign, Arc, Chapter, Scene -- [x] Création des Ports (Repositories interfaces) pour TOUS les contexts -- [x] Création des Adaptateurs d'infrastructure : - - JPA Entities (LoreJpaEntity, LoreNodeJpaEntity, etc.) - - JPA Repositories (LoreJpaRepository, etc.) - - Postgres Repositories (PostgresLoreRepository, etc.) -- [x] Création des Services d'application PARTIELS : - - LoreService ✅ - - CampaignService ✅ -- [x] Création des Services d'application restants : - - LoreNodeService ✅ - - PageService ✅ - - TemplateService ✅ - - ArcService ✅ - - ChapterService ✅ - - SceneService ✅ -- [x] Création des DTOs et Mappers : - - LoreContext DTOs ✅ - - CampaignContext DTOs ✅ - - Mappers pour toutes les entités ✅ -- [x] Création des REST Controllers : - - LoreController, CampaignController ✅ - - LoreNodeController, PageController, TemplateController ✅ - - ArcController, ChapterController, SceneController ✅ -- [x] Configuration CORS ✅ -- [ ] Création du GenerationContext (entités, ports, adaptateurs, services) - -### ⏳ À faire - -#### Phase 2 : Frontend Angular (Priorité haute) -- [x] Initialisation du projet Angular ✅ -- [x] Création du layout de base (Sidebar + Main Content) ✅ -- [x] Création du composant Sidebar ✅ (redesign complet : logo, palette violette, OUTILS, version) -- [x] Configuration routing Angular ✅ -- [x] Styles globaux dark theme ✅ -- [x] Installation lucide-angular (icônes SVG) ✅ -- [x] Page Lore (Vos Univers) — grille de cartes, carte "Nouveau Lore" ✅ -- [x] Page Campagnes — grille de cartes, carte "Nouvelle Campagne" ✅ -- [x] Modal création de Lore (LoreCreateComponent) ✅ -- [x] Branchement LoreService.createLore() ✅ -- [x] Composant SecondarySidebar (réutilisable Lore + Campagne) ✅ -- [x] LayoutService (contrôle affichage secondary sidebar) ✅ -- [x] Page détail Lore (/lore/:id) avec arborescence noeuds ✅ -- [x] Page détail Campagne (/campaigns/:id) avec arborescence arcs/chapitres ✅ -- [x] Formulaire création Campagne (modal) ✅ -- [x] Sidebar globale contextuelle (liste lores/campagnes + retour) ✅ -- [x] Secondary sidebar avec toggle pli/dépli ✅ -- [x] Écran création de noeud (Lore) ✅ -- [x] Refonte secondary sidebar : actions contextuelles (Lore: + Noeud / + Page, Campagne: + Nouvel arc). Bouton "Sauvegarder" retiré (persistance immédiate via REST). ✅ -- [x] Écran création/modification de page (Lore) ✅ — Phase 5A livrée : domaine Page enrichi (`loreId`, `values: Map`, `notes`, `tags`, `relatedPageIds`), écrans `page-create` (titre + grille de templates + noeud pré-rempli depuis `template.defaultNodeId`) et `page-edit` basique (champs dynamiques du template rendus en textarea + notes privées). Pages affichées dans l'arbre sous leur noeud + action "+ Nouvelle page" par noeud. -- [x] Écran création/modification de template (Lore) ✅ (domaine enrichi `loreId` + `defaultNodeId` + `List fields`, panneau sidebar "Templates" fidèle à la maquette) -- [x] Écran création d'arc (Campagne) ✅ (champ `illustration` reporté — à ajouter quand l'écran détail d'un arc sera implémenté) -- [x] Écran création de chapitre (Campagne) ✅ (chargement chapitres existants + action "+ Nouveau chapitre" inline dans l'arbre) -- [x] Écran création de scène (Campagne) ✅ -- [x] Refactor : helper `campaign-tree.helper.ts` (chargement arbre + construction TreeItem[]) + rendu récursif 3 niveaux dans SecondarySidebar ✅ -- [x] Écrans détail/modification (Campagne) : arc-edit, chapter-edit, scene-edit avec Sauvegarder + Supprimer ✅ -- [x] SecondarySidebar : séparation chevron/label pour permettre expand + navigation sur un même item ✅ -- [x] **Enrichissement domaine narratif Arc/Chapter/Scene** (sous-tâches 1-3 ✅, sous-tâche 4 cross-context Lore↔Campaign restante) - -### Enrichissement du domaine Template (Lore) ✅ (18 avril 2026) -Maquettes : `docs/maquettes/lore/Création de template.png` et `Modification de template.png`. - -**Changements backend Java (Option i : champs typés explicites, DDD-friendly) :** -- `Template.java` : ajout `loreId`, `defaultNodeId`, `List fields` ; suppression `Map structure` ; méthodes métier `fieldCount`, `addField`, `removeField`. -- `TemplateRepository.java` (port) : ajout `findByLoreId(String loreId)`. -- `TemplateJpaEntity.java` : colonnes typées `lore_id` (NOT NULL), `default_node_id`, `fields` (TEXT via converter JSON). -- Nouveau converter `StringListJsonConverter` : `List` ↔ JSON pour la persistance PostgreSQL. -- `PostgresTemplateRepository.java` : mapping bidirectionnel enrichi + implémentation `findByLoreId`. -- `TemplateJpaRepository.java` : méthode dérivée `findByLoreId(Long)`. -- `TemplateDTO.java` + `TemplateMapper.java` : alignement sur le nouveau domaine (expose `fieldCount` calculé côté serveur). -- `TemplateService.java` : signatures refactorées ; `updateTemplate` en Parameter Object pattern ; `loreId` volontairement immuable (pas de migration cross-Lore via simple update). -- `TemplateController.java` : `POST /api/templates`, `GET /api/templates?loreId=X`, `GET /{id}`, `PUT /{id}`, `DELETE /{id}`. Retrait du `PATCH /{id}/structure` obsolète. - -**Changements frontend Angular :** -- `template.model.ts`, `template.service.ts` : modèle TS + service HTTP complet. -- `layout.service.ts` : nouveaux types `BottomPanel`, `BottomPanelItem` ; `SecondarySidebarConfig.bottomPanel?` ajouté. `footerLabel` déprécié (gardé optionnel pour compat des callers campagne). -- `SecondarySidebarComponent` : rendu du `bottomPanel` avec toggle ouvert/fermé, items cliquables + meta (ex: `"8 champs"`). -- Nouveau helper `@app/lore/lore-sidebar.helper.ts` : charge lore + all lores + nodes + templates en parallèle et construit la config sidebar complète (panneau Templates inclus). Utilisé par `lore-detail` et `lore-node-create` (→ résout la dette "duplications de pattern côté Lore"). -- `TemplateCreateComponent` et `TemplateEditComponent` : écrans fidèles aux maquettes (colonne gauche = identité, colonne droite = liste dynamique ajout/suppression de champs). -- Routes ajoutées : `/lore/:loreId/templates/create` et `/lore/:loreId/templates/:templateId`. - -### Enrichissement du domaine Page (Lore) — Phase 5A ✅ (18 avril 2026) -Maquettes : `docs/maquettes/lore/création de page.png` et `Modification d'une page.png`. - -**Changements backend Java (structure alignée sur les maquettes) :** -- `Page.java` : suppression `content: String` (trop vague) ; ajout `loreId`, `values: Map` (valeurs des champs dynamiques du template, clé = `fieldName`), `notes` (privé MJ), `tags: List`, `relatedPageIds: List`. Méthodes métier `setFieldValue`, `getFieldValue`, `addTag`, `removeTag`. -- `PageRepository.java` : ajout `findByLoreId(String loreId)`. -- `PageJpaEntity.java` : colonne `lore_id` NOT NULL, `values_json` (colonne renommée car `values` est un mot-clé SQL), `notes`, `tags`, `related_page_ids` — tous stockés en JSON (TEXT) via converters. -- Nouveau converter `StringMapJsonConverter` : `Map` ↔ JSON (distinct du `MapJsonConverter` générique `Map` dont Page n'a pas besoin). -- `PostgresPageRepository.java` + `PageJpaRepository.java` : mapping et méthode dérivée `findByLoreId`. -- `PageDTO.java` + `PageMapper.java` : alignement complet. -- `PageService.java` : `createPage(loreId, nodeId, templateId, title)` minimaliste ; `updatePage(id, changes)` Parameter Object (applique title/nodeId/values/notes/tags/relatedPageIds). `loreId` et `templateId` immuables après création. -- `PageController.java` : `POST /api/pages`, `GET /api/pages?loreId=X` ou `?nodeId=Y`, `GET /{id}`, `PUT /{id}`, `DELETE /{id}`. Suppression de `PATCH /{id}/template` et de `GET /node/{nodeId}` (unifié en query param). - -**Changements frontend Angular :** -- `page.model.ts`, `page.service.ts` : modèle TS + service HTTP. -- `lore-sidebar.helper.ts` enrichi : charge aussi les pages ; les pages apparaissent dans l'arbre **sous leur noeud** (TreeItem `children`) ; chaque noeud a un item action "+ Nouvelle page" pointant vers `/lore/:loreId/nodes/:nodeId/pages/create` ; bouton "+ Page" du header de sidebar pointe vers `/lore/:loreId/pages/create` (choix libre du template). -- `PageCreateComponent` : fidèle à la maquette — titre + grille de cartes Template sélectionnables + select Noeud de destination (pré-rempli depuis `template.defaultNodeId` si l'URL n'impose pas déjà un nodeId). Redirige vers `page-edit` après création. -- `PageEditComponent` basique — titre, noeud (déplaçable), **un textarea par `field` du template** (valeurs stockées dans `values`), notes privées. Bouton "Assistant IA" stub (sera branché en Phase 3 Python). -- Routes ajoutées : `/lore/:loreId/pages/create`, `/lore/:loreId/nodes/:nodeId/pages/create`, `/lore/:loreId/pages/:pageId`. - -### Renommage "dossier" + icônes + hiérarchie ✅ (18 avril 2026) -Triple corrections suite au retour utilisateur : - -**Renommage UI "noeud" → "dossier"** (nom interne `LoreNode` conservé côté Java pour limiter l'impact BDD et code) : -- Textes visibles mis à jour dans : `lore-node-create.*`, `lore-detail.*`, `lore.component.*`, `page-create.*`, `page-edit.*`, `template-create.*`, `template-edit.*`, bouton sidebar `+ Dossier`. - -**Bug corrigé — icônes de dossier invisibles :** -- Côté backend : l'icône envoyée par l'UI était silencieusement ignorée (`icon` absent de `LoreNode.java`, `LoreNodeDTO.java`, du mapping JPA). Ajouté partout : domaine + JPA entity (colonne `icon`) + DTO + mapper + repository + service (Parameter Object pour `createLoreNode`/`updateLoreNode`). -- Côté frontend : nouveau registre partagé `@app/lore/lore-icons.ts` (`LORE_ICON_OPTIONS` + `resolveIcon(key)`). Refactor de `lore-node-create` pour utiliser ce registre. Sidebar rend l'icône via `TreeItem.iconKey` (nouveau champ) + méthode `iconFor()`. - -**Dossiers imbriqués :** -- Le backend supportait déjà `parentId` — seul le frontend ne l'exposait pas. Ajout du champ `parentId` à `LoreNode`/`LoreNodeCreate` TS + formulaire `lore-node-create` (select "Dossier parent"). Nouvelle route `/lore/:loreId/folders/:parentId/create` pour pré-remplir depuis la sidebar. -- Helper `lore-sidebar.helper.ts` refactoré : construction récursive de l'arbre (fonction `buildFolderItem`). Chaque dossier affiche ses sous-dossiers + ses pages + actions `+ Nouveau dossier` et `+ Nouvelle page` inline. - -### Édition et suppression de dossier ✅ (18 avril 2026) -Complète la CRUD manquante pour les `LoreNode` (dossiers). - -**Frontend Angular :** -- `LoreService` enrichi : `getLoreNodeById`, `updateLoreNode`, `deleteLoreNode`. URL des lore-nodes factorisée dans `nodesUrl`. -- Nouveau `LoreNodeEditComponent` (`lore-node-edit/`) — formulaire avec nom, icône (grille), dossier parent (select). Header avec boutons Annuler / Supprimer / Sauvegarder. -- Helper `lore-sidebar.helper.ts` : ajout de la fonction utilitaire `collectDescendantIds(rootId, allNodes)` qui calcule de manière itérative l'ensemble des descendants d'un dossier. Utilisée pour **empêcher les cycles** : le select "Dossier parent" de l'édition exclut le dossier courant et tous ses descendants. -- Chaque dossier dans la sidebar a maintenant une `route` pointant vers son écran d'édition (clic sur le label → édition, clic sur le chevron → expand/collapse). -- Route ajoutée : `/lore/:loreId/folders/:folderId/edit`. - -**Règle de suppression (safe) :** -- La suppression est **refusée si le dossier contient des sous-dossiers ou des pages** (message explicite "Videz-le d'abord : X sous-dossier(s) et Y page(s)"). -- Raison : protéger les notes MJ contre un clic accidentel. Pas de cascade silencieuse. -- La vérification se fait côté frontend à partir des données déjà chargées dans la sidebar (pas d'appel HTTP supplémentaire). Le backend accepte le DELETE sans check, mais le frontend ne l'émet jamais si le dossier n'est pas vide. - -### Pages — Phases à venir - -#### Phase 5B : édition complète ✅ (18 avril 2026) -- [x] **Tags (chips)** — nouveau composant réutilisable `app-chips-input` (`@app/shared/chips-input/`). UX : Entrée ou virgule pour ajouter, Backspace sur input vide retire le dernier chip, doublons silencieusement ignorés, trim automatique. Binding deux-sens via `[value]` / `(valueChange)`. -- [x] **Liens vers d'autres pages** — nouveau composant `app-lore-link-picker` (`@app/shared/lore-link-picker/`). Input de recherche avec dropdown de suggestions filtrées (max 8), chips cliquables pour chaque page liée (clic → navigation, X → retrait). Exclut la page courante via `excludePageId`. -- [x] **Intégration dans `page-edit`** : sections "Tags" et "Pages liées" ajoutées entre les champs dynamiques et les notes privées. `allPages: Page[]` récupéré depuis la sidebar pour alimenter le picker. -- [x] Le composant `app-lore-link-picker` est **conçu pour être réutilisé** en Phase cross-context Campagne↔Lore (Arc/Chapter/Scene pourront lier des Pages du Lore via ce même picker). - -#### Phase 5C : affichage et navigation fine -- [x] Compteur de pages sur chaque dossier dans le sidebar (ex: `PNJ · 3`) ✅ (19 avril 2026). -- [x] Correction compteurs home "Vos univers" (nodeCount/pageCount calculés à la volée via `countByLoreId` au lieu d'être stockés en BDD et jamais MAJ) ✅ (19 avril 2026). -- [x] Breadcrumb `Lore > Dossier > Page` dans `page-edit` ✅ (19 avril 2026) — composant réutilisable `app-breadcrumb` prêt pour Arc/Chapter/Scene. -- [ ] Raccourci clavier `Ctrl+S` pour sauvegarder. - -#### Phase 5D : intégration IA (dépend de la Phase 3 Python) -- [ ] Branchement du bouton "Assistant IA" de `page-edit` sur l'endpoint Python Ollama. -- [ ] Affichage streaming des suggestions par champ. - -### Enrichissement du domaine narratif (Campagne) -Les maquettes (`docs/maquettes/campagne/détail/*.png`) révèlent que chaque entité narrative doit porter bien plus que `name + description`. Voici le détail des champs manquants qui doivent être ajoutés côté Java (domain + JPA + DTO + mapper) et côté Angular (model + forms). - -**Approche retenue :** -- Colonnes TEXT dédiées (pas de JSONB) car chaque champ a un sens métier précis et sera utilisé par l'Assistant IA (Phase 3) comme prompt structuré. -- Les **liens vers le Lore** (PNJ, lieux, objets) = IDs stockés en JSONB ou table de liaison, **pas** de relation JPA cross-context (principe DDD : Bounded Contexts isolés). - -#### Sous-tâche 1 : Arc (5 champs texte) ✅ -- [x] `themes` — Thèmes principaux ✅ -- [x] `stakes` — Enjeux globaux ✅ -- [x] `gmNotes` — Notes et planification du MJ (privé, non exporté FoundryVTT) ✅ -- [x] `rewards` — Récompenses et progression ✅ -- [x] `resolution` — Dénouement prévu ✅ -- Impact : `Arc.java` (domain), `ArcJpaEntity.java` (JPA + colonnes TEXT auto via `ddl-auto=update`), `ArcDTO.java`, `ArcMapper.java`, `PostgresArcRepository.java`, `ArcService.updateArc()` refactoré en Parameter Object pattern, `ArcController.updateArc()`, `campaign.model.ts`, `arc-edit.component.*` (formulaire enrichi, arc-create reste volontairement minimal comme la maquette). - -#### Sous-tâche 2 : Chapter (3 champs texte) ✅ -- [x] `gmNotes` — Notes du Maître de Jeu (privé) ✅ -- [x] `playerObjectives` — Objectifs des joueurs ✅ -- [x] `narrativeStakes` — Enjeux narratifs ✅ -- Impact : `Chapter.java`, `ChapterJpaEntity.java`, `ChapterDTO.java`, `ChapterMapper.java`, `PostgresChapterRepository.java`, `ChapterService.updateChapter()` (Parameter Object), `ChapterController.updateChapter()`, `campaign.model.ts`, `chapter-edit.component.*`. - -#### Sous-tâche 3 : Scene (8 champs texte répartis en sections) ✅ -Sections de la maquette (chaque section est un bloc d'UI expandable) : -- [x] **Contexte et ambiance** : `location` (court), `timing` (court), `atmosphere` (long) ✅ -- [x] **Narration pour les joueurs** : `playerNarration` (long) ✅ -- [x] **Notes et secrets du MJ** (privé, variant "private" rouge) : `gmSecretNotes` (long) ✅ -- [x] **Choix et conséquences** : `choicesConsequences` (long) ✅ -- [x] **Combat ou rencontre** : `combatDifficulty` (court), `enemies` (long) ✅ -- Impact : `Scene.java`, `SceneJpaEntity.java`, `SceneDTO.java`, `SceneMapper.java`, `PostgresSceneRepository.java`, `SceneService.updateScene()` (Parameter Object), `SceneController.updateScene()`, `campaign.model.ts`, `scene-edit.component.*` (11 champs totaux, 5 sections expandables). -- **Nouveau composant partagé** : `@app/shared/expandable-section/` — réutilisable (propriétés : `title`, `icon` emoji, `initiallyOpen`, `variant: 'default' | 'private'`). - -#### Sous-tâche 4 : Liens cross-context Lore ↔ Campaign -Tous des `List` d'IDs de LoreNode : -- [ ] Arc : `antagonistNpcIds`, `allyNpcIds` -- [ ] Chapter : `involvedNpcIds`, `visitedLocationIds` -- [ ] Scene : `presentNpcIds`, `importantObjectIds` -- [ ] Composant Angular réutilisable : `app-lore-link-picker` (autocomplete + liste de chips) -- [ ] Endpoint `GET /api/lore-nodes?ids=a,b,c` (résolution multi-IDs) côté Java - -#### Phase 3 : Backend Python — Brain IA (DÉMARRÉE le 19 avril 2026) - -Stack retenue : **FastAPI + Ollama local + Architecture Hexagonale** (Ports/Adapters via `Protocol` PEP 544). Le Brain est l'executor cognitif : il reçoit des demandes du Core Java, construit un prompt, appelle un LLM et renvoie un résultat structuré. Le Core Java reste le chef d'orchestre (règle `.windsurfrules` ligne 21). - -##### Étape b1 — Squelette FastAPI ✅ -- [x] Structure `brain/app/` + venv + `requirements.txt` minimal (fastapi, uvicorn). -- [x] Endpoint `GET /health` (sonde de vie) et Swagger UI auto sur `/docs`. -- [x] `.gitignore` (venv, __pycache__, .env). - -##### Étape b2 — Refactor hexagonal ✅ -- [x] Config Pydantic Settings (`.env.example` + `app/core/config.py` + `get_settings()` singleton via `@lru_cache`). -- [x] Port `LLMProvider` (Protocol PEP 544) + exception domaine `LLMProviderError` dans `app/domain/ports.py`. -- [x] Adapter `OllamaLLMProvider` dans `app/infrastructure/ollama_adapter.py` — isole tout le code `httpx` et le protocole Ollama. -- [x] Factory `get_llm_provider()` dans `main.py` = **unique point d'inversion de dépendance** (changer d'1 ligne pour switch vers OpenAI/Claude demain). -- [x] Controller `POST /generate` fin : reçoit le port via `Depends`, ignore l'Adapter concret. -- [x] Fiche academy `docs/academy/hexagonal-python.md` (théorie + analogie JDR + Python vs Java + quiz 5 QCM). -- Modèle par défaut : `gemma4:e2b` (validé avec sortie "Borin le forgeron nain" en ~2s). Swap possible via `LLM_MODEL` dans `.env`. - -##### Étape b3 — Génération structurée JSON ✅ -- [x] **b3.1** — Modèles de domaine `PageGenerationContext` / `PageGenerationResult` en `@dataclass(frozen=True)` dans `app/domain/models.py` (domaine sans dépendance Pydantic). -- [x] **b3.1** — Port `LLMProvider.generate()` enrichi d'un kwarg `output_format: str | None` (pass-through vers `format: "json"` d'Ollama). -- [x] **b3.2** — Use case `GeneratePageUseCase` dans `app/application/generate_page.py` : construction prompt système (français, orienté MJ de JDR) + appel LLM avec `output_format="json"` + parsing JSON défensif (filtrage sur `template_fields`, champs manquants → chaîne vide, cast `str` systématique). -- [x] **b3.3** — Endpoint `POST /generate-page` + DTOs Pydantic `GeneratePageRequestDTO` / `GeneratePageResponseDTO` en frontière HTTP + factory `get_generate_page_use_case()` pour l'injection du use case. - -##### Étape b4 — Branchement Core Java ↔ Brain ✅ (19 avril 2026, soir) - -> ✅ **Chaîne complète opérationnelle** : clic "Assistant IA" dans `page-edit` → Angular → Java (`PageGenerationController` → `GeneratePageValuesUseCase` → `BrainAiClient`) → Python (`/generate-page`) → Ollama → retour JSON → merge dans les textareas. Zéro persistance côté génération, l'utilisateur valide et sauvegarde manuellement. Une nouvelle fiche academy `docs/academy/bounded-context.md` a été ajoutée pour formaliser le 3ᵉ Bounded Context (GenerationContext). -> -> **Démarrage de la stack complète pour tester :** -> ```bash -> # Terminal 1 — Ollama (normalement déjà en service système) -> # http://localhost:11434, modèle `gemma4:e2b` tiré -> -> # Terminal 2 — Brain Python -> cd brain && source .venv/Scripts/activate && uvicorn app.main:app --reload --port 8000 -> -> # Terminal 3 — Core Java -> cd core && mvn spring-boot:run -> -> # Terminal 4 — Frontend Angular -> cd web && npm start -> ``` - -###### b4.1 — Domaine DDD côté Java (GenerationContext) ✅ -- [x] Package `domain/generationcontext/` créé. -- [x] `GenerationContext` (immuable `@Value @Builder`) + `GenerationResult` (immuable `@Value`). -- [x] Port `AiProvider` (interface pure, zéro annotation Spring) + exception domaine `AiProviderException` (RuntimeException). - -###### b4.2 — Adapter HTTP Java → Brain Python ✅ -- [x] `BrainAiClient` dans `infrastructure/ai/` — implémente `AiProvider`. -- [x] **Revirement technique assumé** : `RestTemplate` retenu plutôt que `WebClient`. Raisons : déjà présent dans `spring-boot-starter-web` (zéro nouvelle dépendance), usage synchrone suffisant pour un bouton "Assistant IA" (pas de streaming au MVP), plus simple à lire pour un développeur qui n'a pas encore rencontré le paradigme réactif Reactor. Le passage à `WebClient` reste possible sans toucher au domaine (c'est précisément le bénéfice de l'hexagonal). -- [x] Config `brain.base-url` + `brain.timeout-seconds` dans `application.properties`, bean `RestTemplate` dédié dans `RestTemplateConfig` (connect 10s, read 120s). -- [x] DTOs package-private `BrainGeneratePageRequest` / `BrainGeneratePageResponse` avec `@JsonProperty` snake_case (pas de config Jackson globale — isolation au package `infrastructure/ai`). -- [x] Traduction d'erreurs : `ResourceAccessException` (timeout/Brain down) / `RestClientResponseException` (4xx/5xx) → `AiProviderException`. Filet de sécurité pour toute autre `Exception`. - -###### b4.3 — Use case Java `GeneratePageValuesUseCase` ✅ -- [x] `application/generationcontext/GeneratePageValuesUseCase.java` — injection constructeur des 5 ports (PageRepository, TemplateRepository, LoreRepository, LoreNodeRepository, AiProvider). -- [x] `execute(pageId)` : 4 lookups (Page → Template → Lore → LoreNode), validation template.fields non-vide, construction du `GenerationContext`, appel `AiProvider.generatePage`, retour direct de `result.values`. -- [x] **Décision produit respectée** : zéro persistance. Exceptions différenciées : `IllegalArgumentException` (page introuvable) vs `IllegalStateException` (incohérence BDD ou template sans champs). - -###### b4.4 — REST endpoint Java ✅ -- [x] **Écart assumé vs plan initial** : endpoint placé dans un nouveau `PageGenerationController` dédié (pas dans `PageController`) — SRP strict, alignement sur le Bounded Context `generationcontext`. URL RESTful conservée : `POST /api/pages/{id}/generate`. -- [x] DTO `GenerationSuggestionsDTO { Map values }` dans `infrastructure/web/dto/generationcontext/`. -- [x] Gestion d'erreurs inline (pas d'`@ControllerAdvice` — cohérent avec le style du reste du projet) : 200, 404, 422 (template sans champs, détecté sur le message), 502 (`AiProviderException`), 500 (autre `IllegalStateException`). - -###### b4.5 — Frontend Angular : bouton "Assistant IA" branché ✅ -- [x] `PageService.generateValues(pageId)` → `POST /api/pages/{id}/generate`, retourne `Record`. -- [x] `page-edit` : état `aiLoading` + `aiError`, méthode `runAssistantAI()`, libellé du bouton qui passe à "Génération…" pendant l'appel, bouton désactivé si template sans champs ou appel en cours. -- [x] Merge soft simplifié : toute suggestion non-vide écrase (l'utilisateur a demandé la régénération), suggestion vide laisse la valeur courante intacte. -- [x] Banner d'erreur dismissable au-dessus du formulaire : message différencié 502 (Brain down) vs autre. -- [x] Pas de sauvegarde auto — l'utilisateur valide et clique "Sauvegarder" pour persister via le PUT existant. - -###### b4.6 — Améliorations post-MVP (backlog, pas bloquant) -- [ ] Température LLM configurable côté UI (slider "créativité" mappé sur le paramètre `temperature` d'Ollama). -- [ ] Historique des générations par page (BDD côté Java + vue "revert to previous suggestion"). -- [ ] Retry automatique avec backoff côté `BrainAiClient` en cas d'erreur transitoire. -- [ ] Prompt personnalisable par Lore (ex: ton "sombre et épique" vs "aventure familiale") — stocké sur l'entité `Lore` côté Java, transmis dans le `GenerationContext`. - -##### Étape b5 — Chat IA conversationnel avec Structural Context ✅ (19 avril 2026, soir) - -> ✅ **UX "IA qui écrit sous tes yeux"** livrée de bout en bout. Drawer chat à droite de `page-edit` (pattern validé sur les maquettes `lore/Assistance IA dans une page.png` et `campagne/Assistance IA.png`). L'IA voit la structure du Lore (dossiers, pages, templates, tags) sans recevoir le contenu. Conversation éphémère (perdue à la fermeture du drawer). Intégration limitée au Lore pour l'instant — Campagne viendra quand on voudra l'étendre. - -###### b5.1 — Backend Python : endpoint `/chat/stream` SSE ✅ -- [x] Dataclasses `ChatMessage` + `LoreStructuralContext` dans `domain/models.py` (immuables, sans Pydantic). -- [x] Protocol `LLMChatProvider` dans `domain/ports.py` — distinct de `LLMProvider` par ISP (Interface Segregation Principle). `OllamaLLMProvider` satisfait les deux par duck typing. -- [x] `OllamaLLMProvider.stream_chat()` : consomme `/api/chat` d'Ollama en mode `stream=True`, parse le NDJSON ligne par ligne, yield les tokens non-vides. Le formatage SSE est la responsabilité du controller, pas de l'adapter. -- [x] `ChatUseCase` dans `application/chat.py` : construit un system prompt riche avec le Structural Context (carte des dossiers/pages/templates/tags), délègue au port. -- [x] Endpoint `POST /chat/stream` avec `StreamingResponse(media_type="text/event-stream")`. Format de flux : `data: {"token":"..."}`, `event: done`, `event: error`. - -###### b5.2 — Core Java : port `AiChatProvider` + adapter `WebClient` SSE ✅ -- [x] Ajout de `spring-boot-starter-webflux` au `pom.xml` (requis pour `WebClient`, seul outil Spring capable de consommer SSE côté client) + `spring.main.web-application-type=servlet` dans `application.properties` pour forcer Tomcat malgré WebFlux. -- [x] Domaine `generationcontext/` : `ChatMessage`, `LoreStructuralContext` (+ inner `FolderPage`), `ChatRequest`, port `AiChatProvider`. -- [x] **Choix pédagogique : API par callbacks** (`Consumer onToken`, `Runnable onComplete`, `Consumer onError`) plutôt que `Flux`. Raisons : zéro dépendance Reactor dans le domaine, plus simple à comprendre pour un développeur qui n'a pas rencontré le paradigme réactif, mappage naturel vers `SseEmitter` côté controller. -- [x] Adapter `BrainAiChatClient` : `WebClient.retrieve().bodyToFlux(ServerSentEvent)`, dispatch `doOnNext` → callbacks, `blockLast()` pour rester synchrone, timeout 120s, traduction d'erreurs en `AiProviderException`. - -###### b5.3 — Core Java : endpoint `POST /api/ai/chat/stream` SSE ✅ -- [x] Use case `StreamChatForLoreUseCase` dans `application/generationcontext/` : charge `Lore` + `LoreNode[]` + `Page[]` + `Template[]` (4 lookups), construit le `LoreStructuralContext`, délègue au port. 4 ports injectés côté LoreContext + 1 côté GenerationContext. -- [x] `AiChatController` expose `POST /api/ai/chat/stream` (`produces = text/event-stream`). Le streaming tourne dans un thread séparé via `AsyncTaskExecutor` pour ne pas bloquer le servlet. `SseEmitter` (timeout 5 min) thread-safe : les callbacks du port peuvent écrire dessus depuis n'importe quel thread. -- [x] DTOs `ChatMessageDTO` + `ChatStreamRequestDTO` dans `infrastructure/web/dto/generationcontext/`. Helper `jsonEscape()` interne (pas de pull Jackson ici). - -###### b5.4 — Frontend : composant `app-ai-chat-drawer` ✅ -- [x] Service `AiChatService.streamChat()` dans `web/src/app/services/ai-chat.service.ts` : `fetch()` + `ReadableStream` + décodage ligne-par-ligne SSE. Retourne un `Observable` qui emit `{type:'token'}` par fragment, complete sur `event: done`, error sur `event: error` ou échec réseau. Annule proprement via `AbortController` à l'unsubscribe. -- [x] **Pas d'`EventSource`** : l'API navigateur native ne supporte que GET sans body — on a besoin de POST avec JSON (messages + loreId). -- [x] Composant standalone `AiChatDrawerComponent` dans `shared/ai-chat-drawer/` : `@Input` `loreId`/`isOpen`/`welcomeMessage`/`quickSuggestions[]`/`primaryAction`, `@Output` `close`/`primaryActionClick`. État local : `messages[]`, `currentAssistantText` (buffer de streaming), `isStreaming`, `errorMessage`. Conversation éphémère perdue à la fermeture (choix MVP assumé). -- [x] UX fidèle aux maquettes : bulles user (droite violet) / assistant (gauche sombre), welcome message, typing-indicator avant le premier token, caret clignotant pendant le streaming, suggestions rapides en bas, input + bouton envoyer. - -###### b5.5 — Intégration dans `page-edit` ✅ -- [x] Bouton "Assistant IA" du header change de rôle : il ouvre désormais le drawer (`toggleChat()`) au lieu d'appeler le one-shot directement. -- [x] Le one-shot b4 reste accessible via `primaryAction` du drawer (bouton violet pleine largeur "Remplir automatiquement tous les champs") : clic → ferme le drawer + déclenche `runAssistantAI()` → textareas se remplissent. Le meilleur des deux mondes, sans duplication de code. -- [x] Suggestions rapides hardcodées (MVP) : "Étoffe l'histoire", "Suggère des liens avec d'autres pages du Lore", "Propose une intrigue secondaire". -- [x] Bouton "Assistant IA" stylé `active` quand le drawer est ouvert (fond gris + bordure violette) — feedback visuel clair. - -###### b5.6 — Fiche academy `streaming-sse-rag.md` ✅ -- [x] Théorie : SSE vs WebSocket vs polling avec analogie JDR (pigeon voyageur qui revient par fragments). -- [x] Structural Context vs RAG sémantique : pourquoi on n'a PAS encore de DB vectorielle. -- [x] Code réel extrait des 3 étages de la stack (Python / Java / Angular). -- [x] Section "Le savais-tu ?" sur la nature debug-friendly du format SSE (`curl -N` suffit). -- [x] Quiz 5 QCM. - -###### b5.7 — Intégration dans la Campagne (arc / chapter / scene) ✅ (20 avril 2026, après-midi) - -> ✅ **Drawer IA disponible sur les 3 écrans de Campagne.** Un MJ peut dialoguer avec l'IA depuis l'arc, le chapitre ou la scène en cours. Le prompt système reçoit automatiquement l'arbre narratif (noms seulement — pas de contenu), les champs de l'entité focus, et — si la campagne est liée à un Lore — les templates + l'arbre des pages de ce Lore. **Asymétrie respectée** : un Lore ne voit PAS ses campagnes (sens unique Campagne → Lore). - -###### b5.7.1 — Value Objects narratifs (core/domain) ✅ -- [x] `CampaignStructuralContext` : arbre `campaignName + campaignDescription + List` avec `ArcSummary(name + chapters)` et `ChapterSummary(name + sceneNames)`. Lombok `@Value @Builder @Singular`. -- [x] `NarrativeEntityContext` : VO "focus" avec `entityType ∈ {arc, chapter, scene}` + `title` + `Map fields` (description, themes, stakes, playerObjectives, atmosphere…). -- [x] `ChatRequest` étendu : `loreContext` nullable, `campaignContext` et `narrativeEntity` ajoutés. Un chat Lore continue à fonctionner inchangé. - -###### b5.7.2 — Builders applicatifs (DRY + cross-context) ✅ -- [x] `LoreStructuralContextBuilder` extrait en `@Component` partagé : `build(loreId)` lance une exception si absent, `buildOptional(loreId)` retourne `Optional.empty()` pour dégradation gracieuse (Lore supprimé entre deux appels). -- [x] `CampaignStructuralContextBuilder` : traverse Campagne → Arcs (triés par `order`) → Chapters (triés) → noms de Scenes (triés). Pas de contenu, juste la structure. -- [x] `NarrativeEntityContextBuilder` : switch sur `entityType`, mappe les champs domaine vers la Map via `putIfNotBlank`. Ne fuit aucun secret MJ vers le prompt joueur (c'est un chat MJ, donc tout est exposé — mais le découpage par champ reste explicite). -- [x] `StreamChatForCampaignUseCase` : orchestre Campaign → Lore optionnel (via `campaign.isLinkedToLore()`) → Narrative entity optionnelle → délégation au port `AiChatProvider`. -- [x] `StreamChatForLoreUseCase` refactoré : 182 → 114 lignes, délègue à `LoreStructuralContextBuilder` (DRY). - -###### b5.7.3 — Pont Java ↔ Python ✅ -- [x] `BrainAiChatClient.toPayload()` : 4 contextes optionnels (`lore_context`, `page_context`, `campaign_context`, `narrative_entity`) ajoutés au JSON snake_case seulement s'ils existent. -- [x] `brain/app/domain/models.py` : dataclasses `ArcSummary`, `ChapterSummary`, `CampaignStructuralContext`, `NarrativeEntityContext`. -- [x] `brain/app/application/chat.py` : `_BASE_SYSTEM` rendu générique ("contexte ci-dessous"), `stream(…)` en kw-only args, `_build_system_prompt` assemble les sections conditionnelles. Formatters dédiés `_format_campaign`, `_format_arcs`, `_format_chapter_block`, `_format_narrative_entity`. -- [x] `brain/app/main.py` : DTOs `ArcSummaryDTO`, `ChapterSummaryDTO`, `CampaignContextDTO`, `NarrativeEntityDTO` (validation `entity_type` via pattern). `ChatStreamRequestDTO.has_scope()` → HTTP 422 si aucun scope. - -###### b5.7.4 — Controller REST + service Angular ✅ -- [x] `AiChatController.POST /api/ai/chat/stream-campaign` : `ChatStreamCampaignRequestDTO(campaignId, entityType?, entityId?, messages)`. SSE helpers réutilisés. -- [x] `AiChatService.streamChatForCampaign(...)` + type `NarrativeEntityType = 'arc' | 'chapter' | 'scene'`. Helper privé `streamSse(...)` partagé avec `streamChat(...)` (Lore). -- [x] `AiChatDrawerComponent` : nouveaux `@Input()` `campaignId`, `entityType`, `entityId`. Dispatch : `campaignId` truthy → mode Campagne, sinon mode Lore (backward compatible). - -###### b5.7.5 — Intégration UI dans les 3 écrans ✅ -- [x] `arc-edit`, `chapter-edit`, `scene-edit` : bouton `btn-ai` "Assistant IA" dans le header (Sparkles icon + état `active`), `` injecté avec `entityType` + `entityId` appropriés, `quickSuggestions` adaptées au rôle narratif (thèmes/enjeux pour l'arc, objectifs/tensions pour le chapitre, ambiance/narration/choix pour la scène). -- [x] Style global `.btn-ai` extrait en `_buttons.scss` (violet `#a5b4fc`, variante `.active` bordure `#6c63ff`) pour éviter la duplication. -- [x] Validation finale : `mvn clean compile` BUILD SUCCESS + `npx tsc --noEmit` 0 erreur. - -###### b5.7.6 — À faire plus tard -- [ ] Persistance optionnelle de la conversation (entité `Conversation` côté Java, historique reprenable entre sessions). -- [ ] Fiche academy dédiée à la composition de prompts multi-contextes (Lore + Campaign + Entity). - -###### b5.8 — Enrichissement du Structural Context Campagne ✅ (20 avril 2026, après-midi) - -> ✅ **Problème remonté par l'utilisateur** : en éditant une scène, impossible de demander à l'IA "c'est quoi la scène X (qui est ailleurs dans la campagne) ?" — elle ne connaissait QUE les noms. Résolu en ajoutant les descriptions courtes à chaque niveau de l'arbre narratif, sans basculer vers du RAG sémantique. - -- [x] **Domain (core)** : `CampaignStructuralContext.ArcSummary` gagne un champ `description`. `ChapterSummary.sceneNames: List` remplacé par `scenes: List` avec `name + description`. `ChapterSummary` gagne également `description`. -- [x] **Builder (application)** : `CampaignStructuralContextBuilder` peuple maintenant `arc.description`, `chapter.description`, `scene.description` depuis les entités domaine (qui les exposent déjà — on consommait juste les noms). -- [x] **Pont Java ↔ Python** : `BrainAiChatClient` sérialise les nouveaux champs. Côté Python : `models.py` gagne la dataclass `SceneSummary` et les champs description ; `main.py` ajoute `SceneSummaryDTO` + helper `_to_campaign_context` mis à jour. -- [x] **System prompt (chat.py)** : `_format_arcs` et `_format_chapter_block` ajoutent une ligne `Synopsis : …` / `Description : …` sous chaque nœud quand renseigné. Format conditionnel (pas de ligne vide si description absente). -- [x] **Budget tokens** : ~30 tokens par scène × 100 scènes ≈ 3k tokens. Confortable. Si un jour une campagne explose ce budget, on basculera en Option C (RAG sémantique). -- [x] Validation finale : `mvn clean compile` BUILD SUCCESS + `python -m py_compile` sur les 3 fichiers Python + `npx tsc --noEmit` 0 erreur. - -##### Étape b6 — IA dans la création de page (wizard) ✅ (20 avril 2026, nuit) - -> ✅ **Mode wizard livré.** Sur `page-create`, bouton "✨ Créer avec l'IA" à côté du "Créer la page" classique. Au clic, le drawer chat s'ouvre avec un prompt système contextualisé au template (nom + liste exacte des champs + règles de cohérence) qui force l'IA à terminer chaque réponse par un bloc JSON `{...}`. L'utilisateur dialogue jusqu'à être satisfait puis clique "Appliquer et créer la page" → la page est créée en 2 étapes (POST coquille + PUT values) et navigation vers l'édition. - -###### b6.1 — Flux côté `page-create` ✅ -- [x] Bouton "✨ Créer avec l'IA" ajouté entre "Annuler" et "Créer la page". Désactivé tant que titre + template + dossier ne sont pas renseignés (même `canSubmit` que le bouton classique). -- [x] Au clic : le drawer s'ouvre (`chatOpen = true`) avec un `welcomeMessage` contextualisé : *"Super, on va créer une page 'PNJ' ! Décrivez-la-moi en quelques mots…"* (généré dynamiquement à partir du nom du template choisi). -- [x] Création en 2 étapes (POST puis PUT) pour appliquer les `values` — le backend `POST /api/pages` n'accepte pas encore `values` en payload. Choix pragmatique (zéro modification backend) documenté dans le code. -- [x] Erreurs gérées : *"L'assistant n'a pas encore répondu"*, *"Impossible d'extraire les valeurs"*, *"Page créée mais impossible d'appliquer les valeurs"*. Affichées en banner rouge sous le formulaire. - -###### b6.2 — Parsing JSON de la réponse assistant ✅ -- [x] Le system prompt wizard (construit côté Angular dans `page-create.component.ts` getter `wizardSystemPrompt`) demande à l'IA de terminer CHAQUE réponse par un bloc `{...}` avec les clés exactes du template. -- [x] Parsing côté Angular : regex `/\s*([\s\S]*?)\s*<\/values>/i` + `JSON.parse` avec try/catch + coercion des valeurs non-string en string. -- [x] Chaque fin de réponse assistant alimente `lastWizardReply` via le nouveau `@Output() assistantReply` du drawer. - -###### b6.3 — Évolutions du drawer (réutilisable) ✅ -- [x] Nouveau `@Input() systemPromptAddon: string | null` — injecté comme message `role: 'system'` **invisible côté UI** en tête du payload envoyé au backend à chaque tour. Permet au parent de contextualiser la conversation sans polluer l'historique visuel. -- [x] Nouveau `@Output() assistantReply = EventEmitter()` — émis à chaque complétion d'un message assistant. Le parent l'utilise pour extraire le bloc `` du wizard. -- [x] Le composant reste **unique** (pas de composant "wizard" séparé) : la config se fait entièrement via inputs/outputs. SRP respecté (le drawer ne connaît rien du wizard, juste du chat). - -##### Étape b7 — Anti-hallucination ✅ (20 avril 2026, nuit) - -> ✅ **Nuance centrale encodée** : l'IA peut (et doit) inventer des éléments originaux, mais ne peut pas faire référence à des éléments du Lore comme s'ils existaient si on ne les lui a pas montrés. Appliqué via température abaissée + system prompts durcis. - -###### b7.1 — Température configurable par use case ✅ -- [x] `LLMProvider.generate()` et `LLMChatProvider.stream_chat()` enrichis d'un kwarg `temperature: float | None = None` dans `brain/app/domain/ports.py` (docstring explicite la recommandation LoreMind). -- [x] `OllamaLLMProvider` propage via `options.temperature` dans les payloads `/api/generate` et `/api/chat` (la clé `options` est la convention Ollama pour les hyperparamètres). -- [x] `GeneratePageUseCase` : constante `_DEFAULT_TEMPERATURE = 0.4` (remplissage factuel, peu créatif). -- [x] `ChatUseCase` : constante `_DEFAULT_TEMPERATURE = 0.7` (conversation créative mais cohérente). - -###### b7.2 — System prompts durcis ✅ -- [x] `ChatUseCase._BASE_SYSTEM` : section "Règles de cohérence (IMPORTANT)" ajoutée avec la nuance centrale (✅ inventer OK, ❌ référencer l'inexistant KO, ❌ dates/chiffres précis inventés). -- [x] `GeneratePageUseCase._SYSTEM_INSTRUCTIONS` : même section, adaptée au remplissage factuel. Invite explicitement à rester vague (*"il y a longtemps"*, *"un bourg voisin"*) quand une précision externe manque plutôt que d'inventer. - -###### b7.3 — DTO `temperature` optionnel — REPORTÉ au backlog -- [ ] Exposition du paramètre dans les DTOs Pydantic `/generate-page` et `/chat/stream` (override depuis Java/front). **Non fait volontairement** — YAGNI tant que personne ne demande l'override. Les constantes des use cases suffisent. À ajouter quand un slider "créativité" apparaîtra côté UI (backlog b4.6). - -##### Étape b8 — Contextualisation page courante (serveur) ✅ (20 avril 2026, nuit) - -> ✅ **PageContext injecté côté backend**. Sur `page-edit`, le drawer transmet le `pageId` → le Core charge la Page + son Template et construit un `PageContext` (titre, template, champs, valeurs actuelles) → envoyé en `page_context` au Brain Python → injecté dans le system prompt comme bloc "PAGE EN COURS D'ÉDITION" avec instruction de focalisation exclusive. L'IA ne déborde plus sur d'autres pages/templates. - -###### b8.1 — Brain Python ✅ -- [x] Nouveau dataclass `PageContext` dans `domain/models.py` (title, template_name, template_fields, values). -- [x] `ChatUseCase.stream()` accepte un `page_context: PageContext | None = None` optionnel. Rétro-compat totale (sans argument = comportement b5). -- [x] `_build_system_prompt` ajoute un bloc "--- PAGE EN COURS D'ÉDITION ---" quand `page_context` est fourni, listant titre + template + champs + valeurs actuelles + instruction de focalisation exclusive. -- [x] DTO Pydantic `PageContextDTO` + champ optionnel `page_context: PageContextDTO | None = None` sur `ChatStreamRequestDTO`. Mapping DTO → domain dans `main.py`. - -###### b8.2 — Core Java ✅ -- [x] Nouveau value object `PageContext` dans `domain/generationcontext/` (parallèle architectural de `LoreStructuralContext`). Lombok @Value/@Builder. -- [x] `ChatRequest` enrichi d'un champ `pageContext` nullable (JavaDoc explicite le "null = chat générique"). -- [x] `StreamChatForLoreUseCase.execute()` prend désormais un `pageId` nullable. Nouvelle méthode privée `buildPageContext(pageId)` charge Page + Template via les repos existants (zéro nouveau port). Gestion des pages orphelines (template absent → PageContext minimal sans champs, pas d'exception). -- [x] `BrainAiChatClient.toPayload()` sérialise `page_context` en snake_case uniquement si fourni (payload léger par défaut). -- [x] DTO `ChatStreamRequestDTO` gagne un `pageId` optionnel + `AiChatController` le propage au use case. - -###### b8.3 — Angular ✅ -- [x] `AiChatService.streamChat(loreId, messages, pageId?)` : nouveau 3ᵉ argument optionnel, inclus dans le payload JSON uniquement s'il est truthy. -- [x] `AiChatDrawerComponent` gagne un `@Input() pageId: string | null = null` propagé au service. -- [x] `page-edit.component.html` passe `[pageId]="pageId"` au drawer — le composant avait déjà cet ID (depuis la route). -- [x] `page-create` (wizard b6) **volontairement** ne passe pas de `pageId` : la page n'existe pas encore. Le wizard continue de fonctionner via son `systemPromptAddon` sans aucune modification — les deux mécanismes cohabitent proprement. - -###### b7.4 — Fiche academy `anti-hallucination.md` ✅ -- [x] Théorie : pourquoi les LLM hallucinent (prédiction probabiliste sans vérification). -- [x] Analogie JDR : le "MJ qui improvise" — improvisation créative (OK, nouveau PNJ) vs improvisation incohérente (KO, référence à un PNJ inexistant comme session 2). -- [x] Les 5 leviers détaillés (température, prompt strict, contexte riche, modèle plus gros, chain-of-thought) avec tableau coût/effet. -- [x] Application concrète à LoreMind : ce qu'on a retenu (1+2+3), ce qu'on garde en backlog (4+5). -- [x] Section "Le savais-tu ?" sur le fait que `temperature=0` n'est pas 100% déterministe (parallélisme GPU). -- [x] Quiz 5 QCM. - -##### Dette technique Brain (non bloquante, à reprendre plus tard) -- [ ] **Client `httpx` réutilisé** via FastAPI `lifespan` — actuellement un nouveau client est créé à chaque requête dans `OllamaLLMProvider.generate`. Impact : pool de connexions perdu entre requêtes. À corriger quand le débit augmente. -- [ ] **Tests pytest** : créer un `FakeLLMProvider` et tester `GeneratePageUseCase` en isolation. L'hexagonal a été mis en place précisément pour ça — il serait dommage de ne pas en tirer parti. -- [ ] **Logging structuré** (`loguru` ou `logging` standard avec `JsonFormatter`) à la place des prints implicites pour faciliter le debug en conditions réelles. -- [ ] **Endpoint `GET /info`** exposant le modèle actuellement configuré (utile pour diagnostiquer "ce que voit le serveur" sans SSHer dans le container). -- [ ] **Validation Pydantic** plus stricte : `max_length` sur `prompt`, `max_items` sur `template_fields` (ex: 20 max), longueur du `page_title`. -- [ ] **Gestion `output_format` autres que `"json"`** : aujourd'hui on passe la valeur brute à Ollama. Si le Brain doit supporter un adapter qui ne comprend que certains formats, valider côté port. - -## Feature "Illustrations & images" ✅ (20-21 avril 2026, sessions 5 & 6) - -> ✅ **Feature complète livrée en 6 étapes.** Upload d'images via MinIO (S3-compatible), galeries éditables sur Arc/Chapter/Scene, et support d'un nouveau type `IMAGE` dans les champs de Template → les Pages peuvent porter des galeries par champ en plus des textes. Synchro Brain Python pour que l'IA "sache" combien d'illustrations porte chaque entité narrative (sans jamais recevoir les binaires). - -### Étape 1 — Shared Kernel images + MinIO ✅ (2026-04-20 sess.5) -Backend Java pur, testable via `curl`. Aucune intégration métier à ce stade. -- **Infrastructure** : `docker-compose.yml` (service `minio` + `minio-init` auto-création du bucket `loremind-images`) ; `core/pom.xml` + `io.minio:minio:8.5.11` ; `application.properties` (config `minio.*` + multipart 10 Mo). -- **Domaine** : `Image` (VO) + ports `ImageRepository` et `ImageStorage` **séparés** (SRP : la métadonnée DB et le binaire objet-storage sont deux responsabilités distinctes). -- **Application** : `ImageService` (validation MIME `jpeg/png/webp/gif`, taille max 10 Mo). -- **Adapters** : `MinioConfig` + `MinioImageStorageAdapter` (binaire) ; `ImageJpaEntity` + `PostgresImageRepository` (métadonnée). -- **REST** : `ImageController` → `POST /api/images` (multipart), `GET /api/images/{id}`, `GET /api/images/{id}/content` (proxy binaire), `DELETE /api/images/{id}`. -- **Academy** : `docs/academy/object-storage.md` + `docs/academy/shared-kernel.md`. -- Validation : `mvn compile` OK. - -### Étape 2 — Composants Angular partagés ✅ (2026-04-20 sess.5) -Deux composants autonomes, réutilisables partout où une galerie d'images est nécessaire. -- `web/src/app/services/image.service.ts` : upload, getById, delete, contentUrl. -- `app-image-uploader` (`shared/image-uploader/`) : drop-zone standard OU mode compact (bouton `+ ajouter` pour galerie). Validation client alignée serveur. Gestion 413. -- `app-image-gallery` (`shared/image-gallery/`) : grille 120×120 lazy-loading, mode `editable` avec uploader compact intégré + bouton X par vignette (supprime serveur + émet nouvelle liste), **lightbox plein écran** au clic. -- Validation : `npx tsc --noEmit` OK. - -### Étape 3 — Illustrations sur Scene / Chapter / Arc ✅ (2026-04-20 sess.5) -Première intégration métier : les 3 entités narratives portent une liste d'images. -- **Backend** : `List illustrationImageIds` ajouté sur `Arc`/`Chapter`/`Scene` (domaine + JPA avec converter JSON + DTO + Mapper + Postgres repo + Service). -- **Frontend** : champ dans `campaign.model.ts` ; section "Illustrations" en tête des `*-view` (lecture) et `*-edit` (galerie éditable) pour les 3 entités. -- Validation : `mvn compile` + `npx tsc --noEmit` OK. - -### Étape 4 — Refactor `Template.fields` ✅ (2026-04-21 sess.6) -**Pivot structurel** : un champ de template n'est plus juste un nom — il a un **type**. Prépare l'étape 5 (Pages avec champs IMAGE). -- **Backend** : nouveau enum `FieldType { TEXT, IMAGE }` + VO `TemplateField(name, type)`. `Template.fields` devient `List` + helper `textFieldNames()` (utilisé par les use cases IA qui ne savent traiter que du texte). -- **Migration BDD transparente** : `TemplateFieldListJsonConverter` lit l'ancien format `["name", ...]` ET le nouveau `[{name, type}]`, écrit toujours au nouveau format → auto-migration à la première sauvegarde (pas de script SQL). -- **Tolérance** : `TemplateFieldMapper` traite un type inconnu → `TEXT` (robuste face à une régression DTO). -- **Use cases IA mis à jour** : `GeneratePageValuesUseCase` et `StreamChatForLoreUseCase` ne passent à l'IA que les champs TEXT (erreur claire si aucun). -- **Frontend** : sélecteur de type dans `template-create/edit`, chip verte (TEXT) vs indigo (IMAGE), bouton toggle inline. `page-view` rend un placeholder pour les champs IMAGE (la vraie UI vient en étape 5). `page-edit` hydrate les TEXT uniquement, `page-create` wizard ne liste que les TEXT. -- Validation : `mvn compile` + `npx tsc --noEmit` OK. - -### Étape 5 — Support champs IMAGE dans Pages ✅ (2026-04-21 sess.6) -Les Pages gagnent une seconde zone de stockage, parallèle à `values` (TEXT). -- **Backend** : nouveau converter `StringListMapJsonConverter` (`Map>` ↔ JSON). `Page.imageValues` ajouté avec helpers `setImageFieldValue` / `getImageFieldValue`. Nouvelle colonne `image_values_json` sur `PageJpaEntity`. Propagation dans DTO + Mapper + Service. -- **Frontend** : `Page.imageValues?: Record` ; `page-view` affiche une galerie readonly par champ IMAGE via `ImageGalleryComponent` ; `page-edit` hydrate séparément TEXT et IMAGE et rend une galerie éditable par champ IMAGE. -- Validation : `mvn compile` + `npx tsc --noEmit` OK. - -### Étape 6 — Brain Python : synchro DTOs ✅ (2026-04-21 sess.6) -L'IA ne reçoit **pas** les binaires — juste un signal de présence (`illustration_count`) pour qu'elle puisse en tenir compte dans le prompt. -- **Backend Java** : `illustration_count` ajouté sur `ArcSummary` / `ChapterSummary` / `SceneSummary` du `CampaignStructuralContext`. Le builder peuple depuis `getIllustrationImageIds()` (null-safe). `BrainAiChatClient` sérialise **uniquement si > 0** (payload léger pour une campagne sans images). -- **Brain Python** : `illustration_count: int = 0` sur les 3 summaries dans `domain/models.py` ; DTOs Pydantic + `_to_campaign_context` mis à jour dans `main.py`. Les champs inconnus (ex: `illustrationImageIds` des Pages) sont silencieusement ignorés par Pydantic v2 (pas d'erreur). -- **Prompt** : helper `_illustration_hint()` dans `chat.py` ; les lignes arc/chapter/scene du prompt affichent ` [N illustrations]` si présentes (ex: `- A (arc) [2 illustrations]`). -- Validation : `mvn compile` + `python -m py_compile` OK + démo runtime prompt validée. - -### Résidu de scope (non bloquant) -- [ ] `PageSummary` (côté LoreStructuralContext) ne porte pas encore de signal sur les `imageValues` des Pages. Symétrique à faire si l'IA doit raisonner sur "cette page PNJ a 3 portraits". - -### Notes transverses -- **Docker-compose** ne couvre aujourd'hui QUE MinIO. Postgres/Brain/Web restent lancés à la main. -- **Séparation `ImageRepository` / `ImageStorage`** volontaire (SRP, pattern Shared Kernel). -- URL publique d'une image : `/api/images/{id}/content` (proxy Java — évite d'exposer MinIO directement). -- Validation MIME côté `ImageService` : `jpeg/png/webp/gif` uniquement, max 10 Mo. - -## Feature "Branches narratives" ✅ (21 avril 2026, session 7) - -> ✅ **Graphe narratif intra-chapitre livré en 6 étapes.** Une scène peut maintenant pointer vers plusieurs autres scènes du même chapitre selon l'action des joueurs (logique "livre dont vous êtes le héros"). Vue graphique SVG custom (organigramme) accessible depuis `chapter-view`. - -### Cadrage produit -- **Scope** : branches intra-chapitre uniquement (cross-chapitre = scène "finale" qui mène au chapitre suivant, logique existante). -- **Champ `order` conservé (option A)** : la scène `order=1` devient le point d'entrée du graphe. Possibilité future de migrer vers Option B (`isEntryPoint: boolean`) sans effort majeur. -- **Formulaire avant graphe** : édition structurée des branches dans `scene-edit`, visualisation ensuite. - -### Étape 1 — Domain (DDD) ✅ -- **Nouveau** : `SceneBranch` (Value Object, `@Value @Builder @Jacksonized`) — 3 champs : `label`, `targetSceneId`, `condition`. -- **Scene.java** : nouveau champ `List branches` + méthodes métier `addBranch()` (garde anti-auto-référence) / `removeBranchTo()`. - -### Étape 2 — Persistance ✅ -- **Nouveau** : `SceneBranchListJsonConverter` (pattern homogène avec `StringListJsonConverter`, TEXT + JSON via Jackson). -- **SceneJpaEntity** : colonne `branches TEXT` avec `@Convert`. Colonne auto-créée par Hibernate `ddl-auto=update`. -- **PostgresSceneRepository** : mapping `branches` dans les deux sens avec copies défensives. - -### Étape 3 — API ✅ -- **Nouveau** : `SceneBranchDTO` (mutable, pour wire Jackson). -- **SceneDTO** : champ `branches: List`. -- **SceneMapper** : helpers `toBranchDTOs()` / `toBranchDomain()` branchés sur `toDTO()` / `toDomain()`. -- **Controller inchangé** : `PUT /api/scenes/{id}` accepte déjà les branches via `SceneDTO`. - -### Étape 4 — Service (validation métier) ✅ -- **SceneService.updateScene()** propage `branches` + appelle `validateBranches()`. -- Invariants vérifiés : targetSceneId non vide, pas d'auto-référence, appartenance au même chapitre. Chargement une seule fois des IDs du chapitre (évite N+1). - -### Étape 5 — Frontend (édition) ✅ -- **campaign.model.ts** : interface `SceneBranch` + champs `branches?` sur `Scene` / `SceneCreate`. -- **scene-edit** : nouvelle section expandable "🌿 Branches narratives". Chaque branche = carte avec libellé + `` n'était pas appliqué car les `