瀏覽代碼

normalisation de la gestion des fichiers, supression de la logique du dashboard par rapport au chat

master
trauchessec 1 周之前
父節點
當前提交
1ae583ce6a
共有 8 個文件被更改,包括 366 次插入253 次删除
  1. +2
    -2
      src/app/components/chat-input/chat-input.html
  2. +44
    -91
      src/app/components/chat-input/chat-input.ts
  3. +3
    -12
      src/app/models/data.model.ts
  4. +5
    -7
      src/app/pages/chat/chat-view.html
  5. +48
    -66
      src/app/pages/chat/chat-view.ts
  6. +1
    -9
      src/app/pages/dashboard/dashboard.html
  7. +15
    -66
      src/app/pages/dashboard/dashboard.ts
  8. +248
    -0
      src/app/services/file-handler.service.ts

+ 2
- 2
src/app/components/chat-input/chat-input.html 查看文件

@@ -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;"

+ 44
- 91
src/app/components/chat-input/chat-input.ts 查看文件

@@ -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;

}
}

+ 3
- 12
src/app/models/data.model.ts 查看文件

@@ -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;

+ 5
- 7
src/app/pages/chat/chat-view.html 查看文件

@@ -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>

+ 48
- 66
src/app/pages/chat/chat-view.ts 查看文件

@@ -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 {

+ 1
- 9
src/app/pages/dashboard/dashboard.html 查看文件

@@ -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>
}


+ 15
- 66
src/app/pages/dashboard/dashboard.ts 查看文件

@@ -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 '';

+ 248
- 0
src/app/services/file-handler.service.ts 查看文件

@@ -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);
}
}

Loading…
取消
儲存