Migration Angular 20 -> 21 : fin de la migration securite (0 vulnerabilite npm)
- ng update @angular/core@21 @angular/cli@21 (corrige GHSA-jrmj-c5cx-3cw6, XSS SVG) - Migration automatique des templates vers le control flow natif (@if/@for, 138 fichiers) - Correction des 5 `track` invalides generes par la migration (trackBy a 1 argument -> track $index) + suppression des fonctions trackBy mortes - TypeScript 5.9, zone.js 0.15 ; npm audit : 0 vulnerabilite Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,66 +7,72 @@
|
||||
aria-label="Assistant IA">
|
||||
|
||||
<!-- Poignee de redimensionnement (desactivee en mode wide) -->
|
||||
<div
|
||||
class="resize-handle"
|
||||
*ngIf="!isWide"
|
||||
(mousedown)="onResizeStart($event)"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="Redimensionner le panneau"
|
||||
@if (!isWide) {
|
||||
<div
|
||||
class="resize-handle"
|
||||
(mousedown)="onResizeStart($event)"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="Redimensionner le panneau"
|
||||
title="Glisser pour redimensionner"></div>
|
||||
}
|
||||
|
||||
|
||||
<!-- Sidebar conversations (mode persistent uniquement) -->
|
||||
<section class="conv-sidebar" *ngIf="persistent && sidebarOpen" aria-label="Conversations">
|
||||
<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">
|
||||
<lucide-icon [img]="MessageSquarePlus" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="conv-list">
|
||||
<li *ngIf="conversations.length === 0" class="conv-empty">Aucune conversation</li>
|
||||
<li
|
||||
*ngFor="let c of conversations"
|
||||
class="conv-item"
|
||||
[class.active]="c.id === currentConversationId"
|
||||
(click)="selectConversation(c)">
|
||||
<span class="conv-item-title">{{ c.title }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="conv-item-del"
|
||||
(click)="deleteConversation(c, $event)"
|
||||
[disabled]="isStreaming"
|
||||
title="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
|
||||
@if (persistent && sidebarOpen) {
|
||||
<section class="conv-sidebar" aria-label="Conversations">
|
||||
<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">
|
||||
<lucide-icon [img]="MessageSquarePlus" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<ul class="conv-list">
|
||||
@if (conversations.length === 0) {
|
||||
<li class="conv-empty">Aucune conversation</li>
|
||||
}
|
||||
@for (c of conversations; track c) {
|
||||
<li
|
||||
class="conv-item"
|
||||
[class.active]="c.id === currentConversationId"
|
||||
(click)="selectConversation(c)">
|
||||
<span class="conv-item-title">{{ c.title }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="conv-item-del"
|
||||
(click)="deleteConversation(c, $event)"
|
||||
[disabled]="isStreaming"
|
||||
title="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="conv-main">
|
||||
|
||||
<header class="drawer-header">
|
||||
<button
|
||||
*ngIf="persistent"
|
||||
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'">
|
||||
<lucide-icon [img]="sidebarOpen ? PanelLeftClose : PanelLeftOpen" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
@if (persistent) {
|
||||
<button
|
||||
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'">
|
||||
<lucide-icon [img]="sidebarOpen ? PanelLeftClose : PanelLeftOpen" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
<div class="header-title-wrap">
|
||||
<ng-container *ngIf="persistent && currentConversationId; else defaultTitle">
|
||||
<ng-container *ngIf="!editingTitle; else editingTpl">
|
||||
@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">
|
||||
<lucide-icon [img]="Pencil" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-template #editingTpl>
|
||||
} @else {
|
||||
<input
|
||||
class="rename-input"
|
||||
type="text"
|
||||
@@ -75,101 +81,125 @@
|
||||
(keyup.escape)="cancelRenameTitle()"
|
||||
(blur)="submitRenameTitle()"
|
||||
autofocus />
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-template #defaultTitle>
|
||||
<h2 class="header-title">Assistant IA</h2>
|
||||
</ng-template>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<h2 class="header-title">Assistant IA</h2>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="wide-toggle-btn"
|
||||
(click)="toggleWide()"
|
||||
[attr.aria-label]="isWide ? 'Reduire le panneau' : 'Agrandir le panneau'"
|
||||
[title]="isWide ? 'Reduire' : 'Agrandir'">
|
||||
<lucide-icon [img]="isWide ? Minimize2 : Maximize2" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
|
||||
<button type="button" class="close-btn" (click)="onClose()" aria-label="Fermer">
|
||||
<lucide-icon [img]="X" [size]="18"></lucide-icon>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="context-gauge" *ngIf="usage" [attr.data-level]="usageLevel"
|
||||
[attr.title]="'System: ' + usage.system + ' · Historique: ' + usage.history + ' · Courant: ' + usage.current + (usageHasMax ? ' / ' + usage.max : '') + ' tokens'">
|
||||
<!-- Barre seulement si on connaît le plafond (Ollama). Cloud : compteur seul. -->
|
||||
<div class="gauge-bar" *ngIf="usageHasMax">
|
||||
<div class="gauge-fill" [style.width.%]="usagePercent"></div>
|
||||
</div>
|
||||
<div class="gauge-label">
|
||||
<span class="gauge-text">Contexte : {{ usageTotal }}<ng-container *ngIf="usageHasMax"> / {{ usage.max }}</ng-container> tokens</span>
|
||||
<span class="gauge-percent" *ngIf="usageHasMax">{{ usagePercent }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div #messagesContainer class="messages">
|
||||
<div class="msg msg-assistant md" *ngIf="messages.length === 0 && !currentAssistantText"
|
||||
[innerHTML]="welcomeMessage | markdown">
|
||||
</div>
|
||||
|
||||
<ng-container *ngFor="let m of messages">
|
||||
<div *ngIf="m.role === 'user'" class="msg msg-user">{{ m.content }}</div>
|
||||
<div *ngIf="m.role === 'assistant'" class="msg msg-assistant md"
|
||||
[innerHTML]="m.content | markdown"></div>
|
||||
</ng-container>
|
||||
|
||||
<div class="msg msg-assistant msg-streaming md" *ngIf="currentAssistantText">
|
||||
<span [innerHTML]="currentAssistantText | markdown"></span><span class="caret"></span>
|
||||
</div>
|
||||
|
||||
<div class="typing-indicator" *ngIf="isStreaming && !currentAssistantText" aria-live="polite">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
|
||||
<div class="msg msg-error" *ngIf="errorMessage" role="alert">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="primary-action" *ngIf="primaryAction">
|
||||
<button
|
||||
type="button"
|
||||
class="primary-btn"
|
||||
(click)="onPrimaryAction()"
|
||||
[disabled]="isStreaming">
|
||||
<lucide-icon [img]="Wand2" [size]="14"></lucide-icon>
|
||||
{{ primaryAction.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="quick-suggestions" *ngIf="quickSuggestions.length">
|
||||
<p class="quick-label">Suggestions rapides :</p>
|
||||
<div class="quick-list">
|
||||
<button
|
||||
type="button"
|
||||
class="quick-btn"
|
||||
*ngFor="let s of quickSuggestions"
|
||||
(click)="useQuickSuggestion(s)"
|
||||
[disabled]="isStreaming">
|
||||
<lucide-icon [img]="Lightbulb" [size]="12"></lucide-icon>
|
||||
{{ s }}
|
||||
class="wide-toggle-btn"
|
||||
(click)="toggleWide()"
|
||||
[attr.aria-label]="isWide ? 'Reduire le panneau' : 'Agrandir le panneau'"
|
||||
[title]="isWide ? 'Reduire' : 'Agrandir'">
|
||||
<lucide-icon [img]="isWide ? Minimize2 : Maximize2" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
|
||||
<button type="button" class="close-btn" (click)="onClose()" aria-label="Fermer">
|
||||
<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'">
|
||||
<!-- Barre seulement si on connaît le plafond (Ollama). Cloud : compteur seul. -->
|
||||
@if (usageHasMax) {
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" [style.width.%]="usagePercent"></div>
|
||||
</div>
|
||||
}
|
||||
<div class="gauge-label">
|
||||
<span class="gauge-text">Contexte : {{ usageTotal }}@if (usageHasMax) {
|
||||
/ {{ usage.max }}
|
||||
} tokens</span>
|
||||
@if (usageHasMax) {
|
||||
<span class="gauge-percent">{{ usagePercent }}%</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div #messagesContainer class="messages">
|
||||
@if (messages.length === 0 && !currentAssistantText) {
|
||||
<div class="msg msg-assistant md"
|
||||
[innerHTML]="welcomeMessage | markdown">
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (m of messages; track m) {
|
||||
@if (m.role === 'user') {
|
||||
<div class="msg msg-user">{{ m.content }}</div>
|
||||
}
|
||||
@if (m.role === 'assistant') {
|
||||
<div class="msg msg-assistant md"
|
||||
[innerHTML]="m.content | markdown"></div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (currentAssistantText) {
|
||||
<div class="msg msg-assistant msg-streaming md">
|
||||
<span [innerHTML]="currentAssistantText | markdown"></span><span class="caret"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isStreaming && !currentAssistantText) {
|
||||
<div class="typing-indicator" aria-live="polite">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="msg msg-error" role="alert">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="input-row" (ngSubmit)="send()">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="input"
|
||||
name="chatInput"
|
||||
placeholder="Posez une question..."
|
||||
[disabled]="isStreaming"
|
||||
autocomplete="off" />
|
||||
<button type="submit" class="send-btn" [disabled]="!input.trim() || isStreaming" aria-label="Envoyer">
|
||||
<lucide-icon [img]="Send" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
</form>
|
||||
@if (primaryAction) {
|
||||
<div class="primary-action">
|
||||
<button
|
||||
type="button"
|
||||
class="primary-btn"
|
||||
(click)="onPrimaryAction()"
|
||||
[disabled]="isStreaming">
|
||||
<lucide-icon [img]="Wand2" [size]="14"></lucide-icon>
|
||||
{{ primaryAction.label }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
</section>
|
||||
</aside>
|
||||
@if (quickSuggestions.length) {
|
||||
<div class="quick-suggestions">
|
||||
<p class="quick-label">Suggestions rapides :</p>
|
||||
<div class="quick-list">
|
||||
@for (s of quickSuggestions; track s) {
|
||||
<button
|
||||
type="button"
|
||||
class="quick-btn"
|
||||
(click)="useQuickSuggestion(s)"
|
||||
[disabled]="isStreaming">
|
||||
<lucide-icon [img]="Lightbulb" [size]="12"></lucide-icon>
|
||||
{{ s }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<form class="input-row" (ngSubmit)="send()">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="input"
|
||||
name="chatInput"
|
||||
placeholder="Posez une question..."
|
||||
[disabled]="isStreaming"
|
||||
autocomplete="off" />
|
||||
<button type="submit" class="send-btn" [disabled]="!input.trim() || isStreaming" aria-label="Envoyer">
|
||||
<lucide-icon [img]="Send" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
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';
|
||||
@@ -31,7 +31,7 @@ export interface ChatPrimaryAction {
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-ai-chat-drawer',
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule, MarkdownPipe],
|
||||
imports: [FormsModule, LucideAngularModule, MarkdownPipe],
|
||||
templateUrl: './ai-chat-drawer.component.html',
|
||||
styleUrls: ['./ai-chat-drawer.component.scss']
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user