| @@ -1,4 +1,22 @@ | |||
| <div class="chat-input-container"> | |||
| <div class="chat-input-container" (drop)="onFileDrop($event)" (dragover)="onDragOver($event)"> | |||
| <div class="attached-files" *ngIf="attachedFiles.length > 0"> | |||
| <div class="file-item" *ngFor="let attachedFile of attachedFiles"> | |||
| <div class="file-preview"> | |||
| <img *ngIf="attachedFile.preview" [src]="attachedFile.preview" alt="Preview"> | |||
| <div class="file-icon" *ngIf="!attachedFile.preview" [innerHTML]="getFileIcon(attachedFile.file.type)"> | |||
| </div> | |||
| </div> | |||
| <div class="file-info"> | |||
| <span class="file-name">{{ attachedFile.file.name }}</span> | |||
| <span class="file-size">{{ formatFileSize(attachedFile.file.size) }}</span> | |||
| </div> | |||
| <button class="remove-file-button" (click)="removeFile(attachedFile.id)" [attr.aria-label]="'Supprimer le fichier'"> | |||
| <fa-icon [icon]="faTimes"></fa-icon> | |||
| </button> | |||
| </div> | |||
| </div> | |||
| <div class="input-wrapper"> | |||
| <textarea | |||
| #messageInput | |||
| @@ -25,13 +43,23 @@ | |||
| <button | |||
| class="send-button" | |||
| (click)="onSend()" | |||
| [disabled]="!message.trim() || disabled" | |||
| [disabled]="(!message.trim() && attachedFiles.length === 0) || disabled" | |||
| [attr.aria-label]="'Envoyer le message'" | |||
| > | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-up" viewBox="0 0 16 16"> | |||
| <path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5"/> | |||
| </svg> </button> | |||
| </svg> | |||
| </button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <input | |||
| #fileInput | |||
| type="file" | |||
| [accept]="acceptedFileTypes" | |||
| [multiple]="true" | |||
| (change)="onFileSelected($event)" | |||
| style="display: none;" | |||
| /> | |||
| </div> | |||
| @@ -3,11 +3,110 @@ | |||
| padding: 16px 24px; | |||
| background: transparent; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| position: relative; | |||
| .attached-files { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 8px; | |||
| padding: 12px 16px; | |||
| max-width: 1000px; | |||
| margin: 0 auto; | |||
| width: 100%; | |||
| .file-item { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 8px; | |||
| padding: 8px 12px; | |||
| background: #ffffff; | |||
| border: 1px solid #e5e7eb; | |||
| border-radius: 8px; | |||
| max-width: 32%; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| border-color: #d1d5db; | |||
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |||
| } | |||
| .file-preview { | |||
| flex-shrink: 0; | |||
| width: 40px; | |||
| height: 40px; | |||
| border-radius: 6px; | |||
| overflow: hidden; | |||
| background: #ffffff; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| border: 1px solid #e5e7eb; | |||
| img { | |||
| width: 100%; | |||
| height: 100%; | |||
| object-fit: cover; | |||
| } | |||
| .file-icon { | |||
| font-size: 20px; | |||
| } | |||
| } | |||
| .file-info { | |||
| flex: 1; | |||
| min-width: 0; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 2px; | |||
| .file-name { | |||
| font-size: 13px; | |||
| font-weight: 500; | |||
| color: #374151; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| .file-size { | |||
| font-size: 11px; | |||
| color: #9ca3af; | |||
| } | |||
| } | |||
| .remove-file-button { | |||
| flex-shrink: 0; | |||
| width: 24px; | |||
| height: 24px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| background: transparent; | |||
| border: none; | |||
| border-radius: 4px; | |||
| color: #9ca3af; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| background: #ffffff; | |||
| color: #ef4444; | |||
| } | |||
| fa-icon { | |||
| font-size: 14px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .input-wrapper { | |||
| width: 100%; | |||
| max-width: 1000px; | |||
| margin: 0 auto; | |||
| background: #ffffff; | |||
| border: 1px solid #e5e7eb; | |||
| border-radius: 16px; | |||
| @@ -18,6 +117,12 @@ | |||
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); | |||
| transition: all 0.2s ease; | |||
| &.has-files { | |||
| border-radius: 0 0 16px 16px; | |||
| border-top: none; | |||
| padding-top: 12px; | |||
| } | |||
| &:focus-within { | |||
| border-color: #3498db; | |||
| box-shadow: 0 4px 12px rgba(52, 152, 219, 0.12); | |||
| @@ -104,8 +209,9 @@ | |||
| color: #1f2937; | |||
| } | |||
| fa-icon { | |||
| font-size: 18px; | |||
| svg { | |||
| width: 18px; | |||
| height: 18px; | |||
| } | |||
| } | |||
| @@ -124,7 +230,7 @@ | |||
| &:hover:not(:disabled) { | |||
| transform: scale(1.05); | |||
| box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); | |||
| box-shadow: 0 4px 12px rgba(246, 62, 99, 0.3); | |||
| } | |||
| &:active:not(:disabled) { | |||
| @@ -136,18 +242,42 @@ | |||
| cursor: not-allowed; | |||
| opacity: 0.6; | |||
| fa-icon { | |||
| svg { | |||
| color: #9ca3af; | |||
| } | |||
| } | |||
| fa-icon { | |||
| font-size: 16px; | |||
| svg { | |||
| width: 16px; | |||
| height: 16px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| input[type="file"] { | |||
| display: none; | |||
| } | |||
| } | |||
| .chat-input-container.drag-over { | |||
| &::before { | |||
| content: 'Déposez vos fichiers ici'; | |||
| position: absolute; | |||
| inset: 0; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| border: 2px dashed #3498db; | |||
| border-radius: 16px; | |||
| background: rgba(52, 152, 219, 0.05); | |||
| color: #3498db; | |||
| font-size: 16px; | |||
| font-weight: 600; | |||
| pointer-events: none; | |||
| z-index: 10; | |||
| } | |||
| } | |||
| // Responsive | |||
| @@ -155,10 +285,49 @@ | |||
| .chat-input-container { | |||
| padding: 12px 16px; | |||
| .attached-files { | |||
| padding: 10px 12px; | |||
| border-radius: 10px 10px 0 0; | |||
| gap: 6px; | |||
| .file-item { | |||
| padding: 6px 10px; | |||
| max-width: 200px; | |||
| .file-preview { | |||
| width: 36px; | |||
| height: 36px; | |||
| } | |||
| .file-info { | |||
| .file-name { | |||
| font-size: 12px; | |||
| } | |||
| .file-size { | |||
| font-size: 10px; | |||
| } | |||
| } | |||
| .remove-file-button { | |||
| width: 20px; | |||
| height: 20px; | |||
| fa-icon { | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .input-wrapper { | |||
| padding: 12px 16px; | |||
| border-radius: 14px; | |||
| &.has-files { | |||
| border-radius: 0 0 14px 14px; | |||
| } | |||
| textarea { | |||
| font-size: 14px; | |||
| } | |||
| @@ -180,8 +349,9 @@ | |||
| width: 32px; | |||
| height: 32px; | |||
| fa-icon { | |||
| font-size: 15px; | |||
| svg { | |||
| width: 15px; | |||
| height: 15px; | |||
| } | |||
| } | |||
| } | |||
| @@ -196,3 +366,18 @@ | |||
| overflow-y: auto; | |||
| } | |||
| } | |||
| @keyframes fileSlideIn { | |||
| from { | |||
| opacity: 0; | |||
| transform: translateY(-10px); | |||
| } | |||
| to { | |||
| opacity: 1; | |||
| transform: translateY(0); | |||
| } | |||
| } | |||
| .attached-files .file-item { | |||
| animation: fileSlideIn 0.3s ease; | |||
| } | |||
| @@ -1,12 +1,16 @@ | |||
| import { Component, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import { FormsModule } from '@angular/forms'; | |||
| import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | |||
| import { | |||
| // chat-input.ts | |||
| import {Component, Output, EventEmitter, Input, ViewChild, ElementRef} from '@angular/core'; | |||
| import {CommonModule} from '@angular/common'; | |||
| import {FormsModule} from '@angular/forms'; | |||
| import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; | |||
| import {faPaperclip, faTimes} from '@fortawesome/free-solid-svg-icons'; | |||
| import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | |||
| faPaperclip, | |||
| } from '@fortawesome/free-solid-svg-icons'; | |||
| interface AttachedFile { | |||
| file: File; | |||
| id: string; | |||
| preview?: string; | |||
| } | |||
| @Component({ | |||
| selector: 'app-chat-input', | |||
| @@ -17,21 +21,44 @@ import { | |||
| }) | |||
| export class ChatInput { | |||
| @Input() disabled = false; | |||
| @Output() messageSent = new EventEmitter<string>(); | |||
| @Output() fileAttached = new EventEmitter<void>(); | |||
| @Output() messageSent = new EventEmitter<{ message: string; files: File[] }>(); | |||
| @Output() fileAttached = new EventEmitter<File[]>(); | |||
| @Output() voiceActivated = new EventEmitter<void>(); | |||
| @ViewChild('messageInput') messageInput!: ElementRef<HTMLTextAreaElement>; | |||
| @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>; | |||
| message = ''; | |||
| attachedFiles: AttachedFile[] = []; | |||
| faPaperclip = faPaperclip; | |||
| faTimes = faTimes; | |||
| acceptedFileTypes = [ | |||
| 'image/*', | |||
| 'application/pdf', | |||
| 'application/msword', | |||
| 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | |||
| 'application/vnd.ms-excel', | |||
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | |||
| 'text/*' | |||
| ].join(','); | |||
| maxFileSize = 10 * 1024 * 1024; // 10MB | |||
| maxFiles = 10; | |||
| constructor(private sanitizer: DomSanitizer) {} | |||
| onSend(): void { | |||
| if (this.message.trim() && !this.disabled) { | |||
| this.messageSent.emit(this.message.trim()); | |||
| if ((this.message.trim() || this.attachedFiles.length > 0) && !this.disabled) { | |||
| const files = this.attachedFiles.map(af => af.file); | |||
| this.messageSent.emit({ | |||
| message: this.message.trim(), | |||
| files: files | |||
| }); | |||
| this.message = ''; | |||
| this.attachedFiles = []; | |||
| this.adjustTextareaHeight(); | |||
| } | |||
| } | |||
| onInputChange(): void { | |||
| const textarea = this.messageInput.nativeElement; | |||
| textarea.style.height = 'auto'; | |||
| @@ -39,7 +66,109 @@ export class ChatInput { | |||
| } | |||
| onAttach(): void { | |||
| this.fileAttached.emit(); | |||
| this.fileInput.nativeElement.click(); | |||
| } | |||
| onFileSelected(event: Event): void { | |||
| const input = event.target as HTMLInputElement; | |||
| if (input.files && input.files.length > 0) { | |||
| this.handleFiles(Array.from(input.files)); | |||
| input.value = ''; | |||
| } | |||
| } | |||
| onFileDrop(event: DragEvent): void { | |||
| event.preventDefault(); | |||
| event.stopPropagation(); | |||
| if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) { | |||
| this.handleFiles(Array.from(event.dataTransfer.files)); | |||
| } | |||
| } | |||
| onDragOver(event: DragEvent): void { | |||
| event.preventDefault(); | |||
| event.stopPropagation(); | |||
| } | |||
| private handleFiles(files: File[]): void { | |||
| if (this.attachedFiles.length + files.length > this.maxFiles) { | |||
| alert(`Vous ne pouvez joindre que ${this.maxFiles} fichiers maximum.`); | |||
| return; | |||
| } | |||
| files.forEach(file => { | |||
| // Vérifier la taille du fichier | |||
| if (file.size > this.maxFileSize) { | |||
| alert(`Le fichier "${file.name}" dépasse la taille maximale de 10MB.`); | |||
| return; | |||
| } | |||
| const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |||
| const attachedFile: AttachedFile = { | |||
| file: file, | |||
| id: id | |||
| }; | |||
| if (file.type.startsWith('image/')) { | |||
| const reader = new FileReader(); | |||
| reader.onload = (e) => { | |||
| attachedFile.preview = e.target?.result as string; | |||
| }; | |||
| reader.readAsDataURL(file); | |||
| } | |||
| this.attachedFiles.push(attachedFile); | |||
| }); | |||
| this.fileAttached.emit(files); | |||
| } | |||
| removeFile(id: string): void { | |||
| this.attachedFiles = this.attachedFiles.filter(f => f.id !== id); | |||
| } | |||
| getFileIcon(type: string): SafeHtml { | |||
| let svgIcon = ''; | |||
| if (type.startsWith('image/')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-image" viewBox="0 0 16 16"> | |||
| <path d="M8.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/> | |||
| <path d="M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M3 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v8l-2.083-2.083a.5.5 0 0 0-.76.063L8 11 5.835 9.7a.5.5 0 0 0-.611.076L3 12z"/> | |||
| </svg>`; | |||
| } else if (type.includes('pdf')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-filetype-pdf" viewBox="0 0 16 16"> | |||
| <path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM1.6 11.85H0v3.999h.791v-1.342h.803q.43 0 .732-.173.305-.175.463-.474a1.4 1.4 0 0 0 .161-.677q0-.375-.158-.677a1.2 1.2 0 0 0-.46-.477q-.3-.18-.732-.179m.545 1.333a.8.8 0 0 1-.085.38.57.57 0 0 1-.238.241.8.8 0 0 1-.375.082H.788V12.48h.66q.327 0 .512.181.185.183.185.522m1.217-1.333v3.999h1.46q.602 0 .998-.237a1.45 1.45 0 0 0 .595-.689q.196-.45.196-1.084 0-.63-.196-1.075a1.43 1.43 0 0 0-.589-.68q-.396-.234-1.005-.234zm.791.645h.563q.371 0 .609.152a.9.9 0 0 1 .354.454q.118.302.118.753a2.3 2.3 0 0 1-.068.592 1.1 1.1 0 0 1-.196.422.8.8 0 0 1-.334.252 1.3 1.3 0 0 1-.483.082h-.563zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638z"/> | |||
| </svg>`; | |||
| } else if (type.includes('word') || type.includes('document')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-filetype-doc" viewBox="0 0 16 16"> | |||
| <path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM.5 11.85h1.6q.434 0 .732.179.302.175.46.477.158.302.158.677t-.158.677q-.159.3-.464.474a1.45 1.45 0 0 1-.732.173H.5zm.791 1.984v1.328H.5v-3.999h1.915q.493 0 .729.174.237.173.377.472.14.3.14.677 0 .374-.14.677-.137.299-.375.472-.236.173-.728.173zm4.035-3.306h1.6q.434 0 .732.179.302.175.46.477.158.302.158.677t-.158.677q-.159.3-.464.474a1.45 1.45 0 0 1-.732.173h-1.6zm.791 1.984v1.328h-.791v-3.999h1.915q.493 0 .729.174.237.173.377.472.14.3.14.677 0 .374-.14.677-.137.299-.375.472-.236.173-.728.173z"/> | |||
| </svg>`; | |||
| } else if (type.includes('excel') || type.includes('sheet') || type.includes('spreadsheet')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-filetype-xls" viewBox="0 0 16 16"> | |||
| <path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM6.472 15.29a1.2 1.2 0 0 1-.111-.449h.765a.58.58 0 0 0 .254.384q.106.073.25.114.143.041.319.041.246 0 .413-.07a.56.56 0 0 0 .255-.193.5.5 0 0 0 .085-.29.39.39 0 0 0-.153-.326q-.152-.12-.462-.193l-.619-.143a1.7 1.7 0 0 1-.539-.214 1 1 0 0 1-.351-.367 1.1 1.1 0 0 1-.123-.524q0-.366.19-.639.19-.272.527-.422.338-.15.777-.149.457 0 .78.152.324.153.5.41.18.255.2.566h-.75a.56.56 0 0 0-.12-.258.6.6 0 0 0-.247-.181.9.9 0 0 0-.369-.068q-.325 0-.513.152a.47.47 0 0 0-.184.384q0 .18.143.3a1 1 0 0 0 .405.175l.62.143q.326.075.566.211a1 1 0 0 1 .375.358q.135.222.135.56 0 .37-.188.656a1.2 1.2 0 0 1-.539.439q-.351.158-.858.158-.381 0-.665-.09a1.4 1.4 0 0 1-.478-.252 1.1 1.1 0 0 1-.29-.375m-2.945-3.358h-.893L1.81 13.37h-.036l-.832-1.438h-.93l1.227 1.983L0 15.931h.861l.853-1.415h.035l.85 1.415h.908L2.253 13.94zm2.727 3.325H4.557v-3.325h-.79v4h2.487z"/> | |||
| </svg>`; | |||
| } else if (type.startsWith('text/')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-text" viewBox="0 0 16 16"> | |||
| <path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/> | |||
| <path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/> | |||
| </svg>`; | |||
| } else { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark" viewBox="0 0 16 16"> | |||
| <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/> | |||
| </svg>`; | |||
| } | |||
| return this.sanitizer.bypassSecurityTrustHtml(svgIcon); | |||
| } | |||
| formatFileSize(bytes: number): string { | |||
| if (bytes === 0) return '0 Bytes'; | |||
| const k = 1024; | |||
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |||
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |||
| return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; | |||
| } | |||
| onVoice(): void { | |||
| @@ -93,49 +93,63 @@ export const MOCK_CONVERSATIONS: Conversation[] = [ | |||
| timestamp: new Date('2024-11-20T10:39:20'), | |||
| } | |||
| ], | |||
| lastUpdate: new Date('2024-11-20T10:39:20'), | |||
| createdAt: new Date('2024-11-20T10:39:20'), | |||
| updatedAt: new Date('2024-11-20T10:39:20'), | |||
| agentId: 'audit' | |||
| }, | |||
| { | |||
| id: 'conv2', | |||
| title: 'Demande de congé pour le mois de Janvier', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-23T15:00:30'), | |||
| createdAt: new Date('2024-11-23T15:00:30'), | |||
| updatedAt: new Date('2024-11-23T15:00:30'), | |||
| agentId: 'rh' | |||
| }, | |||
| { | |||
| id: 'conv3', | |||
| title: 'Audit des documents fiscaux pour l\'année N-1', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-18T09:15:00'), | |||
| createdAt: new Date('2024-11-18T09:15:00'), | |||
| updatedAt: new Date('2024-11-18T09:15:00'), | |||
| agentId: 'audit-docs' | |||
| }, | |||
| { | |||
| id: 'conv4', | |||
| title: 'Problème de connexion VPN sur mobile', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-24T10:45:35'), | |||
| createdAt: new Date('2024-11-24T10:45:35'), | |||
| updatedAt: new Date('2024-11-24T10:45:35'), | |||
| agentId: 'it' | |||
| }, | |||
| { | |||
| id: 'conv5', | |||
| title: 'Rédaction d\'une clause de non-concurrence', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-22T14:01:15'), | |||
| createdAt: new Date('2024-11-22T14:01:15'), | |||
| updatedAt: new Date('2024-11-22T14:01:15'), | |||
| agentId: 'legal' | |||
| }, | |||
| { | |||
| id: 'conv6', | |||
| title: 'Demande d\'information générale', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-17T16:45:00'), | |||
| createdAt: new Date('2024-11-17T16:45:00'), | |||
| updatedAt: new Date('2024-11-17T16:45:00'), | |||
| agentId: 'default' | |||
| }, | |||
| { | |||
| id: 'conv7', | |||
| title: 'Préparation du rapport annuel des ventes', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-15T11:00:00'), | |||
| createdAt: new Date('2024-11-15T11:00:00'), | |||
| updatedAt: new Date('2024-11-15T11:00:00'), | |||
| agentId: 'audit' | |||
| } | |||
| ]; | |||
| @@ -149,21 +163,24 @@ export const MOCK_PROJECTS: Project[] = [ | |||
| id: 'proj1-conv1', | |||
| title: 'Budget prévisionnel 2025', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-22T09:00:00'), | |||
| createdAt: new Date('2024-11-22T09:00:00'), | |||
| updatedAt: new Date('2024-11-22T09:00:00'), | |||
| agentId: 'audit' | |||
| }, | |||
| { | |||
| id: 'proj1-conv2', | |||
| title: 'Analyse des dépenses Q1-Q3', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-21T14:30:00'), | |||
| createdAt: new Date('2024-11-21T14:30:00'), | |||
| updatedAt: new Date('2024-11-21T14:30:00'), | |||
| agentId: 'audit' | |||
| }, | |||
| { | |||
| id: 'proj1-conv3', | |||
| title: 'Optimisation des coûts', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-20T11:15:00'), | |||
| createdAt: new Date('2024-11-20T11:15:00'), | |||
| updatedAt: new Date('2024-11-20T11:15:00'), | |||
| agentId: 'default' | |||
| } | |||
| ], | |||
| @@ -177,21 +194,24 @@ export const MOCK_PROJECTS: Project[] = [ | |||
| id: 'proj2-conv1', | |||
| title: 'Rapport mensuel Novembre', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-24T16:00:00'), | |||
| createdAt: new Date('2024-11-24T16:00:00'), | |||
| updatedAt: new Date('2024-11-24T16:00:00'), | |||
| agentId: 'audit' | |||
| }, | |||
| { | |||
| id: 'proj2-conv2', | |||
| title: 'Bilan trimestriel Q4 (en cours)', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-23T10:45:00'), | |||
| createdAt: new Date('2024-11-23T10:45:00'), | |||
| updatedAt: new Date('2024-11-23T10:45:00'), | |||
| agentId: 'audit' | |||
| }, | |||
| { | |||
| id: 'proj2-conv3', | |||
| title: 'Validation des écritures 2024', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-20T11:00:00'), | |||
| createdAt: new Date('2024-11-20T11:00:00'), | |||
| updatedAt: new Date('2024-11-20T11:00:00'), | |||
| agentId: 'audit-docs' | |||
| } | |||
| ], | |||
| @@ -205,21 +225,24 @@ export const MOCK_PROJECTS: Project[] = [ | |||
| id: 'proj3-conv1', | |||
| title: 'Audit fiscal Décembre', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-23T13:20:00'), | |||
| createdAt: new Date('2024-11-23T13:20:00'), | |||
| updatedAt: new Date('2024-11-23T13:20:00'), | |||
| agentId: 'audit-docs' | |||
| }, | |||
| { | |||
| id: 'proj3-conv2', | |||
| title: 'Vérification des comptes fournisseurs', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-22T15:30:00'), | |||
| createdAt: new Date('2024-11-22T15:30:00'), | |||
| updatedAt: new Date('2024-11-22T15:30:00'), | |||
| agentId: 'audit-docs' | |||
| }, | |||
| { | |||
| id: 'proj3-conv3', | |||
| title: 'Contrôle des procédures internes', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-21T09:00:00'), | |||
| createdAt: new Date('2024-11-21T09:00:00'), | |||
| updatedAt: new Date('2024-11-21T09:00:00'), | |||
| agentId: 'audit' | |||
| } | |||
| ], | |||
| @@ -233,14 +256,16 @@ export const MOCK_PROJECTS: Project[] = [ | |||
| id: 'proj4-conv1', | |||
| title: 'Grille salariale 2025', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-24T11:00:00'), | |||
| createdAt: new Date('2024-11-24T11:00:00'), | |||
| updatedAt: new Date('2024-11-24T11:00:00'), | |||
| agentId: 'rh' | |||
| }, | |||
| { | |||
| id: 'proj4-conv2', | |||
| title: 'Politique de télétravail', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-21T14:15:00'), | |||
| updatedAt: new Date('2024-11-21T14:15:00'), | |||
| createdAt: new Date('2024-11-21T14:15:00'), | |||
| agentId: 'rh' | |||
| } | |||
| ], | |||
| @@ -254,14 +279,16 @@ export const MOCK_PROJECTS: Project[] = [ | |||
| id: 'proj5-conv1', | |||
| title: 'Configuration nouvelle imprimante réseau', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-23T10:00:00'), | |||
| createdAt: new Date('2024-11-23T10:00:00'), | |||
| updatedAt: new Date('2024-11-23T10:00:00'), | |||
| agentId: 'it' | |||
| }, | |||
| { | |||
| id: 'proj5-conv2', | |||
| title: 'Mise à jour des systèmes d\'exploitation', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-20T17:00:00'), | |||
| createdAt: new Date('2024-11-20T17:00:00'), | |||
| updatedAt: new Date('2024-11-20T17:00:00'), | |||
| agentId: 'it' | |||
| } | |||
| ], | |||
| @@ -275,14 +302,17 @@ export const MOCK_PROJECTS: Project[] = [ | |||
| id: 'proj6-conv1', | |||
| title: 'Vérification des contrats fournisseurs', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-24T09:30:00'), | |||
| createdAt: new Date('2024-11-24T09:30:00'), | |||
| updatedAt: new Date('2024-11-24T09:30:00'), | |||
| agentId: 'legal' | |||
| }, | |||
| { | |||
| id: 'proj6-conv2', | |||
| title: 'Accord de confidentialité', | |||
| messages: [], | |||
| lastUpdate: new Date('2024-11-21T12:00:00'), | |||
| createdAt: new Date('2024-11-21T12:00:00'), | |||
| updatedAt: new Date('2024-11-21T12:00:00'), | |||
| agentId: 'legal' | |||
| } | |||
| ], | |||
| @@ -10,14 +10,33 @@ export interface Message { | |||
| content: string; | |||
| sender: 'user' | 'agent'; | |||
| timestamp: Date; | |||
| files?: AttachedFile[]; | |||
| } | |||
| export interface AttachedFile { | |||
| name: string; | |||
| size: number; | |||
| type: string; | |||
| url?: string; | |||
| preview?: string; | |||
| } | |||
| export interface Conversation { | |||
| id: string; | |||
| title: string; | |||
| agentId: string; | |||
| messages: Message[]; | |||
| createdAt: Date; | |||
| updatedAt: Date; | |||
| } | |||
| export interface Conversation { | |||
| id: string; | |||
| title: string; | |||
| messages: Message[]; | |||
| lastUpdate: Date; | |||
| agentId: string; | |||
| createdAt: Date; | |||
| updatedAt: Date; | |||
| } | |||
| export interface Project { | |||
| @@ -10,7 +10,29 @@ | |||
| <!-- Message de l'utilisateur --> | |||
| <div class="message-content" *ngIf="message.sender === 'user'"> | |||
| <div class="message-bubble user-bubble"> | |||
| <p>{{ message.content }}</p> | |||
| <!-- Fichiers attachés --> | |||
| <div class="message-files" *ngIf="message.files && message.files.length > 0"> | |||
| <div class="file-item" *ngFor="let file of message.files"> | |||
| <div class="file-preview" *ngIf="file.preview"> | |||
| <img [src]="file.preview" [alt]="file.name"> | |||
| </div> | |||
| <div class="file-icon" *ngIf="!file.preview" [innerHTML]="getFileIcon(file.type)"> | |||
| </div> | |||
| <div class="file-details"> | |||
| <span class="file-name">{{ file.name }}</span> | |||
| <span class="file-size">{{ formatFileSize(file.size) }}</span> | |||
| </div> | |||
| <a *ngIf="file.url" [href]="file.url" download [attr.aria-label]="'Télécharger ' + file.name" class="file-download"> | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> | |||
| <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/> | |||
| <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/> | |||
| </svg> | |||
| </a> | |||
| </div> | |||
| </div> | |||
| <!-- Texte du message --> | |||
| <p *ngIf="message.content">{{ message.content }}</p> | |||
| </div> | |||
| </div> | |||
| @@ -38,8 +60,7 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" | |||
| class="bi bi-arrow-clockwise" viewBox="0 0 16 16"> | |||
| <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/> | |||
| <path | |||
| d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/> | |||
| <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/> | |||
| </svg> | |||
| </button> | |||
| </div> | |||
| @@ -2,7 +2,6 @@ | |||
| display: none; | |||
| height: 100%; | |||
| flex-direction: column; | |||
| background: #f5f7fa; | |||
| opacity: 0; | |||
| transform: translateY(20px); | |||
| transition: all 0.4s ease; | |||
| @@ -81,6 +80,117 @@ | |||
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); | |||
| transition: all 0.2s ease; | |||
| .message-files { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 8px; | |||
| margin-bottom: 12px; | |||
| .file-item { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 10px; | |||
| padding: 10px; | |||
| background: rgba(0, 0, 0, 0.03); | |||
| border-radius: 8px; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| background: rgba(0, 0, 0, 0.05); | |||
| } | |||
| .file-preview { | |||
| flex-shrink: 0; | |||
| width: 50px; | |||
| height: 50px; | |||
| border-radius: 6px; | |||
| overflow: hidden; | |||
| background: #ffffff; | |||
| img { | |||
| width: 100%; | |||
| height: 100%; | |||
| object-fit: cover; | |||
| } | |||
| } | |||
| .file-icon { | |||
| flex-shrink: 0; | |||
| width: 50px; | |||
| height: 50px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| font-size: 24px; | |||
| background: #ffffff; | |||
| border-radius: 6px; | |||
| } | |||
| .file-details { | |||
| flex: 1; | |||
| min-width: 0; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 2px; | |||
| .file-name { | |||
| font-size: 13px; | |||
| font-weight: 500; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| .file-size { | |||
| font-size: 11px; | |||
| opacity: 0.7; | |||
| } | |||
| } | |||
| .file-download { | |||
| flex-shrink: 0; | |||
| width: 32px; | |||
| height: 32px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| background: rgba(0, 0, 0, 0.05); | |||
| border-radius: 6px; | |||
| color: inherit; | |||
| text-decoration: none; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| background: rgba(0, 0, 0, 0.1); | |||
| } | |||
| svg { | |||
| width: 16px; | |||
| height: 16px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // Message utilisateur avec fichiers | |||
| &.user-bubble { | |||
| .message-files .file-item { | |||
| background: rgba(255, 255, 255, 0.2); | |||
| &:hover { | |||
| background: rgba(255, 255, 255, 0.3); | |||
| } | |||
| .file-download { | |||
| background: rgba(255, 255, 255, 0.2); | |||
| &:hover { | |||
| background: rgba(255, 255, 255, 0.3); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &:hover { | |||
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |||
| } | |||
| @@ -181,8 +291,6 @@ | |||
| .chat-input-container { | |||
| padding: 16px 24px 24px; | |||
| background: #f5f7fa; | |||
| border-top: 1px solid #e5e7eb; | |||
| app-chat-input { | |||
| max-width: 800px; | |||
| @@ -2,8 +2,9 @@ import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterVie | |||
| import { CommonModule } from '@angular/common'; | |||
| import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | |||
| import { faCopy, faThumbsUp, faThumbsDown, faRotateRight } from '@fortawesome/free-solid-svg-icons'; | |||
| import { ChatInput } from '../../components/chat-input/chat-input'; | |||
| import { Conversation } from '../../models/data.model'; | |||
| import {ChatInput} from '../../components/chat-input/chat-input'; | |||
| import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | |||
| @Component({ | |||
| selector: 'app-chat-view', | |||
| @@ -19,18 +20,17 @@ export class ChatView implements AfterViewChecked { | |||
| @Input() isTyping = false; | |||
| @Input() agentName = 'Assistant'; | |||
| @Output() messageSent = new EventEmitter<string>(); | |||
| @Output() messageSent = new EventEmitter<{ message: string; files: File[] }>(); | |||
| @Output() attach = new EventEmitter<void>(); | |||
| @Output() voice = new EventEmitter<void>(); | |||
| @Output() copyMessage = new EventEmitter<string>(); | |||
| @Output() regenerateMessage = new EventEmitter<void>(); | |||
| // Icons | |||
| faCopy = faCopy; | |||
| private shouldScrollToBottom = false; | |||
| constructor(private sanitizer: DomSanitizer) {} | |||
| ngAfterViewChecked(): void { | |||
| if (this.shouldScrollToBottom) { | |||
| this.scrollToBottom(); | |||
| @@ -38,8 +38,8 @@ export class ChatView implements AfterViewChecked { | |||
| } | |||
| } | |||
| onMessageSent(message: string): void { | |||
| this.messageSent.emit(message); | |||
| onMessageSent(data: { message: string; files: File[] }): void { | |||
| this.messageSent.emit(data); | |||
| this.shouldScrollToBottom = true; | |||
| } | |||
| @@ -60,6 +60,48 @@ export class ChatView implements AfterViewChecked { | |||
| this.regenerateMessage.emit(); | |||
| } | |||
| getFileIcon(type: string): SafeHtml { | |||
| let svgIcon = ''; | |||
| if (type.startsWith('image/')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-image" viewBox="0 0 16 16"> | |||
| <path d="M8.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/> | |||
| <path d="M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M3 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v8l-2.083-2.083a.5.5 0 0 0-.76.063L8 11 5.835 9.7a.5.5 0 0 0-.611.076L3 12z"/> | |||
| </svg>`; | |||
| } else if (type.includes('pdf')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-filetype-pdf" viewBox="0 0 16 16"> | |||
| <path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM1.6 11.85H0v3.999h.791v-1.342h.803q.43 0 .732-.173.305-.175.463-.474a1.4 1.4 0 0 0 .161-.677q0-.375-.158-.677a1.2 1.2 0 0 0-.46-.477q-.3-.18-.732-.179m.545 1.333a.8.8 0 0 1-.085.38.57.57 0 0 1-.238.241.8.8 0 0 1-.375.082H.788V12.48h.66q.327 0 .512.181.185.183.185.522m1.217-1.333v3.999h1.46q.602 0 .998-.237a1.45 1.45 0 0 0 .595-.689q.196-.45.196-1.084 0-.63-.196-1.075a1.43 1.43 0 0 0-.589-.68q-.396-.234-1.005-.234zm.791.645h.563q.371 0 .609.152a.9.9 0 0 1 .354.454q.118.302.118.753a2.3 2.3 0 0 1-.068.592 1.1 1.1 0 0 1-.196.422.8.8 0 0 1-.334.252 1.3 1.3 0 0 1-.483.082h-.563zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638z"/> | |||
| </svg>`; | |||
| } else if (type.includes('word') || type.includes('document')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-filetype-doc" viewBox="0 0 16 16"> | |||
| <path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM.5 11.85h1.6q.434 0 .732.179.302.175.46.477.158.302.158.677t-.158.677q-.159.3-.464.474a1.45 1.45 0 0 1-.732.173H.5zm.791 1.984v1.328H.5v-3.999h1.915q.493 0 .729.174.237.173.377.472.14.3.14.677 0 .374-.14.677-.137.299-.375.472-.236.173-.728.173zm4.035-3.306h1.6q.434 0 .732.179.302.175.46.477.158.302.158.677t-.158.677q-.159.3-.464.474a1.45 1.45 0 0 1-.732.173h-1.6zm.791 1.984v1.328h-.791v-3.999h1.915q.493 0 .729.174.237.173.377.472.14.3.14.677 0 .374-.14.677-.137.299-.375.472-.236.173-.728.173z"/> | |||
| </svg>`; | |||
| } else if (type.includes('excel') || type.includes('sheet') || type.includes('spreadsheet')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-filetype-xls" viewBox="0 0 16 16"> | |||
| <path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM6.472 15.29a1.2 1.2 0 0 1-.111-.449h.765a.58.58 0 0 0 .254.384q.106.073.25.114.143.041.319.041.246 0 .413-.07a.56.56 0 0 0 .255-.193.5.5 0 0 0 .085-.29.39.39 0 0 0-.153-.326q-.152-.12-.462-.193l-.619-.143a1.7 1.7 0 0 1-.539-.214 1 1 0 0 1-.351-.367 1.1 1.1 0 0 1-.123-.524q0-.366.19-.639.19-.272.527-.422.338-.15.777-.149.457 0 .78.152.324.153.5.41.18.255.2.566h-.75a.56.56 0 0 0-.12-.258.6.6 0 0 0-.247-.181.9.9 0 0 0-.369-.068q-.325 0-.513.152a.47.47 0 0 0-.184.384q0 .18.143.3a1 1 0 0 0 .405.175l.62.143q.326.075.566.211a1 1 0 0 1 .375.358q.135.222.135.56 0 .37-.188.656a1.2 1.2 0 0 1-.539.439q-.351.158-.858.158-.381 0-.665-.09a1.4 1.4 0 0 1-.478-.252 1.1 1.1 0 0 1-.29-.375m-2.945-3.358h-.893L1.81 13.37h-.036l-.832-1.438h-.93l1.227 1.983L0 15.931h.861l.853-1.415h.035l.85 1.415h.908L2.253 13.94zm2.727 3.325H4.557v-3.325h-.79v4h2.487z"/> | |||
| </svg>`; | |||
| } else if (type.startsWith('text/')) { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-text" viewBox="0 0 16 16"> | |||
| <path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/> | |||
| <path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/> | |||
| </svg>`; | |||
| } else { | |||
| svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark" viewBox="0 0 16 16"> | |||
| <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/> | |||
| </svg>`; | |||
| } | |||
| return this.sanitizer.bypassSecurityTrustHtml(svgIcon); | |||
| } | |||
| formatFileSize(bytes: number): string { | |||
| if (bytes === 0) return '0 Bytes'; | |||
| const k = 1024; | |||
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |||
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |||
| return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; | |||
| } | |||
| private scrollToBottom(): void { | |||
| try { | |||
| const messagesElement = this.messagesContainer?.nativeElement; | |||
| @@ -10,17 +10,14 @@ | |||
| flex-direction: column; | |||
| overflow: hidden; | |||
| position: relative; | |||
| background-image: url("/assets/images/image.png"); | |||
| // dashboard.scss (extrait - partie header avec back button) | |||
| .content-header { | |||
| display: flex; | |||
| justify-content: flex-end; | |||
| align-items: center; | |||
| gap: 12px; | |||
| padding: 16px 24px; | |||
| background: rgba(255, 255, 255, 0.9); | |||
| backdrop-filter: blur(10px); | |||
| border-bottom: 1px solid #e5e7eb; | |||
| z-index: 10; | |||
| .back-button { | |||
| @@ -142,7 +139,7 @@ | |||
| animation: fadeInDown 0.6s ease; | |||
| .welcome-title { | |||
| font-size: 48px; | |||
| font-size: 60px; | |||
| font-weight: 700; | |||
| color: #1f2937; | |||
| margin: 0 0 12px 0; | |||
| @@ -154,7 +151,7 @@ | |||
| } | |||
| .welcome-subtitle { | |||
| font-size: 20px; | |||
| font-size: 40px; | |||
| color: #6b7280; | |||
| margin: 0; | |||
| font-weight: 400; | |||
| @@ -6,7 +6,7 @@ import { Sidebar } from '../../components/sidebar/sidebar'; | |||
| import { AgentCard } from '../../components/agent-card/agent-card'; | |||
| import { ChatInput } from '../../components/chat-input/chat-input'; | |||
| import { DataService } from '../../services/data.service'; | |||
| import { Agent, Conversation } from '../../models/data.model'; | |||
| import { Agent, Conversation, AttachedFile } from '../../models/data.model'; | |||
| import {ChatView} from '../chat/chat-view'; | |||
| import {RouterLink} from '@angular/router'; | |||
| @@ -84,15 +84,17 @@ export class Dashboard implements OnInit { | |||
| this.dataService.selectConversation(conversationId); | |||
| } | |||
| onMessageSent(message: string): void { | |||
| onMessageSent(data: { message: string; files: File[] } | string): void { | |||
| const message = typeof data === 'string' ? data : data.message; | |||
| const files = typeof data === 'string' ? [] : data.files; | |||
| if (!this.currentConversation) { | |||
| this.dataService.createNewConversation('default'); | |||
| setTimeout(() => { | |||
| this.sendMessage(message); | |||
| this.sendMessage(message, files); | |||
| }, 100); | |||
| } else { | |||
| this.sendMessage(message); | |||
| this.sendMessage(message, files); | |||
| } | |||
| } | |||
| @@ -147,12 +149,20 @@ export class Dashboard implements OnInit { | |||
| } | |||
| } | |||
| private sendMessage(message: string): void { | |||
| private async sendMessage(message: string, files: File[] = []): Promise<void> { | |||
| if (!this.currentConversation) return; | |||
| const attachedFiles: AttachedFile[] = await this.processFiles(files); | |||
| let messageContent = message; | |||
| if (!messageContent && attachedFiles.length > 0) { | |||
| messageContent = `${attachedFiles.length} fichier(s) joint(s)`; | |||
| } | |||
| this.dataService.addMessage({ | |||
| content: message, | |||
| content: messageContent, | |||
| sender: 'user', | |||
| files: attachedFiles.length > 0 ? attachedFiles : undefined | |||
| }); | |||
| this.isTyping = true; | |||
| @@ -163,7 +173,7 @@ export class Dashboard implements OnInit { | |||
| setTimeout(() => { | |||
| this.dataService.simulateAgentResponse( | |||
| this.currentConversation!.agentId, | |||
| message | |||
| messageContent | |||
| ); | |||
| this.isTyping = false; | |||
| @@ -173,6 +183,49 @@ export class Dashboard implements OnInit { | |||
| }, 1500); | |||
| } | |||
| private async processFiles(files: File[]): Promise<AttachedFile[]> { | |||
| const attachedFiles: AttachedFile[] = []; | |||
| for (const file of files) { | |||
| const attachedFile: AttachedFile = { | |||
| name: file.name, | |||
| size: file.size, | |||
| type: file.type, | |||
| url: URL.createObjectURL(file) | |||
| }; | |||
| if (file.type.startsWith('image/')) { | |||
| try { | |||
| attachedFile.preview = await this.generateImagePreview(file); | |||
| } catch (error) { | |||
| console.error('Erreur lors de la génération de la preview:', error); | |||
| } | |||
| } | |||
| attachedFiles.push(attachedFile); | |||
| } | |||
| return attachedFiles; | |||
| } | |||
| private generateImagePreview(file: File): Promise<string> { | |||
| return new Promise((resolve, reject) => { | |||
| const reader = new FileReader(); | |||
| reader.onload = (e: ProgressEvent<FileReader>) => { | |||
| if (e.target?.result) { | |||
| resolve(e.target.result as string); | |||
| } else { | |||
| reject(new Error('Impossible de lire le fichier')); | |||
| } | |||
| }; | |||
| reader.onerror = () => reject(new Error('Erreur de lecture du fichier')); | |||
| reader.readAsDataURL(file); | |||
| }); | |||
| } | |||
| get hasActiveConversation(): boolean { | |||
| return this.currentConversation !== null && | |||
| this.currentConversation.messages && | |||
| @@ -33,7 +33,8 @@ export class DataService { | |||
| id: this.generateId(), | |||
| title: 'Nouvelle conversation...', | |||
| messages: [], | |||
| lastUpdate: new Date(), | |||
| createdAt: new Date(), | |||
| updatedAt:new Date(), | |||
| agentId | |||
| }; | |||
| @@ -54,7 +55,7 @@ export class DataService { | |||
| }; | |||
| current.messages.push(newMessage); | |||
| current.lastUpdate = new Date(); | |||
| current.updatedAt = new Date(); | |||
| // Update title with first user message | |||
| if (current.messages.length === 1 && message.sender === 'user') { | |||