Ajout de 2 fonctionnalitées principales : import PDF que ce soit pour les règles ou les campagnes directement.
Some checks failed
Build & Push Images / build (web) (push) Has been cancelled
Build & Push Images / build-switcher (push) Has been cancelled
Build & Push Images / build (core) (push) Has been cancelled
Build & Push Images / build (brain) (push) Has been cancelled

Fonctionnalité de comparaison PDF / campagne pour faire un mix et demander des conseils à l'IA
This commit is contained in:
2026-06-04 13:55:27 +02:00
parent 091de0daf7
commit 6d00543a59
78 changed files with 5250 additions and 183 deletions

View File

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { LucideAngularModule, Save, ArrowLeft, Dices, Plus, Trash2, ChevronDown, ChevronRight } from 'lucide-angular';
import { LucideAngularModule, Save, ArrowLeft, Dices, Plus, Trash2, ChevronDown, ChevronRight, Upload } from 'lucide-angular';
import { GameSystemService } from '../../services/game-system.service';
import { TemplateField } from '../../services/template.model';
import { TemplateFieldsEditorComponent } from '../../shared/template-fields-editor/template-fields-editor.component';
@@ -54,9 +54,23 @@ export class GameSystemEditComponent implements OnInit {
readonly Trash2 = Trash2;
readonly ChevronDown = ChevronDown;
readonly ChevronRight = ChevronRight;
readonly Upload = Upload;
id: string | null = null;
/** Import PDF en cours (appel LLM long) → désactive le bouton + spinner. */
importing = false;
/** Message de succès post-import (sections ajoutées, pages, OCR). */
importNote: string | null = null;
/** Message d'erreur d'import (Brain injoignable, PDF illisible…). */
importError: string | null = null;
/** Libellé de l'étape courante (« Extraction… », « Analyse… (3/12) »). */
importPhase = '';
/** Avancement de la structuration ; null pendant l'extraction (total inconnu). */
importProgress: { current: number; total: number } | null = null;
/** Titres de sections trouvés au fil de l'eau (affichage live). */
importFound: string[] = [];
name = '';
description = '';
author = '';
@@ -108,6 +122,96 @@ export class GameSystemEditComponent implements OnInit {
this.sections.push({ title: '', content: '', collapsed: false });
}
// --- Import d'un PDF de règles -------------------------------------------
/** Déclenché par le <input file> caché : lance l'import du PDF choisi. */
onPdfSelected(event: Event): void {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
// Reset de la valeur : re-sélectionner le même fichier doit re-déclencher.
input.value = '';
if (file) this.importPdf(file);
}
private importPdf(file: File): void {
this.importing = true;
this.importNote = null;
this.importError = null;
this.importPhase = 'Extraction du texte…';
this.importProgress = null;
this.importFound = [];
this.service.importRulesStream(file).subscribe({
next: (ev) => {
if (ev.type === 'progress') {
if (ev.total === 0) {
// Phase d'extraction (total encore inconnu).
this.importPhase = 'Extraction du texte…';
this.importProgress = null;
} else {
this.importPhase = `Analyse des règles… (${ev.current}/${ev.total})`;
this.importProgress = { current: ev.current, total: ev.total };
for (const t of ev.newSectionTitles) {
if (!this.importFound.includes(t)) this.importFound.push(t);
}
}
} else if (ev.type === 'done') {
this.finishImport(ev.sections, ev.pageCount, ev.ocrPageCount);
}
},
error: (err: Error) => {
this.resetImportProgress();
this.importError = err?.message
? `Échec de l'import : ${err.message}`
: "Échec de l'import du PDF.";
}
});
}
private finishImport(sections: Record<string, string>, pageCount: number, ocrPageCount: number): void {
this.resetImportProgress();
const added = this.mergeImportedSections(sections);
if (added === 0) {
this.importError = "Aucune règle exploitable n'a été extraite de ce PDF "
+ "(scan sans OCR, ou contenu non reconnu).";
return;
}
const ocr = ocrPageCount > 0 ? ` (dont ${ocrPageCount} page(s) via OCR)` : '';
this.importNote = `${added} section(s) proposée(s) depuis ${pageCount} page(s)${ocr}. `
+ `Relisez et ajustez ci-dessous avant d'enregistrer.`;
}
private resetImportProgress(): void {
this.importing = false;
this.importPhase = '';
this.importProgress = null;
}
/**
* Fusionne les sections proposées dans l'éditeur. Section de même titre
* (insensible à la casse) → contenu ajouté à la suite ; sinon nouvelle carte.
* Retourne le nombre de sections effectivement intégrées.
*/
private mergeImportedSections(sections: Record<string, string>): number {
let count = 0;
for (const [rawTitle, rawContent] of Object.entries(sections ?? {})) {
const title = (rawTitle ?? '').trim();
const content = (rawContent ?? '').trim();
if (!title || !content) continue;
const existing = this.sections.find(
s => s.title.trim().toLowerCase() === title.toLowerCase()
);
if (existing) {
existing.content = `${existing.content.trim()}\n\n${content}`.trim();
existing.collapsed = false;
} else {
this.sections.push({ title, content, collapsed: false });
}
count++;
}
return count;
}
removeSection(index: number): void {
this.sections.splice(index, 1);
}