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:
@@ -5,37 +5,55 @@
|
||||
<span class="nac-name">{{ action.name }}</span>
|
||||
</div>
|
||||
|
||||
<p class="nac-desc" *ngIf="action.description">{{ action.description }}</p>
|
||||
@if (action.description) {
|
||||
<p class="nac-desc">{{ action.description }}</p>
|
||||
}
|
||||
|
||||
<!-- Cibles -->
|
||||
<div class="nac-targets" *ngIf="status !== 'created' && needsArc">
|
||||
<label>
|
||||
Arc
|
||||
<select [(ngModel)]="selectedArcId" (ngModelChange)="syncChapter()">
|
||||
<option *ngFor="let a of arcs" [value]="a.id">{{ a.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label *ngIf="needsChapter">
|
||||
Chapitre
|
||||
<select [(ngModel)]="selectedChapterId">
|
||||
<option *ngFor="let c of targetChapters" [value]="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
@if (status !== 'created' && needsArc) {
|
||||
<div class="nac-targets">
|
||||
<label>
|
||||
Arc
|
||||
<select [(ngModel)]="selectedArcId" (ngModelChange)="syncChapter()">
|
||||
@for (a of arcs; track a) {
|
||||
<option [value]="a.id">{{ a.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (needsChapter) {
|
||||
<label>
|
||||
Chapitre
|
||||
<select [(ngModel)]="selectedChapterId">
|
||||
@for (c of targetChapters; track c) {
|
||||
<option [value]="c.id">{{ c.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<p class="nac-warn" *ngIf="needsArc && arcs.length === 0">
|
||||
Crée d'abord un arc {{ needsChapter ? 'et un chapitre ' : '' }}dans la campagne pour pouvoir y rattacher cet élément.
|
||||
</p>
|
||||
@if (needsArc && arcs.length === 0) {
|
||||
<p class="nac-warn">
|
||||
Crée d'abord un arc {{ needsChapter ? 'et un chapitre ' : '' }}dans la campagne pour pouvoir y rattacher cet élément.
|
||||
</p>
|
||||
}
|
||||
|
||||
<div class="nac-foot">
|
||||
<button class="nac-create" *ngIf="status !== 'created'" (click)="create()" [disabled]="!canCreate">
|
||||
<lucide-icon [img]="Plus" [size]="13"></lucide-icon>
|
||||
{{ status === 'creating' ? 'Création…' : 'Créer dans la campagne' }}
|
||||
</button>
|
||||
<button class="nac-open" *ngIf="status === 'created'" (click)="openCreated()">
|
||||
<lucide-icon [img]="Check" [size]="13"></lucide-icon> Créé — ouvrir
|
||||
<lucide-icon [img]="ExternalLink" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
<span class="nac-error" *ngIf="status === 'error'">{{ errorMsg }}</span>
|
||||
@if (status !== 'created') {
|
||||
<button class="nac-create" (click)="create()" [disabled]="!canCreate">
|
||||
<lucide-icon [img]="Plus" [size]="13"></lucide-icon>
|
||||
{{ status === 'creating' ? 'Création…' : 'Créer dans la campagne' }}
|
||||
</button>
|
||||
}
|
||||
@if (status === 'created') {
|
||||
<button class="nac-open" (click)="openCreated()">
|
||||
<lucide-icon [img]="Check" [size]="13"></lucide-icon> Créé — ouvrir
|
||||
<lucide-icon [img]="ExternalLink" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
@if (status === 'error') {
|
||||
<span class="nac-error">{{ errorMsg }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { LucideAngularModule, Plus, Check, Drama, Clapperboard, BookText, GitBranch, Dices, ExternalLink } from 'lucide-angular';
|
||||
@@ -15,7 +15,7 @@ import { NotebookAction } from '../../../services/notebook-action.model';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-notebook-action-card',
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule],
|
||||
imports: [FormsModule, LucideAngularModule],
|
||||
templateUrl: './notebook-action-card.component.html',
|
||||
styleUrls: ['./notebook-action-card.component.scss']
|
||||
})
|
||||
|
||||
@@ -1,98 +1,117 @@
|
||||
<div class="nbd-page" *ngIf="detail">
|
||||
<div class="nbd-toolbar">
|
||||
<button class="btn-back" (click)="back()">
|
||||
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon> Ateliers
|
||||
</button>
|
||||
<input class="nbd-title" [(ngModel)]="detail.name" (blur)="rename()" (keyup.enter)="rename()">
|
||||
</div>
|
||||
|
||||
<div class="nbd-grid">
|
||||
<!-- Sources -->
|
||||
<aside class="nbd-sources">
|
||||
<div class="nbd-sources-head">
|
||||
<h3><lucide-icon [img]="FileText" [size]="15"></lucide-icon> Sources</h3>
|
||||
<label class="btn-upload" [class.disabled]="uploading">
|
||||
<lucide-icon [img]="Upload" [size]="13"></lucide-icon>
|
||||
{{ uploading ? 'Indexation…' : 'Ajouter un PDF' }}
|
||||
<input type="file" accept="application/pdf" hidden (change)="onFile($event)" [disabled]="uploading">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="nbd-upload-error" *ngIf="uploadError">{{ uploadError }}</p>
|
||||
|
||||
<div class="nbd-source" *ngFor="let s of sources">
|
||||
<lucide-icon
|
||||
[img]="s.status === 'READY' ? CheckCircle2 : (s.status === 'FAILED' ? AlertCircle : Loader)"
|
||||
[size]="14"
|
||||
[class.ok]="s.status === 'READY'" [class.fail]="s.status === 'FAILED'" [class.busy]="s.status === 'INDEXING'">
|
||||
</lucide-icon>
|
||||
<div class="nbd-source-info">
|
||||
<div class="nbd-source-name">{{ s.filename }}</div>
|
||||
<div class="nbd-source-meta">
|
||||
<span *ngIf="s.status === 'READY'">{{ s.pageCount }} p. · {{ s.chunkCount }} extraits</span>
|
||||
<span *ngIf="s.status === 'INDEXING'">indexation en cours…</span>
|
||||
<span *ngIf="s.status === 'FAILED'">échec de l'indexation</span>
|
||||
</div>
|
||||
@if (detail) {
|
||||
<div class="nbd-page">
|
||||
<div class="nbd-toolbar">
|
||||
<button class="btn-back" (click)="back()">
|
||||
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon> Ateliers
|
||||
</button>
|
||||
<input class="nbd-title" [(ngModel)]="detail.name" (blur)="rename()" (keyup.enter)="rename()">
|
||||
</div>
|
||||
<div class="nbd-grid">
|
||||
<!-- Sources -->
|
||||
<aside class="nbd-sources">
|
||||
<div class="nbd-sources-head">
|
||||
<h3><lucide-icon [img]="FileText" [size]="15"></lucide-icon> Sources</h3>
|
||||
<label class="btn-upload" [class.disabled]="uploading">
|
||||
<lucide-icon [img]="Upload" [size]="13"></lucide-icon>
|
||||
{{ uploading ? 'Indexation…' : 'Ajouter un PDF' }}
|
||||
<input type="file" accept="application/pdf" hidden (change)="onFile($event)" [disabled]="uploading">
|
||||
</label>
|
||||
</div>
|
||||
<button class="nbd-source-del" (click)="removeSource(s)" title="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="nbd-empty" *ngIf="sources.length === 0 && !uploading">
|
||||
Ajoute un PDF source pour commencer à discuter avec.
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
<!-- Chat -->
|
||||
<section class="nbd-chat">
|
||||
<div class="nbd-messages">
|
||||
<p class="nbd-empty" *ngIf="messages.length === 0">
|
||||
Pose une question sur ta source, ou demande une adaptation pour ta campagne.
|
||||
<span *ngIf="!hasReadySource()"><br>(Ajoute d'abord une source indexée pour des réponses ancrées.)</span>
|
||||
</p>
|
||||
<div class="nbd-msg" *ngFor="let m of messages" [class.user]="m.role === 'user'" [class.assistant]="m.role === 'assistant'">
|
||||
<div class="nbd-msg-role">
|
||||
<lucide-icon *ngIf="m.role === 'assistant'" [img]="Sparkles" [size]="12"></lucide-icon>
|
||||
{{ m.role === 'user' ? 'Vous' : 'IA' }}
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="m.role === 'assistant'; else userContent">
|
||||
<ng-container *ngIf="parsedOf(m) as p">
|
||||
<div class="nbd-deep-progress" *ngIf="sending && deepProgress && !p.text">
|
||||
<lucide-icon [img]="Layers" [size]="13"></lucide-icon>
|
||||
Analyse approfondie du document… {{ deepProgress.current }}/{{ deepProgress.total }}
|
||||
@if (uploadError) {
|
||||
<p class="nbd-upload-error">{{ uploadError }}</p>
|
||||
}
|
||||
@for (s of sources; track s) {
|
||||
<div class="nbd-source">
|
||||
<lucide-icon
|
||||
[img]="s.status === 'READY' ? CheckCircle2 : (s.status === 'FAILED' ? AlertCircle : Loader)"
|
||||
[size]="14"
|
||||
[class.ok]="s.status === 'READY'" [class.fail]="s.status === 'FAILED'" [class.busy]="s.status === 'INDEXING'">
|
||||
</lucide-icon>
|
||||
<div class="nbd-source-info">
|
||||
<div class="nbd-source-name">{{ s.filename }}</div>
|
||||
<div class="nbd-source-meta">
|
||||
@if (s.status === 'READY') {
|
||||
<span>{{ s.pageCount }} p. · {{ s.chunkCount }} extraits</span>
|
||||
}
|
||||
@if (s.status === 'INDEXING') {
|
||||
<span>indexation en cours…</span>
|
||||
}
|
||||
@if (s.status === 'FAILED') {
|
||||
<span>échec de l'indexation</span>
|
||||
}
|
||||
</div>
|
||||
<div class="nbd-msg-content">{{ p.text }}<span class="cursor" *ngIf="sending && !p.text && !p.actions.length && !deepProgress">▌</span></div>
|
||||
<app-notebook-action-card
|
||||
*ngFor="let a of p.actions; trackBy: trackAction"
|
||||
[action]="a"
|
||||
[campaignId]="campaignId"
|
||||
[arcs]="arcs"
|
||||
[chaptersByArc]="chaptersByArc"
|
||||
(created)="onActionCreated()">
|
||||
</app-notebook-action-card>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #userContent>
|
||||
<div class="nbd-msg-content">{{ m.content }}</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<button class="nbd-source-del" (click)="removeSource(s)" title="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (sources.length === 0 && !uploading) {
|
||||
<p class="nbd-empty">
|
||||
Ajoute un PDF source pour commencer à discuter avec.
|
||||
</p>
|
||||
}
|
||||
</aside>
|
||||
<!-- Chat -->
|
||||
<section class="nbd-chat">
|
||||
<div class="nbd-messages">
|
||||
@if (messages.length === 0) {
|
||||
<p class="nbd-empty">
|
||||
Pose une question sur ta source, ou demande une adaptation pour ta campagne.
|
||||
@if (!hasReadySource()) {
|
||||
<span><br>(Ajoute d'abord une source indexée pour des réponses ancrées.)</span>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
@for (m of messages; track m) {
|
||||
<div class="nbd-msg" [class.user]="m.role === 'user'" [class.assistant]="m.role === 'assistant'">
|
||||
<div class="nbd-msg-role">
|
||||
@if (m.role === 'assistant') {
|
||||
<lucide-icon [img]="Sparkles" [size]="12"></lucide-icon>
|
||||
}
|
||||
{{ m.role === 'user' ? 'Vous' : 'IA' }}
|
||||
</div>
|
||||
@if (m.role === 'assistant') {
|
||||
@if (parsedOf(m); as p) {
|
||||
@if (sending && deepProgress && !p.text) {
|
||||
<div class="nbd-deep-progress">
|
||||
<lucide-icon [img]="Layers" [size]="13"></lucide-icon>
|
||||
Analyse approfondie du document… {{ deepProgress.current }}/{{ deepProgress.total }}
|
||||
</div>
|
||||
}
|
||||
<div class="nbd-msg-content">{{ p.text }}@if (sending && !p.text && !p.actions.length && !deepProgress) {
|
||||
<span class="cursor">▌</span>
|
||||
}</div>
|
||||
@for (a of p.actions; track $index) {
|
||||
<app-notebook-action-card
|
||||
[action]="a"
|
||||
[campaignId]="campaignId"
|
||||
[arcs]="arcs"
|
||||
[chaptersByArc]="chaptersByArc"
|
||||
(created)="onActionCreated()">
|
||||
</app-notebook-action-card>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<div class="nbd-msg-content">{{ m.content }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nbd-input">
|
||||
<textarea [(ngModel)]="draft" rows="2"
|
||||
placeholder="Demande une adaptation, un résumé, un PNJ inspiré de la source…"
|
||||
(keydown.enter)="$event.preventDefault(); send()"></textarea>
|
||||
<button class="btn-deep" (click)="send(true)" [disabled]="sending || !draft.trim()"
|
||||
title="Analyse approfondie : lit TOUT le document (plus lent, exhaustif). Idéal pour « liste tous les… »">
|
||||
<lucide-icon [img]="Layers" [size]="15"></lucide-icon>
|
||||
</button>
|
||||
<button class="btn-send" (click)="send()" [disabled]="sending || !draft.trim()"
|
||||
title="Réponse rapide (recherche ciblée dans le document)">
|
||||
<lucide-icon [img]="Send" [size]="15"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="nbd-input">
|
||||
<textarea [(ngModel)]="draft" rows="2"
|
||||
placeholder="Demande une adaptation, un résumé, un PNJ inspiré de la source…"
|
||||
(keydown.enter)="$event.preventDefault(); send()"></textarea>
|
||||
<button class="btn-deep" (click)="send(true)" [disabled]="sending || !draft.trim()"
|
||||
title="Analyse approfondie : lit TOUT le document (plus lent, exhaustif). Idéal pour « liste tous les… »">
|
||||
<lucide-icon [img]="Layers" [size]="15"></lucide-icon>
|
||||
</button>
|
||||
<button class="btn-send" (click)="send()" [disabled]="sending || !draft.trim()"
|
||||
title="Réponse rapide (recherche ciblée dans le document)">
|
||||
<lucide-icon [img]="Send" [size]="15"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LucideAngularModule, ArrowLeft, Upload, Trash2, Send, FileText, Loader, CheckCircle2, AlertCircle, Sparkles, Layers } from 'lucide-angular';
|
||||
@@ -20,7 +20,7 @@ import { NotebookActionCardComponent } from '../notebook-action-card/notebook-ac
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-notebook-detail',
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule, NotebookActionCardComponent],
|
||||
imports: [FormsModule, LucideAngularModule, NotebookActionCardComponent],
|
||||
templateUrl: './notebook-detail.component.html',
|
||||
styleUrls: ['./notebook-detail.component.scss']
|
||||
})
|
||||
@@ -108,7 +108,6 @@ export class NotebookDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
/** trackBy stable pour les cartes d'action (évite toute recréation parasite). */
|
||||
trackAction(index: number): number { return index; }
|
||||
|
||||
load(): void {
|
||||
this.service.get(this.notebookId).subscribe({
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<div class="nbl-create">
|
||||
<input type="text" [(ngModel)]="newName" placeholder="Nom de l'atelier (ex: Adaptation du module X)"
|
||||
(keyup.enter)="create()">
|
||||
(keyup.enter)="create()">
|
||||
<button class="btn-create" (click)="create()" [disabled]="creating">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
{{ creating ? 'Création…' : 'Nouvel atelier' }}
|
||||
@@ -23,13 +23,17 @@
|
||||
</div>
|
||||
|
||||
<div class="nbl-list">
|
||||
<p class="empty" *ngIf="notebooks.length === 0">Aucun atelier pour l'instant.</p>
|
||||
<button class="nbl-item" *ngFor="let nb of notebooks" (click)="open(nb)">
|
||||
<lucide-icon [img]="BookOpen" [size]="16"></lucide-icon>
|
||||
<span class="nbl-item-name">{{ nb.name }}</span>
|
||||
<span class="nbl-del" (click)="remove(nb, $event)" title="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</span>
|
||||
</button>
|
||||
@if (notebooks.length === 0) {
|
||||
<p class="empty">Aucun atelier pour l'instant.</p>
|
||||
}
|
||||
@for (nb of notebooks; track nb) {
|
||||
<button class="nbl-item" (click)="open(nb)">
|
||||
<lucide-icon [img]="BookOpen" [size]="16"></lucide-icon>
|
||||
<span class="nbl-item-name">{{ nb.name }}</span>
|
||||
<span class="nbl-del" (click)="remove(nb, $event)" title="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LucideAngularModule, ArrowLeft, Plus, Trash2, BookOpen } from 'lucide-angular';
|
||||
@@ -14,7 +14,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-notebook-list',
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule],
|
||||
imports: [FormsModule, LucideAngularModule],
|
||||
templateUrl: './notebook-list.component.html',
|
||||
styleUrls: ['./notebook-list.component.scss']
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user