Co-MJ v1 : quêtes de première classe, guidage de préparation, assistant IA,
All checks were successful
All checks were successful
mode séance, battlemaps multiples et export Foundry ciblé
- Quêtes (Niveau 1) : entité Quest orthogonale à l'arbre, rattachée à un arc HUB
ou libre (migrations V9-V12, V18, V20 ; réconciliation des jumeaux V10).
Fusion quête/conteneur dans la sidebar, progression par partie (statuts
disponible/en cours/terminée), et quêtes libres avec espace de scènes créé
automatiquement (arc technique « Quêtes libres », V21 : levée de la
contrainte arcs.type héritée du baseline).
- Guidage (Pilier B) : bilan de préparation 100 % dérivé (règles arc/chapitre/
scène/quête), panneau « Prochaines étapes » avec boutons « Corriger », et
pastilles détaillées (tooltip des manques) dans l'arbre.
- Assistant IA (Pilier A) : étoffage champ par champ (scène, chapitre, arc) et
brouillons de scènes en propose→applique (brain : narrative-fields,
scene-drafts).
- Horloges & menaces (V15-V17) : clocks à segments avec déclencheurs, fronts.
- Mode séance : préparation de séance (readiness + quêtes dispo), scène
épinglée (V19), récap « précédemment » (brain : session-recap), onglet
« Partie » du panneau de référence, graphe amélioré (pan/zoom, éditeur de
liens).
- Perf : endpoint agrégé GET /api/campaigns/{id}/tree — la sidebar charge en
1 requête au lieu de ~15.
- Battlemaps multiples par scène (variantes jour/nuit, étages…) : liste JSON
étiquetée (V22, reprise automatique de la carte existante), rendu PDF avec
légendes, export/import rétro-compatible.
- Export Foundry ciblé : modale de périmètre (cartes+ennemis / journaux /
tables), bundle filtré côté serveur (zip allégé), module Foundry à jour
(respect du périmètre + une Scene Foundry par variante de carte,
rétro-compatible anciens bundles).
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { LucideAngularModule, Sparkles, RefreshCw, Check, X, ChevronDown, ChevronRight } from 'lucide-angular';
|
||||
import { EntityAssistService } from '../../services/entity-assist.service';
|
||||
import { FieldProposal } from '../../services/entity-assist.model';
|
||||
|
||||
interface ReviewField extends FieldProposal {
|
||||
accepted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Panneau « Étoffer avec l'IA » (Pilier A — co-création), générique par type d'entité
|
||||
* narrative ({@code arc|chapter|scene}). Demande au co-MJ des propositions de valeurs pour
|
||||
* les champs de l'entité, les affiche en diff (actuel → proposé) avec accept/reject champ
|
||||
* par champ, et ÉMET les champs retenus.
|
||||
*
|
||||
* <p>Ne persiste rien : le parent (arc/chapter/scene-edit) applique les champs retenus au
|
||||
* FORMULAIRE ; l'utilisateur enregistre ensuite normalement (human-in-the-loop).</p>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-entity-assist-panel',
|
||||
standalone: true,
|
||||
imports: [FormsModule, TranslatePipe, LucideAngularModule],
|
||||
templateUrl: './entity-assist-panel.component.html',
|
||||
styleUrls: ['./entity-assist-panel.component.scss']
|
||||
})
|
||||
export class EntityAssistPanelComponent implements OnChanges {
|
||||
|
||||
/** Type d'entité : 'arc' | 'chapter' | 'scene'. */
|
||||
@Input() entityType = 'scene';
|
||||
@Input() entityId = '';
|
||||
@Input() campaignId = '';
|
||||
/** Émet les champs ACCEPTÉS ; le parent les applique où il veut (formulaire, /apply…). */
|
||||
@Output() applied = new EventEmitter<FieldProposal[]>();
|
||||
|
||||
open = false;
|
||||
instruction = '';
|
||||
loading = false;
|
||||
generated = false;
|
||||
error = '';
|
||||
fields: ReviewField[] = [];
|
||||
|
||||
readonly Sparkles = Sparkles;
|
||||
readonly RefreshCw = RefreshCw;
|
||||
readonly Check = Check;
|
||||
readonly X = X;
|
||||
readonly ChevronDown = ChevronDown;
|
||||
readonly ChevronRight = ChevronRight;
|
||||
|
||||
constructor(private assist: EntityAssistService, private translate: TranslateService) {}
|
||||
|
||||
/**
|
||||
* L'écran d'édition RÉUTILISE ce composant quand on navigue entre entités sœurs
|
||||
* (même route pattern → pas de recréation). On repart donc d'un état vierge dès que
|
||||
* l'entité ciblée change, sinon on afficherait les propositions de l'entité précédente.
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['entityId'] && !changes['entityId'].firstChange) {
|
||||
this.open = false;
|
||||
this.instruction = '';
|
||||
this.loading = false;
|
||||
this.generated = false;
|
||||
this.error = '';
|
||||
this.fields = [];
|
||||
}
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
this.open = !this.open;
|
||||
}
|
||||
|
||||
generate(): void {
|
||||
if (!this.entityId || this.loading) return;
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
this.generated = false;
|
||||
this.fields = [];
|
||||
this.assist.generateFields(this.entityType, this.entityId, this.campaignId, this.instruction.trim() || undefined).subscribe({
|
||||
next: proposal => {
|
||||
this.fields = (proposal.fields ?? []).map(f => ({ ...f, accepted: true }));
|
||||
this.generated = true;
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.error = this.translate.instant('entityAssist.error');
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get acceptedCount(): number {
|
||||
return this.fields.filter(f => f.accepted).length;
|
||||
}
|
||||
|
||||
apply(): void {
|
||||
const accepted: FieldProposal[] = this.fields
|
||||
.filter(f => f.accepted)
|
||||
.map(f => ({ key: f.key, currentValue: f.currentValue, proposedValue: f.proposedValue }));
|
||||
if (accepted.length > 0) {
|
||||
this.applied.emit(accepted);
|
||||
}
|
||||
this.fields = [];
|
||||
this.generated = false;
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
/** Libellé lisible d'une clé de champ (fallback sur la clé brute si non traduite). */
|
||||
fieldLabel(key: string): string {
|
||||
const label = this.translate.instant('entityAssist.field.' + key);
|
||||
return label === 'entityAssist.field.' + key ? key : label;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user