| @@ -4,7 +4,7 @@ | |||
| <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 class="file-icon" *ngIf="!attachedFile.preview" [innerHTML]="getFileIcon(attachedFile.file)"> | |||
| </div> | |||
| </div> | |||
| <div class="file-info"> | |||
| @@ -57,7 +57,7 @@ | |||
| <input | |||
| #fileInput | |||
| type="file" | |||
| [accept]="acceptedFileTypes" | |||
| [accept]="getAcceptedTypes()" | |||
| [multiple]="true" | |||
| (change)="onFileSelected($event)" | |||
| style="display: none;" | |||
| @@ -1,16 +1,12 @@ | |||
| // 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 {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'; | |||
| 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({ | |||
| selector: 'app-chat-input', | |||
| @@ -19,9 +15,10 @@ interface AttachedFile { | |||
| templateUrl: './chat-input.html', | |||
| styleUrls: ['./chat-input.scss'] | |||
| }) | |||
| export class ChatInput { | |||
| export class ChatInput implements OnDestroy { | |||
| @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() voiceActivated = new EventEmitter<void>(); | |||
| @ViewChild('messageInput') messageInput!: ElementRef<HTMLTextAreaElement>; | |||
| @@ -32,26 +29,22 @@ export class ChatInput { | |||
| 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(','); | |||
| constructor( | |||
| private fileHandlerService: FileHandlerService | |||
| ) { | |||
| } | |||
| maxFileSize = 10 * 1024 * 1024; // 10MB | |||
| maxFiles = 10; | |||
| constructor(private sanitizer: DomSanitizer) {} | |||
| ngOnDestroy(): void { | |||
| this.fileHandlerService.cleanupFiles(this.attachedFiles); | |||
| } | |||
| onSend(): void { | |||
| 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({ | |||
| message: this.message.trim(), | |||
| files: files | |||
| files: filesClone | |||
| }); | |||
| this.message = ''; | |||
| this.attachedFiles = []; | |||
| @@ -91,84 +84,39 @@ export class ChatInput { | |||
| 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; | |||
| } | |||
| 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 { | |||
| const file = this.attachedFiles.find(f => f.id === id); | |||
| if (file) { | |||
| this.fileHandlerService.cleanupFile(file); | |||
| } | |||
| 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 { | |||
| 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 { | |||
| @@ -182,4 +130,9 @@ export class ChatInput { | |||
| textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; | |||
| } | |||
| } | |||
| protected getAcceptedTypes() { | |||
| return this.fileHandlerService.getAcceptedTypes; | |||
| } | |||
| } | |||
| @@ -14,21 +14,12 @@ export interface Message { | |||
| } | |||
| export interface AttachedFile { | |||
| name: string; | |||
| size: number; | |||
| type: string; | |||
| url?: string; | |||
| file: File; | |||
| id: string; | |||
| preview?: string; | |||
| url?: string; | |||
| } | |||
| export interface Conversation { | |||
| id: string; | |||
| title: string; | |||
| agentId: string; | |||
| messages: Message[]; | |||
| createdAt: Date; | |||
| updatedAt: Date; | |||
| } | |||
| export interface Conversation { | |||
| id: string; | |||
| @@ -14,15 +14,15 @@ | |||
| <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"> | |||
| <img [src]="file.preview" [alt]="file.file.name"> | |||
| </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 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> | |||
| <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"> | |||
| <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"/> | |||
| @@ -84,8 +84,6 @@ | |||
| <div class="chat-input-container"> | |||
| <app-chat-input | |||
| (messageSent)="onMessageSent($event)" | |||
| (attach)="onAttach()" | |||
| (voice)="onVoice()" | |||
| ></app-chat-input> | |||
| </div> | |||
| </div> | |||
| @@ -1,8 +1,6 @@ | |||
| import { | |||
| Component, | |||
| Input, | |||
| Output, | |||
| EventEmitter, | |||
| ViewChild, | |||
| ElementRef, | |||
| AfterViewChecked, | |||
| @@ -10,12 +8,13 @@ import { | |||
| } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| 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 { 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 {DataService} from '../../services/data.service'; | |||
| import {FileHandlerService} from '../../services/file-handler.service'; | |||
| @Component({ | |||
| selector: 'app-chat-view', | |||
| @@ -30,11 +29,6 @@ export class ChatView implements AfterViewChecked, OnInit, OnDestroy { | |||
| @Input() conversation: Conversation | null = null; | |||
| @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; | |||
| @@ -43,8 +37,8 @@ export class ChatView implements AfterViewChecked, OnInit, OnDestroy { | |||
| private shouldScrollToBottom = false; | |||
| constructor( | |||
| private sanitizer: DomSanitizer, | |||
| private dataService: DataService | |||
| private dataService: DataService, | |||
| private fileHandlerService: FileHandlerService | |||
| ) {} | |||
| ngOnInit(): void { | |||
| @@ -65,45 +59,68 @@ export class ChatView implements AfterViewChecked, OnInit, OnDestroy { | |||
| } | |||
| ngOnDestroy(): void { | |||
| this.isTypingSubscription?.unsubscribe(); | |||
| this.conversation?.messages.forEach(message => { | |||
| if (message.files) { | |||
| this.fileHandlerService.cleanupFiles(message.files); | |||
| } | |||
| }); | |||
| } | |||
| onMessageSentV1(data: { message: string; files: File[] }): void { | |||
| this.messageSent.emit(data); | |||
| this.shouldScrollToBottom = true; | |||
| } | |||
| onMessageSent(data: { message: string; files: File[] }): void { | |||
| async onMessageSent(data: { message: string; files: AttachedFile[] }): Promise<void> { | |||
| 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({ | |||
| content: data.message, | |||
| 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.shouldScrollToBottom = true; | |||
| } | |||
| onAttach(): void { | |||
| this.attach.emit(); | |||
| } | |||
| onVoice(): void { | |||
| this.voice.emit(); | |||
| } | |||
| onCopyMessage(content: string): void { | |||
| navigator.clipboard.writeText(content); | |||
| this.copyMessage.emit(content); | |||
| } | |||
| 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 { | |||
| @@ -122,49 +139,14 @@ export class ChatView implements AfterViewChecked, OnInit, OnDestroy { | |||
| 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 { | |||
| 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 { | |||
| @@ -39,9 +39,7 @@ | |||
| <div class="center-input-wrapper" [class.animate-down]="currentConversation"> | |||
| <app-chat-input | |||
| (messageSent)="onMessageSent($event)" | |||
| (attach)="onAttach()" | |||
| (voice)="onVoice()" | |||
| (messageSent)="onMessageSentFromWelcome($event)" | |||
| ></app-chat-input> | |||
| </div> | |||
| @@ -58,13 +56,7 @@ | |||
| @if (currentConversation) { | |||
| <app-chat-view | |||
| [conversation]="currentConversation" | |||
| [isTyping]="isTyping" | |||
| [agentName]="currentAgentName" | |||
| (messageSent)="onMessageSent($event)" | |||
| (attach)="onAttach()" | |||
| (voice)="onVoice()" | |||
| (copyMessage)="onCopyMessage($event)" | |||
| (regenerateMessage)="onRegenerateMessage()" | |||
| ></app-chat-view> | |||
| } | |||
| @@ -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, AttachedFile } from '../../models/data.model'; | |||
| import {Agent, AttachedFile, Conversation} from '../../models/data.model'; | |||
| import {ChatView} from '../chat/chat-view'; | |||
| import {RouterLink} from '@angular/router'; | |||
| @@ -83,73 +83,28 @@ export class Dashboard implements OnInit { | |||
| onConversationSelected(conversationId: string): void { | |||
| 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(() => { | |||
| this.sendMessage(message, files); | |||
| if (this.chatView) { | |||
| this.chatView.onMessageSent(data); | |||
| } | |||
| }, 100); | |||
| } else { | |||
| this.sendMessage(message, files); | |||
| } | |||
| }, 600); | |||
| } | |||
| onToggleSidebar(): void { | |||
| 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; | |||
| const attachedFiles: AttachedFile[] = await this.processFiles(files); | |||
| @@ -189,9 +144,8 @@ export class Dashboard implements OnInit { | |||
| for (const file of files) { | |||
| const attachedFile: AttachedFile = { | |||
| name: file.name, | |||
| size: file.size, | |||
| type: file.type, | |||
| file:file, | |||
| id: | |||
| url: URL.createObjectURL(file) | |||
| }; | |||
| @@ -224,13 +178,8 @@ export class Dashboard implements OnInit { | |||
| reader.onerror = () => reject(new Error('Erreur de lecture du fichier')); | |||
| reader.readAsDataURL(file); | |||
| }); | |||
| } | |||
| }*/ | |||
| get hasActiveConversation(): boolean { | |||
| return this.currentConversation !== null && | |||
| this.currentConversation.messages && | |||
| this.currentConversation.messages.length > 0; | |||
| } | |||
| get currentAgentName(): string { | |||
| if (!this.currentConversation) return ''; | |||
| @@ -0,0 +1,248 @@ | |||
| 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); | |||
| } | |||
| } | |||