Mise en place de l'anglais comme deuxième langue pour l'application
Some checks failed
Build & Push Images / build (brain) (push) Has been cancelled
Build & Push Images / build (core) (push) Has been cancelled
Build & Push Images / build (web) (push) Has been cancelled
Build & Push Images / build-switcher (push) Has been cancelled

This commit is contained in:
2026-06-14 16:24:05 +02:00
parent 6e75326779
commit af3a6d443c
198 changed files with 7801 additions and 1720 deletions

View File

@@ -4,7 +4,7 @@
[class.is-resizing]="isResizing"
[class.is-wide]="isWide"
[style.width.px]="effectiveWidth"
aria-label="Assistant IA">
[attr.aria-label]="'aiChatDrawer.title' | translate">
<!-- Poignee de redimensionnement (desactivee en mode wide) -->
@if (!isWide) {
@@ -13,23 +13,23 @@
(mousedown)="onResizeStart($event)"
role="separator"
aria-orientation="vertical"
aria-label="Redimensionner le panneau"
title="Glisser pour redimensionner"></div>
[attr.aria-label]="'aiChatDrawer.resizeAria' | translate"
[title]="'aiChatDrawer.resizeTitle' | translate"></div>
}
<!-- Sidebar conversations (mode persistent uniquement) -->
@if (persistent && sidebarOpen) {
<section class="conv-sidebar" aria-label="Conversations">
<section class="conv-sidebar" [attr.aria-label]="'aiChatDrawer.conversations' | translate">
<div class="conv-sidebar-header">
<span class="conv-sidebar-title">Conversations</span>
<button type="button" class="conv-new-btn" (click)="startNewConversation()" [disabled]="isStreaming" title="Nouvelle conversation">
<span class="conv-sidebar-title">{{ 'aiChatDrawer.conversations' | translate }}</span>
<button type="button" class="conv-new-btn" (click)="startNewConversation()" [disabled]="isStreaming" [title]="'aiChatDrawer.newConversation' | translate">
<lucide-icon [img]="MessageSquarePlus" [size]="16"></lucide-icon>
</button>
</div>
<ul class="conv-list">
@if (conversations.length === 0) {
<li class="conv-empty">Aucune conversation</li>
<li class="conv-empty">{{ 'aiChatDrawer.noConversation' | translate }}</li>
}
@for (c of conversations; track c) {
<li
@@ -42,7 +42,7 @@
class="conv-item-del"
(click)="deleteConversation(c, $event)"
[disabled]="isStreaming"
title="Supprimer">
[title]="'common.delete' | translate">
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
</button>
</li>
@@ -59,8 +59,8 @@
type="button"
class="sidebar-toggle"
(click)="toggleSidebar()"
[attr.aria-label]="sidebarOpen ? 'Masquer la liste' : 'Afficher la liste'"
[title]="sidebarOpen ? 'Masquer la liste' : 'Afficher la liste'">
[attr.aria-label]="(sidebarOpen ? 'aiChatDrawer.hideList' : 'aiChatDrawer.showList') | translate"
[title]="(sidebarOpen ? 'aiChatDrawer.hideList' : 'aiChatDrawer.showList') | translate">
<lucide-icon [img]="sidebarOpen ? PanelLeftClose : PanelLeftOpen" [size]="16"></lucide-icon>
</button>
}
@@ -68,8 +68,8 @@
<div class="header-title-wrap">
@if (persistent && currentConversationId) {
@if (!editingTitle) {
<h2 class="header-title" [title]="currentTitle">{{ currentTitle || 'Nouvelle conversation' }}</h2>
<button type="button" class="rename-btn" (click)="startRenameTitle()" title="Renommer" aria-label="Renommer">
<h2 class="header-title" [title]="currentTitle">{{ currentTitle || ('aiChatDrawer.newConversation' | translate) }}</h2>
<button type="button" class="rename-btn" (click)="startRenameTitle()" [title]="'common.rename' | translate" [attr.aria-label]="'common.rename' | translate">
<lucide-icon [img]="Pencil" [size]="12"></lucide-icon>
</button>
} @else {
@@ -83,7 +83,7 @@
autofocus />
}
} @else {
<h2 class="header-title">Assistant IA</h2>
<h2 class="header-title">{{ 'aiChatDrawer.title' | translate }}</h2>
}
</div>
@@ -91,19 +91,19 @@
type="button"
class="wide-toggle-btn"
(click)="toggleWide()"
[attr.aria-label]="isWide ? 'Reduire le panneau' : 'Agrandir le panneau'"
[title]="isWide ? 'Reduire' : 'Agrandir'">
[attr.aria-label]="(isWide ? 'aiChatDrawer.shrinkAria' : 'aiChatDrawer.expandAria') | translate"
[title]="(isWide ? 'aiChatDrawer.shrink' : 'aiChatDrawer.expand') | translate">
<lucide-icon [img]="isWide ? Minimize2 : Maximize2" [size]="16"></lucide-icon>
</button>
<button type="button" class="close-btn" (click)="onClose()" aria-label="Fermer">
<button type="button" class="close-btn" (click)="onClose()" [attr.aria-label]="'common.close' | translate">
<lucide-icon [img]="X" [size]="18"></lucide-icon>
</button>
</header>
@if (usage) {
<div class="context-gauge" [attr.data-level]="usageLevel"
[attr.title]="'System: ' + usage.system + ' · Historique: ' + usage.history + ' · Courant: ' + usage.current + (usageHasMax ? ' / ' + usage.max : '') + ' tokens'">
[attr.title]="(usageHasMax ? 'aiChatDrawer.gaugeTitleMax' : 'aiChatDrawer.gaugeTitle') | translate:{ system: usage.system, history: usage.history, current: usage.current, max: usage.max }">
<!-- Barre seulement si on connaît le plafond (Ollama). Cloud : compteur seul. -->
@if (usageHasMax) {
<div class="gauge-bar">
@@ -111,9 +111,11 @@
</div>
}
<div class="gauge-label">
<span class="gauge-text">Contexte : {{ usageTotal }}@if (usageHasMax) {
/ {{ usage.max }}
} tokens</span>
<span class="gauge-text">@if (usageHasMax) {
{{ 'aiChatDrawer.gaugeLabelMax' | translate:{ total: usageTotal, max: usage.max } }}
} @else {
{{ 'aiChatDrawer.gaugeLabel' | translate:{ total: usageTotal } }}
}</span>
@if (usageHasMax) {
<span class="gauge-percent">{{ usagePercent }}%</span>
}
@@ -172,7 +174,7 @@
@if (quickSuggestions.length) {
<div class="quick-suggestions">
<p class="quick-label">Suggestions rapides :</p>
<p class="quick-label">{{ 'aiChatDrawer.quickSuggestions' | translate }}</p>
<div class="quick-list">
@for (s of quickSuggestions; track s) {
<button
@@ -193,10 +195,10 @@
type="text"
[(ngModel)]="input"
name="chatInput"
placeholder="Posez une question..."
[placeholder]="'aiChatDrawer.inputPlaceholder' | translate"
[disabled]="isStreaming"
autocomplete="off" />
<button type="submit" class="send-btn" [disabled]="!input.trim() || isStreaming" aria-label="Envoyer">
<button type="submit" class="send-btn" [disabled]="!input.trim() || isStreaming" [attr.aria-label]="'aiChatDrawer.send' | translate">
<lucide-icon [img]="Send" [size]="16"></lucide-icon>
</button>
</form>

View File

@@ -2,6 +2,7 @@
import { Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { LucideAngularModule, Lightbulb, Maximize2, MessageSquarePlus, Minimize2, PanelLeftClose, PanelLeftOpen, Pencil, Send, Sparkles, Trash2, Wand2, X } from 'lucide-angular';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { AiChatService, ChatMessage, ChatUsage, NarrativeEntityType } from '../../services/ai-chat.service';
import { Conversation, ConversationContext } from '../../services/conversation.model';
@@ -31,7 +32,7 @@ export interface ChatPrimaryAction {
*/
@Component({
selector: 'app-ai-chat-drawer',
imports: [FormsModule, LucideAngularModule, MarkdownPipe],
imports: [FormsModule, LucideAngularModule, MarkdownPipe, TranslatePipe],
templateUrl: './ai-chat-drawer.component.html',
styleUrls: ['./ai-chat-drawer.component.scss']
})
@@ -80,7 +81,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
@Input() entityType: NarrativeEntityType | null = null;
@Input() entityId: string | null = null;
@Input() isOpen = false;
@Input() welcomeMessage = 'Bonjour ! Je peux vous aider a developper cette page. Que souhaitez-vous creer ?';
@Input() welcomeMessage = '';
@Input() quickSuggestions: string[] = [];
@Input() primaryAction: ChatPrimaryAction | null = null;
@Input() systemPromptAddon: string | null = null;
@@ -120,7 +121,10 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
private readonly chatService: AiChatService,
private readonly conversationService: ConversationService,
private readonly confirmDialog: ConfirmDialogService,
) {}
private readonly translate: TranslateService,
) {
this.welcomeMessage = this.translate.instant('aiChatDrawer.welcome');
}
// --- Jauge de contexte --------------------------------------------------
@@ -310,7 +314,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
this.usage = null;
this.scrollToBottom();
},
error: () => (this.errorMessage = 'Impossible de charger la conversation.'),
error: () => (this.errorMessage = this.translate.instant('aiChatDrawer.loadConversationError')),
});
}
@@ -318,9 +322,9 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
event.stopPropagation();
if (this.isStreaming) return;
this.confirmDialog.confirm({
title: 'Supprimer la conversation',
message: `Supprimer la conversation "${conv.title}" ?`,
confirmLabel: 'Supprimer',
title: this.translate.instant('aiChatDrawer.deleteConversationTitle'),
message: this.translate.instant('aiChatDrawer.deleteConversationMessage', { title: conv.title }),
confirmLabel: this.translate.instant('common.delete'),
variant: 'danger'
}).then(ok => {
if (!ok) return;
@@ -410,7 +414,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
if (this.currentConversationId) return Promise.resolve(this.currentConversationId);
const ctx = this.buildContext();
if (!ctx.loreId && !ctx.campaignId) {
this.errorMessage = 'Contexte manquant pour creer une conversation.';
this.errorMessage = this.translate.instant('aiChatDrawer.missingContextError');
return Promise.resolve(null);
}
return new Promise((resolve) => {
@@ -422,7 +426,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
resolve(conv.id);
},
error: () => {
this.errorMessage = 'Impossible de creer la conversation.';
this.errorMessage = this.translate.instant('aiChatDrawer.createConversationError');
resolve(null);
},
});
@@ -460,7 +464,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
},
error: (err) => {
this.isStreaming = false;
this.errorMessage = err?.message ?? 'Erreur inconnue.';
this.errorMessage = err?.message ?? this.translate.instant('aiChatDrawer.unknownError');
this.currentAssistantText = '';
},
complete: () => {