Prise en compte du langage de l'utilisateur pour le prompt de réponse. Si par exemple l'interface est en anglais, les IA vont favoriser l'anglais pour la réponse

This commit is contained in:
2026-06-15 09:49:05 +02:00
parent 914767f793
commit 84025911f8
29 changed files with 319 additions and 63 deletions

View File

@@ -0,0 +1,18 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { LanguageService } from '../services/language.service';
/**
* Ajoute l'entête `X-User-Language` (langue choisie dans l'UI : `fr`/`en`) à
* toutes les requêtes HttpClient. Le Core la relaie au Brain, qui rédige alors
* ses réponses IA dans cette langue.
*
* NB : les appels SSE en `fetch()` (chat, imports, notebooks) ne passent PAS par
* les intercepteurs Angular — ils ajoutent l'entête manuellement de leur côté.
*/
export const languageInterceptor: HttpInterceptorFn = (req, next) => {
const language = inject(LanguageService);
return next(
req.clone({ setHeaders: { 'X-User-Language': language.current } })
);
};

View File

@@ -1,6 +1,7 @@
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { LanguageService } from './language.service';
/**
* Un message d'une conversation IA (vue front).
@@ -47,6 +48,7 @@ export type NarrativeEntityType = 'arc' | 'chapter' | 'scene' | 'character' | 'n
@Injectable({ providedIn: 'root' })
export class AiChatService {
private readonly translate = inject(TranslateService);
private readonly language = inject(LanguageService);
private readonly loreEndpoint = '/api/ai/chat/stream';
private readonly campaignEndpoint = '/api/ai/chat/stream-campaign';
private readonly sessionEndpoint = '/api/ai/chat/stream-session';
@@ -110,7 +112,8 @@ export class AiChatService {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
'Accept': 'text/event-stream',
'X-User-Language': this.language.current
},
body: JSON.stringify(body),
signal: controller.signal

View File

@@ -7,6 +7,7 @@ import {
CampaignImportProposal,
CampaignImportStreamEvent
} from './campaign-import.model';
import { LanguageService } from './language.service';
/**
* Service HTTP pour l'import d'un PDF de campagne.
@@ -17,7 +18,7 @@ import {
*/
@Injectable({ providedIn: 'root' })
export class CampaignImportService {
constructor(private http: HttpClient, private translate: TranslateService) {}
constructor(private http: HttpClient, private translate: TranslateService, private language: LanguageService) {}
importStructureStream(campaignId: string, file: File): Observable<CampaignImportStreamEvent> {
return new Observable<CampaignImportStreamEvent>((subscriber) => {
@@ -27,7 +28,7 @@ export class CampaignImportService {
fetch(`/api/campaigns/${campaignId}/import-structure/stream`, {
method: 'POST',
headers: { 'Accept': 'text/event-stream' },
headers: { 'Accept': 'text/event-stream', 'X-User-Language': this.language.current },
body: form,
signal: controller.signal
})

View File

@@ -3,6 +3,7 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { GameSystem, GameSystemCreate, RulesImportResponse, RulesImportStreamEvent } from './game-system.model';
import { LanguageService } from './language.service';
/**
* Service HTTP pour les GameSystems (systèmes de JDR).
@@ -11,7 +12,7 @@ import { GameSystem, GameSystemCreate, RulesImportResponse, RulesImportStreamEve
export class GameSystemService {
private apiUrl = '/api/game-systems';
constructor(private http: HttpClient, private translate: TranslateService) {}
constructor(private http: HttpClient, private translate: TranslateService, private language: LanguageService) {}
getAll(): Observable<GameSystem[]> {
return this.http.get<GameSystem[]>(this.apiUrl);
@@ -63,7 +64,7 @@ export class GameSystemService {
fetch(`${this.apiUrl}/import-rules/stream`, {
method: 'POST',
headers: { 'Accept': 'text/event-stream' },
headers: { 'Accept': 'text/event-stream', 'X-User-Language': this.language.current },
body: form,
signal: controller.signal
})

View File

@@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { Notebook, NotebookArchive, NotebookDetail, NotebookSource, NotebookChatEvent } from './notebook.model';
import { LanguageService } from './language.service';
/**
* Service des notebooks (atelier RAG) : CRUD, upload/indexation de sources,
@@ -12,7 +13,7 @@ import { Notebook, NotebookArchive, NotebookDetail, NotebookSource, NotebookChat
export class NotebookService {
private readonly apiUrl = '/api/notebooks';
constructor(private http: HttpClient, private zone: NgZone, private translate: TranslateService) {}
constructor(private http: HttpClient, private zone: NgZone, private translate: TranslateService, private language: LanguageService) {}
listByCampaign(campaignId: string): Observable<Notebook[]> {
return this.http.get<Notebook[]>(`${this.apiUrl}/campaign/${campaignId}`);
@@ -73,7 +74,7 @@ export class NotebookService {
try {
const response = await fetch(`${this.apiUrl}/${notebookId}/chat/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', 'X-User-Language': this.language.current },
credentials: 'include',
body: JSON.stringify({
message, deep,

View File

@@ -9,6 +9,7 @@ import { provideTranslateHttpLoader } from '@ngx-translate/http-loader';
import { ConfigService } from './app/services/config.service';
import { LanguageService } from './app/services/language.service';
import { sessionExpiredInterceptor } from './app/interceptors/session-expired.interceptor';
import { languageInterceptor } from './app/interceptors/language.interceptor';
// withPreloading(PreloadAllModules) : une fois l'app initiale rendue, Angular
// telecharge en arriere-plan tous les chunks lazy-loades. Consequence : la
@@ -18,7 +19,7 @@ import { sessionExpiredInterceptor } from './app/interceptors/session-expired.in
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection(),provideRouter(routes, withPreloading(PreloadAllModules)),
provideHttpClient(withInterceptors([sessionExpiredInterceptor])),
provideHttpClient(withInterceptors([sessionExpiredInterceptor, languageInterceptor])),
provideTranslateService({
loader: provideTranslateHttpLoader({
prefix: 'assets/i18n/',