Ajout de 2 fonctionnalitées principales : import PDF que ce soit pour les règles ou les campagnes directement.
Fonctionnalité de comparaison PDF / campagne pour faire un mix et demander des conseils à l'IA
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { GameSystem, GameSystemCreate } from './game-system.model';
|
||||
import { GameSystem, GameSystemCreate, RulesImportResponse, RulesImportStreamEvent } from './game-system.model';
|
||||
|
||||
/**
|
||||
* Service HTTP pour les GameSystems (systèmes de JDR).
|
||||
@@ -36,4 +36,109 @@ export class GameSystemService {
|
||||
const params = new HttpParams().set('q', q);
|
||||
return this.http.get<GameSystem[]>(`${this.apiUrl}/search`, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* Importe un PDF de règles : renvoie une PROPOSITION de sections (titre →
|
||||
* markdown). Rien n'est persisté côté serveur — l'appelant injecte les
|
||||
* sections dans l'éditeur pour révision avant enregistrement.
|
||||
*/
|
||||
importRules(file: File): Observable<RulesImportResponse> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
return this.http.post<RulesImportResponse>(`${this.apiUrl}/import-rules`, form);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variante streamée : émet l'avancement au fil de l'eau puis `done`.
|
||||
* On utilise fetch() (pas EventSource : POST + multipart impossibles avec
|
||||
* EventSource) et on décode le flux SSE ligne par ligne. Annuler la
|
||||
* subscription annule le fetch (AbortController).
|
||||
*/
|
||||
importRulesStream(file: File): Observable<RulesImportStreamEvent> {
|
||||
return new Observable<RulesImportStreamEvent>((subscriber) => {
|
||||
const controller = new AbortController();
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
|
||||
fetch(`${this.apiUrl}/import-rules/stream`, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'text/event-stream' },
|
||||
body: form,
|
||||
signal: controller.signal
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok || !response.body) {
|
||||
subscriber.error(new Error(`HTTP ${response.status}`));
|
||||
return;
|
||||
}
|
||||
await this.consumeImportSse(response.body, subscriber);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (controller.signal.aborted) return;
|
||||
subscriber.error(err);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
});
|
||||
}
|
||||
|
||||
/** Décode un ReadableStream SSE et émet les évènements d'import typés. */
|
||||
private async consumeImportSse(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
subscriber: { next: (e: RulesImportStreamEvent) => void; error: (e: unknown) => void; complete: () => void }
|
||||
): Promise<void> {
|
||||
const reader = body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let currentEvent: string | null = null;
|
||||
let currentData = '';
|
||||
|
||||
const dispatch = () => {
|
||||
const name = currentEvent ?? 'message';
|
||||
if (name === 'error') {
|
||||
let message = 'Échec de l\'import.';
|
||||
try { message = (JSON.parse(currentData) as { message?: string }).message ?? message; } catch { /* garde le défaut */ }
|
||||
subscriber.error(new Error(message));
|
||||
} else if (name === 'progress' || name === 'done') {
|
||||
try {
|
||||
const obj = JSON.parse(currentData);
|
||||
if (name === 'done') {
|
||||
subscriber.next({ type: 'done', ...obj });
|
||||
subscriber.complete();
|
||||
} else {
|
||||
subscriber.next({ type: 'progress', ...obj });
|
||||
}
|
||||
} catch { /* bloc malformé ignoré */ }
|
||||
}
|
||||
currentEvent = null;
|
||||
currentData = '';
|
||||
};
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let idx: number;
|
||||
while ((idx = buffer.indexOf('\n')) >= 0) {
|
||||
const line = buffer.slice(0, idx).replace(/\r$/, '');
|
||||
buffer = buffer.slice(idx + 1);
|
||||
if (line === '') {
|
||||
if (currentEvent !== null || currentData !== '') dispatch();
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.slice(6).trim();
|
||||
} else if (line.startsWith('data:')) {
|
||||
const chunk = line.slice(5).replace(/^ /, '');
|
||||
currentData = currentData ? `${currentData}\n${chunk}` : chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentEvent !== null || currentData !== '') dispatch();
|
||||
subscriber.complete();
|
||||
} catch (err) {
|
||||
subscriber.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user