Amélioration de l'IA pour la partie atelier PDF

Mise en place d'un outil permettant de faire des tableau d'objets pour des boutiques par exemple
This commit is contained in:
2026-06-08 17:00:22 +02:00
parent 70ec1f2fb9
commit 833280c784
38 changed files with 1712 additions and 12 deletions

View File

@@ -180,7 +180,8 @@
[sessionId]="session.id"
[canAddToJournal]="session.active"
(rolled)="onDiceRolled($event)"
(aiReplyToJournal)="onAiReplyToJournal($event)">
(aiReplyToJournal)="onAiReplyToJournal($event)"
(noteToJournal)="onItemNoteToJournal($event)">
</app-session-reference-panel>
</aside>

View File

@@ -265,6 +265,22 @@ export class SessionDetailComponent implements OnInit, OnDestroy {
});
}
/**
* Réception d'un objet de catalogue à consigner dans le journal.
* Le panneau fournit déjà une chaîne formatée (🛒 …) : on la sauvegarde
* telle quelle comme entrée NOTE.
*/
onItemNoteToJournal(content: string): void {
if (!this.session || !this.session.active) return;
const trimmed = content.trim();
if (!trimmed) return;
const input: SessionEntryInput = { type: 'NOTE', content: trimmed };
this.entryService.createEntry(this.session.id, input).subscribe({
next: created => this.entries = [created, ...this.entries],
error: () => console.error('Erreur lors de l\'ajout de l\'objet au journal')
});
}
deleteEntry(entry: SessionEntry): void {
if (!this.session) return;
const session = this.session;

View File

@@ -0,0 +1,38 @@
<div class="sic-panel">
<p class="loading-hint" *ngIf="loading">Chargement…</p>
<!-- Liste des catalogues -->
<div *ngIf="!loading && !selected" class="sic-list">
<p class="empty-hint" *ngIf="catalogs.length === 0">
Aucun catalogue d'objets dans cette campagne.
</p>
<button *ngFor="let c of catalogs" type="button" class="sic-item" (click)="select(c)">
<lucide-icon [img]="Package" [size]="13"></lucide-icon>
<span class="sic-item-name">{{ c.name }}</span>
<span class="sic-item-count">{{ c.items.length }}</span>
</button>
</div>
<!-- Catalogue sélectionné -->
<div *ngIf="selected" class="sic-detail">
<button type="button" class="sic-back" (click)="backToList()">
<lucide-icon [img]="ChevronLeft" [size]="13"></lucide-icon> Catalogues
</button>
<h4>{{ selected.name }}</h4>
<div class="sic-group" *ngFor="let g of groups">
<div class="sic-group-title" *ngIf="g.category !== '—'">{{ g.category }}</div>
<div class="sic-obj" *ngFor="let it of g.items">
<div class="sic-obj-main">
<span class="sic-obj-name">{{ it.name }}</span>
<span class="sic-obj-price" *ngIf="it.price">{{ it.price }}</span>
</div>
<div class="sic-obj-desc" *ngIf="it.description">{{ it.description }}</div>
<button type="button" class="sic-note" (click)="note(it)" [disabled]="!canAddToJournal"
title="Consigner au journal (ex. achat)">
<lucide-icon [img]="BookmarkPlus" [size]="12"></lucide-icon> Journal
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,48 @@
.sic-panel { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.25rem; }
.loading-hint, .empty-hint {
color: var(--color-text-muted, #9aa0aa); font-size: 0.85rem; font-style: italic; padding: 0.5rem 0;
}
.sic-list { display: flex; flex-direction: column; gap: 0.3rem; }
.sic-item {
display: flex; align-items: center; gap: 0.45rem; width: 100%;
padding: 0.45rem 0.6rem; border-radius: 7px;
border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.03);
color: inherit; cursor: pointer; text-align: left;
&:hover { background: rgba(255,255,255,0.08); }
.sic-item-name { flex: 1; }
.sic-item-count {
font-size: 0.72rem; color: var(--color-text-muted, #9aa0aa);
background: rgba(255,255,255,0.06); padding: 0.05rem 0.35rem; border-radius: 4px;
}
}
.sic-detail { display: flex; flex-direction: column; gap: 0.5rem; }
.sic-back {
display: inline-flex; align-items: center; gap: 0.25rem; align-self: flex-start;
padding: 0.2rem 0.4rem; border: none; background: none;
color: var(--color-text-muted, #9aa0aa); cursor: pointer; font-size: 0.8rem;
&:hover { color: inherit; }
}
h4 { margin: 0; font-size: 0.95rem; }
.sic-group-title {
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
color: #c4a8ff; margin: 0.5rem 0 0.2rem;
}
.sic-obj {
padding: 0.4rem 0.5rem; border-radius: 7px; background: rgba(255,255,255,0.03); margin-bottom: 0.3rem;
.sic-obj-main { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.sic-obj-name { font-weight: 500; }
.sic-obj-price { color: #e0c074; font-size: 0.82rem; white-space: nowrap; }
.sic-obj-desc { font-size: 0.82rem; color: var(--color-text-muted, #cfd3da); margin-top: 0.15rem; }
.sic-note {
display: inline-flex; align-items: center; gap: 0.25rem; margin-top: 0.35rem;
padding: 0.2rem 0.45rem; border-radius: 5px; font-size: 0.74rem;
border: 1px solid rgba(255,255,255,0.14); background: rgba(255,255,255,0.05); color: inherit; cursor: pointer;
&:hover:not(:disabled) { background: rgba(255,255,255,0.1); }
&:disabled { opacity: 0.5; cursor: default; }
}
}

View File

@@ -0,0 +1,64 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LucideAngularModule, Package, BookmarkPlus, ChevronLeft } from 'lucide-angular';
import { catchError, of } from 'rxjs';
import { ItemCatalogService } from '../../services/item-catalog.service';
import { ItemCatalog, CatalogItem } from '../../services/item-catalog.model';
interface ItemGroup { category: string; items: CatalogItem[]; }
/**
* Panneau « Objets » du mode jeu : consulter les catalogues (boutiques) de la
* campagne et consigner un objet au journal (ex. « le joueur achète X »).
*/
@Component({
selector: 'app-session-item-catalogs-panel',
standalone: true,
imports: [CommonModule, LucideAngularModule],
templateUrl: './session-item-catalogs-panel.component.html',
styleUrls: ['./session-item-catalogs-panel.component.scss']
})
export class SessionItemCatalogsPanelComponent implements OnInit {
readonly Package = Package;
readonly BookmarkPlus = BookmarkPlus;
readonly ChevronLeft = ChevronLeft;
@Input() campaignId!: string;
@Input() canAddToJournal = true;
/** Émis pour consigner un objet au journal (entrée NOTE). */
@Output() noteToJournal = new EventEmitter<string>();
catalogs: ItemCatalog[] = [];
loading = false;
selected: ItemCatalog | null = null;
constructor(private service: ItemCatalogService) {}
ngOnInit(): void {
if (!this.campaignId) return;
this.loading = true;
this.service.getByCampaign(this.campaignId).pipe(catchError(() => of([] as ItemCatalog[])))
.subscribe(list => { this.catalogs = list; this.loading = false; });
}
select(c: ItemCatalog): void { this.selected = c; }
backToList(): void { this.selected = null; }
get groups(): ItemGroup[] {
const map = new Map<string, CatalogItem[]>();
for (const it of this.selected?.items ?? []) {
const cat = (it.category ?? '').trim() || '—';
if (!map.has(cat)) map.set(cat, []);
map.get(cat)!.push(it);
}
return [...map.entries()]
.sort(([a], [b]) => (a === '—' ? 1 : b === '—' ? -1 : a.localeCompare(b, 'fr')))
.map(([category, items]) => ({ category, items }));
}
note(it: CatalogItem): void {
if (!this.canAddToJournal || !this.selected) return;
const price = it.price ? ` (${it.price})` : '';
this.noteToJournal.emit(`🛒 ${this.selected.name}${it.name}${price}`);
}
}

View File

@@ -22,6 +22,13 @@
<lucide-icon [img]="Table2" [size]="14"></lucide-icon>
Tables
</button>
<button type="button"
class="ref-tab"
[class.ref-tab--active]="activeTab === 'objects'"
(click)="selectTab('objects')">
<lucide-icon [img]="Package" [size]="14"></lucide-icon>
Objets
</button>
<button type="button"
class="ref-tab"
[class.ref-tab--active]="activeTab === 'characters'"
@@ -64,6 +71,14 @@
(aiReplyToJournal)="onAiSaveToJournal($event)">
</app-session-random-tables-panel>
<!-- ====== Catalogues d'objets ====== -->
<app-session-item-catalogs-panel
*ngIf="activeTab === 'objects'"
[campaignId]="campaignId"
[canAddToJournal]="canAddToJournal"
(noteToJournal)="onItemNote($event)">
</app-session-item-catalogs-panel>
<!-- ====== Personnages (PJ + PNJ) ====== -->
<div *ngIf="activeTab === 'characters'" class="ref-list">
<p class="loading-hint" *ngIf="loadingChars">Chargement…</p>

View File

@@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LucideAngularModule, User, Drama, Swords, Dices, ExternalLink, Sparkles, Table2 } from 'lucide-angular';
import { LucideAngularModule, User, Drama, Swords, Dices, ExternalLink, Sparkles, Table2, Package } from 'lucide-angular';
import { catchError, of } from 'rxjs';
import { CampaignService } from '../../services/campaign.service';
import { CharacterService } from '../../services/character.service';
@@ -14,8 +14,9 @@ import {
} from '../session-dice-panel/session-dice-panel.component';
import { SessionAiChatPanelComponent } from '../session-ai-chat-panel/session-ai-chat-panel.component';
import { SessionRandomTablesPanelComponent } from '../session-random-tables-panel/session-random-tables-panel.component';
import { SessionItemCatalogsPanelComponent } from '../session-item-catalogs-panel/session-item-catalogs-panel.component';
type TabId = 'dice' | 'tables' | 'characters' | 'scenes' | 'ai';
type TabId = 'dice' | 'tables' | 'objects' | 'characters' | 'scenes' | 'ai';
/**
* Panneau latéral du mode jeu : référence rapide en lecture seule.
@@ -30,7 +31,7 @@ type TabId = 'dice' | 'tables' | 'characters' | 'scenes' | 'ai';
@Component({
selector: 'app-session-reference-panel',
standalone: true,
imports: [CommonModule, LucideAngularModule, SessionDicePanelComponent, SessionAiChatPanelComponent, SessionRandomTablesPanelComponent],
imports: [CommonModule, LucideAngularModule, SessionDicePanelComponent, SessionAiChatPanelComponent, SessionRandomTablesPanelComponent, SessionItemCatalogsPanelComponent],
templateUrl: './session-reference-panel.component.html',
styleUrls: ['./session-reference-panel.component.scss']
})
@@ -42,6 +43,7 @@ export class SessionReferencePanelComponent implements OnChanges {
readonly ExternalLink = ExternalLink;
readonly Sparkles = Sparkles;
readonly Table2 = Table2;
readonly Package = Package;
@Input() campaignId!: string;
/** Partie active — nécessaire pour charger les PJ (refonte Playthrough). */
@@ -51,6 +53,8 @@ export class SessionReferencePanelComponent implements OnChanges {
@Output() rolled = new EventEmitter<DiceRollResult>();
/** Émis quand l'IA répond et que le MJ veut sauvegarder la réponse comme entrée. */
@Output() aiReplyToJournal = new EventEmitter<string>();
/** Émis pour consigner un objet de catalogue au journal (entrée NOTE). */
@Output() noteToJournal = new EventEmitter<string>();
activeTab: TabId = 'dice';
@@ -143,4 +147,8 @@ export class SessionReferencePanelComponent implements OnChanges {
onAiSaveToJournal(content: string): void {
this.aiReplyToJournal.emit(content);
}
onItemNote(content: string): void {
this.noteToJournal.emit(content);
}
}