| <div class="file-item" *ngFor="let attachedFile of attachedFiles"> | <div class="file-item" *ngFor="let attachedFile of attachedFiles"> | ||||
| <div class="file-preview"> | <div class="file-preview"> | ||||
| <img *ngIf="attachedFile.preview" [src]="attachedFile.preview" alt="Preview"> | <img *ngIf="attachedFile.preview" [src]="attachedFile.preview" alt="Preview"> | ||||
| <div class="file-icon" *ngIf="!attachedFile.preview" [innerHTML]="getFileIcon(attachedFile.file.type)"> | |||||
| <div class="file-icon" *ngIf="!attachedFile.preview" [innerHTML]="getFileIcon(attachedFile.file)"> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="file-info"> | <div class="file-info"> | ||||
| <input | <input | ||||
| #fileInput | #fileInput | ||||
| type="file" | type="file" | ||||
| [accept]="acceptedFileTypes" | |||||
| [accept]="getAcceptedTypes()" | |||||
| [multiple]="true" | [multiple]="true" | ||||
| (change)="onFileSelected($event)" | (change)="onFileSelected($event)" | ||||
| style="display: none;" | style="display: none;" |
| // chat-input.ts | |||||
| import {Component, Output, EventEmitter, Input, ViewChild, ElementRef} from '@angular/core'; | |||||
| import {Component, Output, EventEmitter, Input, ViewChild, ElementRef, OnDestroy} from '@angular/core'; | |||||
| import {CommonModule} from '@angular/common'; | import {CommonModule} from '@angular/common'; | ||||
| import {FormsModule} from '@angular/forms'; | import {FormsModule} from '@angular/forms'; | ||||
| import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; | import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; | ||||
| import {faPaperclip, faTimes} from '@fortawesome/free-solid-svg-icons'; | import {faPaperclip, faTimes} from '@fortawesome/free-solid-svg-icons'; | ||||
| import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | |||||
| import {SafeHtml} from '@angular/platform-browser'; | |||||
| import {AttachedFile} from '../../models/data.model'; | |||||
| import {FileHandlerService} from '../../services/file-handler.service'; | |||||
| interface AttachedFile { | |||||
| file: File; | |||||
| id: string; | |||||
| preview?: string; | |||||
| } | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-chat-input', | selector: 'app-chat-input', | ||||
| templateUrl: './chat-input.html', | templateUrl: './chat-input.html', | ||||
| styleUrls: ['./chat-input.scss'] | styleUrls: ['./chat-input.scss'] | ||||
| }) | }) | ||||
| export class ChatInput { | |||||
| export class ChatInput implements OnDestroy { | |||||
| @Input() disabled = false; | @Input() disabled = false; | ||||
| @Output() messageSent = new EventEmitter<{ message: string; files: File[] }>(); | |||||
| @Output() messageSent = new EventEmitter<{ message: string; files: AttachedFile[] }>(); | |||||
| @Output() fileAttached = new EventEmitter<File[]>(); | @Output() fileAttached = new EventEmitter<File[]>(); | ||||
| @Output() voiceActivated = new EventEmitter<void>(); | @Output() voiceActivated = new EventEmitter<void>(); | ||||
| @ViewChild('messageInput') messageInput!: ElementRef<HTMLTextAreaElement>; | @ViewChild('messageInput') messageInput!: ElementRef<HTMLTextAreaElement>; | ||||
| faPaperclip = faPaperclip; | faPaperclip = faPaperclip; | ||||
| faTimes = faTimes; | 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(','); | |||||
| constructor( | |||||
| private fileHandlerService: FileHandlerService | |||||
| ) { | |||||
| } | |||||
| maxFileSize = 10 * 1024 * 1024; // 10MB | |||||
| maxFiles = 10; | |||||
| constructor(private sanitizer: DomSanitizer) {} | |||||
| ngOnDestroy(): void { | |||||
| this.fileHandlerService.cleanupFiles(this.attachedFiles); | |||||
| } | |||||
| onSend(): void { | onSend(): void { | ||||
| if ((this.message.trim() || this.attachedFiles.length > 0) && !this.disabled) { | if ((this.message.trim() || this.attachedFiles.length > 0) && !this.disabled) { | ||||
| const files = this.attachedFiles.map(af => af.file); | |||||
| const filesClone = this.fileHandlerService.cloneAttachedFiles(this.attachedFiles); | |||||
| this.messageSent.emit({ | this.messageSent.emit({ | ||||
| message: this.message.trim(), | message: this.message.trim(), | ||||
| files: files | |||||
| files: filesClone | |||||
| }); | }); | ||||
| this.message = ''; | this.message = ''; | ||||
| this.attachedFiles = []; | this.attachedFiles = []; | ||||
| event.stopPropagation(); | 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.`); | |||||
| private async handleFiles(files: File[]): Promise<void> { | |||||
| const validationResult = this.fileHandlerService.validateFiles( | |||||
| files, | |||||
| this.attachedFiles.length | |||||
| ); | |||||
| if (!validationResult.isValid) { | |||||
| alert(validationResult.error); | |||||
| return; | 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); | |||||
| try { | |||||
| const processedFiles = await this.fileHandlerService.processFiles(files); | |||||
| this.attachedFiles.push(...processedFiles); | |||||
| this.fileAttached.emit(files); | |||||
| } catch (error) { | |||||
| console.error('Erreur lors du traitement des fichiers:', error); | |||||
| alert('Erreur lors du traitement des fichiers'); | |||||
| } | |||||
| } | } | ||||
| removeFile(id: string): void { | removeFile(id: string): void { | ||||
| const file = this.attachedFiles.find(f => f.id === id); | |||||
| if (file) { | |||||
| this.fileHandlerService.cleanupFile(file); | |||||
| } | |||||
| this.attachedFiles = this.attachedFiles.filter(f => f.id !== id); | 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); | |||||
| getFileIcon(file: File): SafeHtml { | |||||
| return this.fileHandlerService.getFileIcon(file); | |||||
| } | } | ||||
| formatFileSize(bytes: number): string { | 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]; | |||||
| return this.fileHandlerService.formatFileSize(bytes); | |||||
| } | } | ||||
| onVoice(): void { | onVoice(): void { | ||||
| textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; | textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; | ||||
| } | } | ||||
| } | } | ||||
| protected getAcceptedTypes() { | |||||
| return this.fileHandlerService.getAcceptedTypes; | |||||
| } | |||||
| } | } |
| } | } | ||||
| export interface AttachedFile { | export interface AttachedFile { | ||||
| name: string; | |||||
| size: number; | |||||
| type: string; | |||||
| url?: string; | |||||
| file: File; | |||||
| id: string; | |||||
| preview?: string; | preview?: string; | ||||
| url?: string; | |||||
| } | } | ||||
| export interface Conversation { | |||||
| id: string; | |||||
| title: string; | |||||
| agentId: string; | |||||
| messages: Message[]; | |||||
| createdAt: Date; | |||||
| updatedAt: Date; | |||||
| } | |||||
| export interface Conversation { | export interface Conversation { | ||||
| id: string; | id: string; |
| <div class="message-files" *ngIf="message.files && message.files.length > 0"> | <div class="message-files" *ngIf="message.files && message.files.length > 0"> | ||||
| <div class="file-item" *ngFor="let file of message.files"> | <div class="file-item" *ngFor="let file of message.files"> | ||||
| <div class="file-preview" *ngIf="file.preview"> | <div class="file-preview" *ngIf="file.preview"> | ||||
| <img [src]="file.preview" [alt]="file.name"> | |||||
| <img [src]="file.preview" [alt]="file.file.name"> | |||||
| </div> | </div> | ||||
| <div class="file-icon" *ngIf="!file.preview" [innerHTML]="getFileIcon(file.type)"> | |||||
| <div class="file-icon" *ngIf="!file.preview" [innerHTML]="getFileIcon(file.file)"> | |||||
| </div> | </div> | ||||
| <div class="file-details"> | <div class="file-details"> | ||||
| <span class="file-name">{{ file.name }}</span> | |||||
| <span class="file-size">{{ formatFileSize(file.size) }}</span> | |||||
| <span class="file-name">{{ file.file.name }}</span> | |||||
| <span class="file-size">{{ formatFileSize(file.file.size) }}</span> | |||||
| </div> | </div> | ||||
| <a *ngIf="file.url" [href]="file.url" download [attr.aria-label]="'Télécharger ' + file.name" class="file-download"> | |||||
| <a *ngIf="file.url" [href]="file.url" download [attr.aria-label]="'Télécharger ' + file.file.name" class="file-download"> | |||||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> | <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="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"/> | <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"/> | ||||
| <div class="chat-input-container"> | <div class="chat-input-container"> | ||||
| <app-chat-input | <app-chat-input | ||||
| (messageSent)="onMessageSent($event)" | (messageSent)="onMessageSent($event)" | ||||
| (attach)="onAttach()" | |||||
| (voice)="onVoice()" | |||||
| ></app-chat-input> | ></app-chat-input> | ||||
| </div> | </div> | ||||
| </div> | </div> |
| import { | import { | ||||
| Component, | Component, | ||||
| Input, | Input, | ||||
| Output, | |||||
| EventEmitter, | |||||
| ViewChild, | ViewChild, | ||||
| ElementRef, | ElementRef, | ||||
| AfterViewChecked, | AfterViewChecked, | ||||
| } from '@angular/core'; | } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | import { CommonModule } from '@angular/common'; | ||||
| import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | ||||
| import { faCopy, faThumbsUp, faThumbsDown, faRotateRight } from '@fortawesome/free-solid-svg-icons'; | |||||
| import { faCopy } from '@fortawesome/free-solid-svg-icons'; | |||||
| import { ChatInput } from '../../components/chat-input/chat-input'; | import { ChatInput } from '../../components/chat-input/chat-input'; | ||||
| import { Conversation } from '../../models/data.model'; | |||||
| import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | |||||
| import {AttachedFile, Conversation} from '../../models/data.model'; | |||||
| import { SafeHtml} from '@angular/platform-browser'; | |||||
| import {Subscription} from 'rxjs'; | import {Subscription} from 'rxjs'; | ||||
| import {DataService} from '../../services/data.service'; | import {DataService} from '../../services/data.service'; | ||||
| import {FileHandlerService} from '../../services/file-handler.service'; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-chat-view', | selector: 'app-chat-view', | ||||
| @Input() conversation: Conversation | null = null; | @Input() conversation: Conversation | null = null; | ||||
| @Input() agentName = 'Assistant'; | @Input() agentName = 'Assistant'; | ||||
| @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>(); | |||||
| faCopy = faCopy; | faCopy = faCopy; | ||||
| private shouldScrollToBottom = false; | private shouldScrollToBottom = false; | ||||
| constructor( | constructor( | ||||
| private sanitizer: DomSanitizer, | |||||
| private dataService: DataService | |||||
| private dataService: DataService, | |||||
| private fileHandlerService: FileHandlerService | |||||
| ) {} | ) {} | ||||
| ngOnInit(): void { | ngOnInit(): void { | ||||
| } | } | ||||
| ngOnDestroy(): void { | ngOnDestroy(): void { | ||||
| this.isTypingSubscription?.unsubscribe(); | this.isTypingSubscription?.unsubscribe(); | ||||
| this.conversation?.messages.forEach(message => { | |||||
| if (message.files) { | |||||
| this.fileHandlerService.cleanupFiles(message.files); | |||||
| } | |||||
| }); | |||||
| } | } | ||||
| onMessageSentV1(data: { message: string; files: File[] }): void { | onMessageSentV1(data: { message: string; files: File[] }): void { | ||||
| this.messageSent.emit(data); | |||||
| this.shouldScrollToBottom = true; | this.shouldScrollToBottom = true; | ||||
| } | } | ||||
| onMessageSent(data: { message: string; files: File[] }): void { | |||||
| async onMessageSent(data: { message: string; files: AttachedFile[] }): Promise<void> { | |||||
| if (!this.conversation) return; | if (!this.conversation) return; | ||||
| const processedFiles = data.files.map(attachedFile => ({ | |||||
| ...attachedFile, | |||||
| url: attachedFile.url || URL.createObjectURL(attachedFile.file) | |||||
| })); | |||||
| //this.messageSent.emit(data); | |||||
| this.dataService.addMessage({ | this.dataService.addMessage({ | ||||
| content: data.message, | content: data.message, | ||||
| sender: 'user', | sender: 'user', | ||||
| files: data.files.length > 0 ? data.files.map(f => ({ | |||||
| name: f.name, | |||||
| size: f.size, | |||||
| type: f.type | |||||
| })) : undefined | |||||
| files: processedFiles.length > 0 ? processedFiles : undefined | |||||
| }); | }); | ||||
| this.dataService.sendMessageToGroq(this.conversation.agentId, data.message); | this.dataService.sendMessageToGroq(this.conversation.agentId, data.message); | ||||
| this.shouldScrollToBottom = true; | this.shouldScrollToBottom = true; | ||||
| } | } | ||||
| onAttach(): void { | |||||
| this.attach.emit(); | |||||
| } | |||||
| onVoice(): void { | |||||
| this.voice.emit(); | |||||
| } | |||||
| onCopyMessage(content: string): void { | onCopyMessage(content: string): void { | ||||
| navigator.clipboard.writeText(content); | navigator.clipboard.writeText(content); | ||||
| this.copyMessage.emit(content); | |||||
| } | } | ||||
| onRegenerateMessageV1(): void { | onRegenerateMessageV1(): void { | ||||
| this.regenerateMessage.emit(); | |||||
| if (!this.conversation || this.conversation.messages.length === 0) { | |||||
| return; | |||||
| } | |||||
| const lastUserMessage = [...this.conversation.messages] | |||||
| .reverse() | |||||
| .find(msg => msg.sender === 'user'); | |||||
| if (lastUserMessage) { | |||||
| // Supprimer la dernière réponse de l'agent | |||||
| const messages = this.conversation.messages; | |||||
| if (messages[messages.length - 1].sender === 'agent') { | |||||
| messages.pop(); | |||||
| } | |||||
| this.isTyping = true; | |||||
| setTimeout(() => { | |||||
| this.dataService.simulateAgentResponse( | |||||
| this.conversation!.agentId, | |||||
| lastUserMessage.content | |||||
| ); | |||||
| this.isTyping = false; | |||||
| }, 1500); | |||||
| } | |||||
| } | } | ||||
| onRegenerateMessage(): void { | onRegenerateMessage(): void { | ||||
| this.dataService.sendMessageToGroq(this.conversation.agentId, lastUserMessage.content); | this.dataService.sendMessageToGroq(this.conversation.agentId, lastUserMessage.content); | ||||
| //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); | |||||
| getFileIcon(file: File): SafeHtml { | |||||
| return this.fileHandlerService.getFileIcon(file) | |||||
| } | } | ||||
| formatFileSize(bytes: number): string { | 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]; | |||||
| return this.fileHandlerService.formatFileSize(bytes); | |||||
| } | } | ||||
| private scrollToBottom(): void { | private scrollToBottom(): void { |
| <div class="center-input-wrapper" [class.animate-down]="currentConversation"> | <div class="center-input-wrapper" [class.animate-down]="currentConversation"> | ||||
| <app-chat-input | <app-chat-input | ||||
| (messageSent)="onMessageSent($event)" | |||||
| (attach)="onAttach()" | |||||
| (voice)="onVoice()" | |||||
| (messageSent)="onMessageSentFromWelcome($event)" | |||||
| ></app-chat-input> | ></app-chat-input> | ||||
| </div> | </div> | ||||
| @if (currentConversation) { | @if (currentConversation) { | ||||
| <app-chat-view | <app-chat-view | ||||
| [conversation]="currentConversation" | [conversation]="currentConversation" | ||||
| [isTyping]="isTyping" | |||||
| [agentName]="currentAgentName" | [agentName]="currentAgentName" | ||||
| (messageSent)="onMessageSent($event)" | |||||
| (attach)="onAttach()" | |||||
| (voice)="onVoice()" | |||||
| (copyMessage)="onCopyMessage($event)" | |||||
| (regenerateMessage)="onRegenerateMessage()" | |||||
| ></app-chat-view> | ></app-chat-view> | ||||
| } | } | ||||
| import { AgentCard } from '../../components/agent-card/agent-card'; | import { AgentCard } from '../../components/agent-card/agent-card'; | ||||
| import { ChatInput } from '../../components/chat-input/chat-input'; | import { ChatInput } from '../../components/chat-input/chat-input'; | ||||
| import { DataService } from '../../services/data.service'; | import { DataService } from '../../services/data.service'; | ||||
| import { Agent, Conversation, AttachedFile } from '../../models/data.model'; | |||||
| import {Agent, AttachedFile, Conversation} from '../../models/data.model'; | |||||
| import {ChatView} from '../chat/chat-view'; | import {ChatView} from '../chat/chat-view'; | ||||
| import {RouterLink} from '@angular/router'; | import {RouterLink} from '@angular/router'; | ||||
| onConversationSelected(conversationId: string): void { | onConversationSelected(conversationId: string): void { | ||||
| this.dataService.selectConversation(conversationId); | this.dataService.selectConversation(conversationId); | ||||
| } | } | ||||
| onMessageSentFromWelcome(data: { message: string; files: AttachedFile[] }): void { | |||||
| const defaultAgentId = this.agents.length > 0 ? this.agents[0].id : 'default'; | |||||
| onMessageSent(data: { message: string; files: File[] } | string): void { | |||||
| const message = typeof data === 'string' ? data : data.message; | |||||
| const files = typeof data === 'string' ? [] : data.files; | |||||
| this.animatingInput = true; | |||||
| if (!this.currentConversation) { | |||||
| this.dataService.createNewConversation('default'); | |||||
| setTimeout(() => { | |||||
| this.dataService.createNewConversation(defaultAgentId); | |||||
| this.animatingInput = false; | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| this.sendMessage(message, files); | |||||
| if (this.chatView) { | |||||
| this.chatView.onMessageSent(data); | |||||
| } | |||||
| }, 100); | }, 100); | ||||
| } else { | |||||
| this.sendMessage(message, files); | |||||
| } | |||||
| }, 600); | |||||
| } | } | ||||
| onToggleSidebar(): void { | onToggleSidebar(): void { | ||||
| this.sidebarCollapsed = !this.sidebarCollapsed; | this.sidebarCollapsed = !this.sidebarCollapsed; | ||||
| } | } | ||||
| onAttach(): void { | |||||
| console.log('Attach file clicked'); | |||||
| } | |||||
| onVoice(): void { | |||||
| console.log('Voice input clicked'); | |||||
| } | |||||
| onCopyMessage(content: string): void { | |||||
| console.log('Message copied:', content); | |||||
| } | |||||
| onRegenerateMessage(): void { | |||||
| if (!this.currentConversation || this.currentConversation.messages.length === 0) { | |||||
| return; | |||||
| } | |||||
| const lastUserMessage = [...this.currentConversation.messages] | |||||
| .reverse() | |||||
| .find(msg => msg.sender === 'user'); | |||||
| if (lastUserMessage) { | |||||
| // Supprimer la dernière réponse de l'agent | |||||
| const messages = this.currentConversation.messages; | |||||
| if (messages[messages.length - 1].sender === 'agent') { | |||||
| messages.pop(); | |||||
| } | |||||
| this.isTyping = true; | |||||
| if (this.chatView) { | |||||
| this.chatView.markScrollNeeded(); | |||||
| } | |||||
| setTimeout(() => { | |||||
| this.dataService.simulateAgentResponse( | |||||
| this.currentConversation!.agentId, | |||||
| lastUserMessage.content | |||||
| ); | |||||
| this.isTyping = false; | |||||
| if (this.chatView) { | |||||
| this.chatView.markScrollNeeded(); | |||||
| } | |||||
| }, 1500); | |||||
| } | |||||
| } | |||||
| private async sendMessage(message: string, files: File[] = []): Promise<void> { | |||||
| /*private async sendMessage(message: string, files: File[] = []): Promise<void> { | |||||
| if (!this.currentConversation) return; | if (!this.currentConversation) return; | ||||
| const attachedFiles: AttachedFile[] = await this.processFiles(files); | const attachedFiles: AttachedFile[] = await this.processFiles(files); | ||||
| for (const file of files) { | for (const file of files) { | ||||
| const attachedFile: AttachedFile = { | const attachedFile: AttachedFile = { | ||||
| name: file.name, | |||||
| size: file.size, | |||||
| type: file.type, | |||||
| file:file, | |||||
| id: | |||||
| url: URL.createObjectURL(file) | url: URL.createObjectURL(file) | ||||
| }; | }; | ||||
| reader.onerror = () => reject(new Error('Erreur de lecture du fichier')); | reader.onerror = () => reject(new Error('Erreur de lecture du fichier')); | ||||
| reader.readAsDataURL(file); | reader.readAsDataURL(file); | ||||
| }); | }); | ||||
| } | |||||
| }*/ | |||||
| get hasActiveConversation(): boolean { | |||||
| return this.currentConversation !== null && | |||||
| this.currentConversation.messages && | |||||
| this.currentConversation.messages.length > 0; | |||||
| } | |||||
| get currentAgentName(): string { | get currentAgentName(): string { | ||||
| if (!this.currentConversation) return ''; | if (!this.currentConversation) return ''; |
| import { Injectable } from '@angular/core'; | |||||
| import { AttachedFile } from '../models/data.model'; | |||||
| import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | |||||
| export interface FileValidationResult { | |||||
| isValid: boolean; | |||||
| error?: string; | |||||
| } | |||||
| export interface FileHandlerConfig { | |||||
| maxFileSize: number; | |||||
| maxFiles: number; | |||||
| acceptedTypes: string[]; | |||||
| } | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class FileHandlerService { | |||||
| private defaultConfig: FileHandlerConfig = { | |||||
| maxFileSize: 10 * 1024 * 1024, // 10MB | |||||
| maxFiles: 10, | |||||
| acceptedTypes: [ | |||||
| 'image/*', | |||||
| 'application/pdf', | |||||
| 'application/msword', | |||||
| 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | |||||
| 'application/vnd.ms-excel', | |||||
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | |||||
| 'text/*' | |||||
| ] | |||||
| }; | |||||
| constructor(private sanitizer: DomSanitizer) { | |||||
| } | |||||
| get getAcceptedTypes(){ | |||||
| return this.defaultConfig.acceptedTypes | |||||
| } | |||||
| validateFile(file: File, config: Partial<FileHandlerConfig> = {}): FileValidationResult { | |||||
| const finalConfig = { ...this.defaultConfig, ...config }; | |||||
| if (file.size > finalConfig.maxFileSize) { | |||||
| return { | |||||
| isValid: false, | |||||
| error: `Le fichier "${file.name}" dépasse la taille maximale de ${this.formatFileSize(finalConfig.maxFileSize)}.` | |||||
| }; | |||||
| } | |||||
| const isTypeAccepted = finalConfig.acceptedTypes.some(acceptedType => { | |||||
| if (acceptedType.endsWith('/*')) { | |||||
| const category = acceptedType.split('/')[0]; | |||||
| return file.type.startsWith(category + '/'); | |||||
| } | |||||
| return file.type === acceptedType; | |||||
| }); | |||||
| if (!isTypeAccepted) { | |||||
| return { | |||||
| isValid: false, | |||||
| error: `Le type de fichier "${file.type}" n'est pas accepté.` | |||||
| }; | |||||
| } | |||||
| return { isValid: true }; | |||||
| } | |||||
| validateFiles( | |||||
| files: File[], | |||||
| existingFilesCount: number = 0, | |||||
| config: Partial<FileHandlerConfig> = {} | |||||
| ): FileValidationResult { | |||||
| const finalConfig = { ...this.defaultConfig, ...config }; | |||||
| if (existingFilesCount + files.length > finalConfig.maxFiles) { | |||||
| return { | |||||
| isValid: false, | |||||
| error: `Vous ne pouvez joindre que ${finalConfig.maxFiles} fichiers maximum.` | |||||
| }; | |||||
| } | |||||
| for (const file of files) { | |||||
| const result = this.validateFile(file, config); | |||||
| if (!result.isValid) { | |||||
| return result; | |||||
| } | |||||
| } | |||||
| return { isValid: true }; | |||||
| } | |||||
| async processFiles(files: File[]): Promise<AttachedFile[]> { | |||||
| const processedFiles: AttachedFile[] = []; | |||||
| for (const file of files) { | |||||
| const attachedFile = await this.processFile(file); | |||||
| processedFiles.push(attachedFile); | |||||
| } | |||||
| return processedFiles; | |||||
| } | |||||
| async processFile(file: File): Promise<AttachedFile> { | |||||
| const id = this.generateFileId(); | |||||
| const url = URL.createObjectURL(file); | |||||
| const attachedFile: AttachedFile = { | |||||
| id, | |||||
| file, | |||||
| url | |||||
| }; | |||||
| if (this.isImage(file)) { | |||||
| try { | |||||
| attachedFile.preview = await this.generateImagePreview(file); | |||||
| } catch (error) { | |||||
| console.error('Erreur lors de la génération de la preview:', error); | |||||
| } | |||||
| } | |||||
| return attachedFile; | |||||
| } | |||||
| 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); | |||||
| }); | |||||
| } | |||||
| cleanupFiles(files: AttachedFile[]): void { | |||||
| files.forEach(file => { | |||||
| if (file.url) { | |||||
| URL.revokeObjectURL(file.url); | |||||
| } | |||||
| }); | |||||
| } | |||||
| cleanupFile(file: AttachedFile): void { | |||||
| if (file.url) { | |||||
| URL.revokeObjectURL(file.url); | |||||
| } | |||||
| } | |||||
| private generateFileId(): string { | |||||
| return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |||||
| } | |||||
| isImage(file: File): boolean { | |||||
| return file.type.startsWith('image/'); | |||||
| } | |||||
| isPdf(file: File): boolean { | |||||
| return file.type === 'application/pdf' || file.type.includes('pdf'); | |||||
| } | |||||
| isWordDocument(file: File): boolean { | |||||
| return file.type.includes('word') || | |||||
| file.type.includes('document') || | |||||
| file.type === 'application/msword' || | |||||
| file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; | |||||
| } | |||||
| isExcelDocument(file: File): boolean { | |||||
| return file.type.includes('excel') || | |||||
| file.type.includes('sheet') || | |||||
| file.type.includes('spreadsheet') || | |||||
| file.type === 'application/vnd.ms-excel' || | |||||
| file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; | |||||
| } | |||||
| isTextFile(file: File): boolean { | |||||
| return file.type.startsWith('text/'); | |||||
| } | |||||
| formatFileSize(bytes: number): string { | |||||
| if (bytes === 0) return '0 Bytes'; | |||||
| const k = 1024; | |||||
| const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | |||||
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |||||
| return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; | |||||
| } | |||||
| cloneAttachedFile(attachedFile: AttachedFile): AttachedFile { | |||||
| return { | |||||
| ...attachedFile, | |||||
| file: attachedFile.file, | |||||
| url: attachedFile.url, | |||||
| preview: attachedFile.preview | |||||
| }; | |||||
| } | |||||
| cloneAttachedFiles(attachedFiles: AttachedFile[]): AttachedFile[] { | |||||
| return attachedFiles.map(file => this.cloneAttachedFile(file)); | |||||
| } | |||||
| getFileIcon(file: File): SafeHtml { | |||||
| let svgIcon = ''; | |||||
| if (this.isImage(file)) { | |||||
| 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 (this.isPdf(file)) { | |||||
| 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 (this.isWordDocument(file)) { | |||||
| 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 (this.isExcelDocument(file)) { | |||||
| 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 (this.isTextFile(file)) { | |||||
| 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); | |||||
| } | |||||
| } |