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. * *

Ne persiste rien : le parent (arc/chapter/scene-edit) applique les champs retenus au * FORMULAIRE ; l'utilisateur enregistre ensuite normalement (human-in-the-loop).

*/ @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(); 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; } }