Procházet zdrojové kódy

gestion des pieces jointe

master
trauchessec před 1 týdnem
rodič
revize
9de9315047
11 změnil soubory, kde provedl 688 přidání a 75 odebrání
  1. +31
    -3
      src/app/components/chat-input/chat-input.html
  2. +193
    -8
      src/app/components/chat-input/chat-input.scss
  3. +142
    -13
      src/app/components/chat-input/chat-input.ts
  4. +52
    -22
      src/app/data/mock-data.ts
  5. +20
    -1
      src/app/models/data.model.ts
  6. +24
    -3
      src/app/pages/chat/chat-view.html
  7. +111
    -3
      src/app/pages/chat/chat-view.scss
  8. +48
    -6
      src/app/pages/chat/chat-view.ts
  9. +3
    -6
      src/app/pages/dashboard/dashboard.scss
  10. +61
    -8
      src/app/pages/dashboard/dashboard.ts
  11. +3
    -2
      src/app/services/data.service.ts

+ 31
- 3
src/app/components/chat-input/chat-input.html Zobrazit soubor

@@ -1,4 +1,22 @@
<div class="chat-input-container">
<div class="chat-input-container" (drop)="onFileDrop($event)" (dragover)="onDragOver($event)">

<div class="attached-files" *ngIf="attachedFiles.length > 0">
<div class="file-item" *ngFor="let attachedFile of attachedFiles">
<div class="file-preview">
<img *ngIf="attachedFile.preview" [src]="attachedFile.preview" alt="Preview">
<div class="file-icon" *ngIf="!attachedFile.preview" [innerHTML]="getFileIcon(attachedFile.file.type)">
</div>
</div>
<div class="file-info">
<span class="file-name">{{ attachedFile.file.name }}</span>
<span class="file-size">{{ formatFileSize(attachedFile.file.size) }}</span>
</div>
<button class="remove-file-button" (click)="removeFile(attachedFile.id)" [attr.aria-label]="'Supprimer le fichier'">
<fa-icon [icon]="faTimes"></fa-icon>
</button>
</div>
</div>

<div class="input-wrapper">
<textarea
#messageInput
@@ -25,13 +43,23 @@
<button
class="send-button"
(click)="onSend()"
[disabled]="!message.trim() || disabled"
[disabled]="(!message.trim() && attachedFiles.length === 0) || disabled"
[attr.aria-label]="'Envoyer le message'"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-up" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5"/>
</svg> </button>
</svg>
</button>
</div>
</div>
</div>

<input
#fileInput
type="file"
[accept]="acceptedFileTypes"
[multiple]="true"
(change)="onFileSelected($event)"
style="display: none;"
/>
</div>

+ 193
- 8
src/app/components/chat-input/chat-input.scss Zobrazit soubor

@@ -3,11 +3,110 @@
padding: 16px 24px;
background: transparent;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;

.attached-files {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 16px;
max-width: 1000px;
margin: 0 auto;
width: 100%;

.file-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
max-width: 32%;
transition: all 0.2s ease;

&:hover {
border-color: #d1d5db;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}

.file-preview {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 6px;
overflow: hidden;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #e5e7eb;

img {
width: 100%;
height: 100%;
object-fit: cover;
}

.file-icon {
font-size: 20px;
}
}

.file-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;

.file-name {
font-size: 13px;
font-weight: 500;
color: #374151;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.file-size {
font-size: 11px;
color: #9ca3af;
}
}

.remove-file-button {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 4px;
color: #9ca3af;
cursor: pointer;
transition: all 0.2s ease;

&:hover {
background: #ffffff;
color: #ef4444;
}

fa-icon {
font-size: 14px;
}
}
}
}

.input-wrapper {
width: 100%;
max-width: 1000px;
margin: 0 auto;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 16px;
@@ -18,6 +117,12 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;

&.has-files {
border-radius: 0 0 16px 16px;
border-top: none;
padding-top: 12px;
}

&:focus-within {
border-color: #3498db;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.12);
@@ -104,8 +209,9 @@
color: #1f2937;
}

fa-icon {
font-size: 18px;
svg {
width: 18px;
height: 18px;
}
}

@@ -124,7 +230,7 @@

&:hover:not(:disabled) {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
box-shadow: 0 4px 12px rgba(246, 62, 99, 0.3);
}

&:active:not(:disabled) {
@@ -136,18 +242,42 @@
cursor: not-allowed;
opacity: 0.6;

fa-icon {
svg {
color: #9ca3af;
}
}

fa-icon {
font-size: 16px;
svg {
width: 16px;
height: 16px;
}
}
}
}
}

input[type="file"] {
display: none;
}
}

.chat-input-container.drag-over {
&::before {
content: 'Déposez vos fichiers ici';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed #3498db;
border-radius: 16px;
background: rgba(52, 152, 219, 0.05);
color: #3498db;
font-size: 16px;
font-weight: 600;
pointer-events: none;
z-index: 10;
}
}

// Responsive
@@ -155,10 +285,49 @@
.chat-input-container {
padding: 12px 16px;

.attached-files {
padding: 10px 12px;
border-radius: 10px 10px 0 0;
gap: 6px;

.file-item {
padding: 6px 10px;
max-width: 200px;

.file-preview {
width: 36px;
height: 36px;
}

.file-info {
.file-name {
font-size: 12px;
}

.file-size {
font-size: 10px;
}
}

.remove-file-button {
width: 20px;
height: 20px;

fa-icon {
font-size: 12px;
}
}
}
}

.input-wrapper {
padding: 12px 16px;
border-radius: 14px;

&.has-files {
border-radius: 0 0 14px 14px;
}

textarea {
font-size: 14px;
}
@@ -180,8 +349,9 @@
width: 32px;
height: 32px;

fa-icon {
font-size: 15px;
svg {
width: 15px;
height: 15px;
}
}
}
@@ -196,3 +366,18 @@
overflow-y: auto;
}
}

@keyframes fileSlideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.attached-files .file-item {
animation: fileSlideIn 0.3s ease;
}

+ 142
- 13
src/app/components/chat-input/chat-input.ts Zobrazit soubor

@@ -1,12 +1,16 @@
import { Component, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import {
// chat-input.ts
import {Component, Output, EventEmitter, Input, ViewChild, ElementRef} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {faPaperclip, faTimes} from '@fortawesome/free-solid-svg-icons';
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';

faPaperclip,

} from '@fortawesome/free-solid-svg-icons';
interface AttachedFile {
file: File;
id: string;
preview?: string;
}

@Component({
selector: 'app-chat-input',
@@ -17,21 +21,44 @@ import {
})
export class ChatInput {
@Input() disabled = false;
@Output() messageSent = new EventEmitter<string>();
@Output() fileAttached = new EventEmitter<void>();
@Output() messageSent = new EventEmitter<{ message: string; files: File[] }>();
@Output() fileAttached = new EventEmitter<File[]>();
@Output() voiceActivated = new EventEmitter<void>();
@ViewChild('messageInput') messageInput!: ElementRef<HTMLTextAreaElement>;
@ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;

message = '';
attachedFiles: AttachedFile[] = [];
faPaperclip = faPaperclip;
faTimes = faTimes;

acceptedFileTypes = [
'image/*',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/*'
].join(',');

maxFileSize = 10 * 1024 * 1024; // 10MB
maxFiles = 10;
constructor(private sanitizer: DomSanitizer) {}

onSend(): void {
if (this.message.trim() && !this.disabled) {
this.messageSent.emit(this.message.trim());
if ((this.message.trim() || this.attachedFiles.length > 0) && !this.disabled) {
const files = this.attachedFiles.map(af => af.file);
this.messageSent.emit({
message: this.message.trim(),
files: files
});
this.message = '';
this.attachedFiles = [];
this.adjustTextareaHeight();
}
}

onInputChange(): void {
const textarea = this.messageInput.nativeElement;
textarea.style.height = 'auto';
@@ -39,7 +66,109 @@ export class ChatInput {
}

onAttach(): void {
this.fileAttached.emit();
this.fileInput.nativeElement.click();
}

onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.handleFiles(Array.from(input.files));
input.value = '';
}
}

onFileDrop(event: DragEvent): void {
event.preventDefault();
event.stopPropagation();

if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
this.handleFiles(Array.from(event.dataTransfer.files));
}
}

onDragOver(event: DragEvent): void {
event.preventDefault();
event.stopPropagation();
}

private handleFiles(files: File[]): void {
if (this.attachedFiles.length + files.length > this.maxFiles) {
alert(`Vous ne pouvez joindre que ${this.maxFiles} fichiers maximum.`);
return;
}

files.forEach(file => {
// Vérifier la taille du fichier
if (file.size > this.maxFileSize) {
alert(`Le fichier "${file.name}" dépasse la taille maximale de 10MB.`);
return;
}

const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

const attachedFile: AttachedFile = {
file: file,
id: id
};

if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
attachedFile.preview = e.target?.result as string;
};
reader.readAsDataURL(file);
}

this.attachedFiles.push(attachedFile);
});

this.fileAttached.emit(files);
}

removeFile(id: string): void {
this.attachedFiles = this.attachedFiles.filter(f => f.id !== id);
}

getFileIcon(type: string): SafeHtml {
let svgIcon = '';

if (type.startsWith('image/')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-image" viewBox="0 0 16 16">
<path d="M8.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
<path d="M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M3 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v8l-2.083-2.083a.5.5 0 0 0-.76.063L8 11 5.835 9.7a.5.5 0 0 0-.611.076L3 12z"/>
</svg>`;
} else if (type.includes('pdf')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-filetype-pdf" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM1.6 11.85H0v3.999h.791v-1.342h.803q.43 0 .732-.173.305-.175.463-.474a1.4 1.4 0 0 0 .161-.677q0-.375-.158-.677a1.2 1.2 0 0 0-.46-.477q-.3-.18-.732-.179m.545 1.333a.8.8 0 0 1-.085.38.57.57 0 0 1-.238.241.8.8 0 0 1-.375.082H.788V12.48h.66q.327 0 .512.181.185.183.185.522m1.217-1.333v3.999h1.46q.602 0 .998-.237a1.45 1.45 0 0 0 .595-.689q.196-.45.196-1.084 0-.63-.196-1.075a1.43 1.43 0 0 0-.589-.68q-.396-.234-1.005-.234zm.791.645h.563q.371 0 .609.152a.9.9 0 0 1 .354.454q.118.302.118.753a2.3 2.3 0 0 1-.068.592 1.1 1.1 0 0 1-.196.422.8.8 0 0 1-.334.252 1.3 1.3 0 0 1-.483.082h-.563zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638z"/>
</svg>`;
} else if (type.includes('word') || type.includes('document')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-filetype-doc" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM.5 11.85h1.6q.434 0 .732.179.302.175.46.477.158.302.158.677t-.158.677q-.159.3-.464.474a1.45 1.45 0 0 1-.732.173H.5zm.791 1.984v1.328H.5v-3.999h1.915q.493 0 .729.174.237.173.377.472.14.3.14.677 0 .374-.14.677-.137.299-.375.472-.236.173-.728.173zm4.035-3.306h1.6q.434 0 .732.179.302.175.46.477.158.302.158.677t-.158.677q-.159.3-.464.474a1.45 1.45 0 0 1-.732.173h-1.6zm.791 1.984v1.328h-.791v-3.999h1.915q.493 0 .729.174.237.173.377.472.14.3.14.677 0 .374-.14.677-.137.299-.375.472-.236.173-.728.173z"/>
</svg>`;
} else if (type.includes('excel') || type.includes('sheet') || type.includes('spreadsheet')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-filetype-xls" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM6.472 15.29a1.2 1.2 0 0 1-.111-.449h.765a.58.58 0 0 0 .254.384q.106.073.25.114.143.041.319.041.246 0 .413-.07a.56.56 0 0 0 .255-.193.5.5 0 0 0 .085-.29.39.39 0 0 0-.153-.326q-.152-.12-.462-.193l-.619-.143a1.7 1.7 0 0 1-.539-.214 1 1 0 0 1-.351-.367 1.1 1.1 0 0 1-.123-.524q0-.366.19-.639.19-.272.527-.422.338-.15.777-.149.457 0 .78.152.324.153.5.41.18.255.2.566h-.75a.56.56 0 0 0-.12-.258.6.6 0 0 0-.247-.181.9.9 0 0 0-.369-.068q-.325 0-.513.152a.47.47 0 0 0-.184.384q0 .18.143.3a1 1 0 0 0 .405.175l.62.143q.326.075.566.211a1 1 0 0 1 .375.358q.135.222.135.56 0 .37-.188.656a1.2 1.2 0 0 1-.539.439q-.351.158-.858.158-.381 0-.665-.09a1.4 1.4 0 0 1-.478-.252 1.1 1.1 0 0 1-.29-.375m-2.945-3.358h-.893L1.81 13.37h-.036l-.832-1.438h-.93l1.227 1.983L0 15.931h.861l.853-1.415h.035l.85 1.415h.908L2.253 13.94zm2.727 3.325H4.557v-3.325h-.79v4h2.487z"/>
</svg>`;
} else if (type.startsWith('text/')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-text" viewBox="0 0 16 16">
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/>
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
</svg>`;
} else {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/>
</svg>`;
}

return this.sanitizer.bypassSecurityTrustHtml(svgIcon);
}

formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}

onVoice(): void {

+ 52
- 22
src/app/data/mock-data.ts Zobrazit soubor

@@ -93,49 +93,63 @@ export const MOCK_CONVERSATIONS: Conversation[] = [
timestamp: new Date('2024-11-20T10:39:20'),
}
],
lastUpdate: new Date('2024-11-20T10:39:20'),
createdAt: new Date('2024-11-20T10:39:20'),
updatedAt: new Date('2024-11-20T10:39:20'),

agentId: 'audit'
},
{
id: 'conv2',
title: 'Demande de congé pour le mois de Janvier',
messages: [],
lastUpdate: new Date('2024-11-23T15:00:30'),
createdAt: new Date('2024-11-23T15:00:30'),
updatedAt: new Date('2024-11-23T15:00:30'),

agentId: 'rh'
},
{
id: 'conv3',
title: 'Audit des documents fiscaux pour l\'année N-1',
messages: [],
lastUpdate: new Date('2024-11-18T09:15:00'),
createdAt: new Date('2024-11-18T09:15:00'),
updatedAt: new Date('2024-11-18T09:15:00'),

agentId: 'audit-docs'
},
{
id: 'conv4',
title: 'Problème de connexion VPN sur mobile',
messages: [],
lastUpdate: new Date('2024-11-24T10:45:35'),
createdAt: new Date('2024-11-24T10:45:35'),
updatedAt: new Date('2024-11-24T10:45:35'),

agentId: 'it'
},
{
id: 'conv5',
title: 'Rédaction d\'une clause de non-concurrence',
messages: [],
lastUpdate: new Date('2024-11-22T14:01:15'),
createdAt: new Date('2024-11-22T14:01:15'),
updatedAt: new Date('2024-11-22T14:01:15'),

agentId: 'legal'
},
{
id: 'conv6',
title: 'Demande d\'information générale',
messages: [],
lastUpdate: new Date('2024-11-17T16:45:00'),
createdAt: new Date('2024-11-17T16:45:00'),
updatedAt: new Date('2024-11-17T16:45:00'),

agentId: 'default'
},
{
id: 'conv7',
title: 'Préparation du rapport annuel des ventes',
messages: [],
lastUpdate: new Date('2024-11-15T11:00:00'),
createdAt: new Date('2024-11-15T11:00:00'),
updatedAt: new Date('2024-11-15T11:00:00'),

agentId: 'audit'
}
];
@@ -149,21 +163,24 @@ export const MOCK_PROJECTS: Project[] = [
id: 'proj1-conv1',
title: 'Budget prévisionnel 2025',
messages: [],
lastUpdate: new Date('2024-11-22T09:00:00'),
createdAt: new Date('2024-11-22T09:00:00'),
updatedAt: new Date('2024-11-22T09:00:00'),
agentId: 'audit'
},
{
id: 'proj1-conv2',
title: 'Analyse des dépenses Q1-Q3',
messages: [],
lastUpdate: new Date('2024-11-21T14:30:00'),
createdAt: new Date('2024-11-21T14:30:00'),
updatedAt: new Date('2024-11-21T14:30:00'),
agentId: 'audit'
},
{
id: 'proj1-conv3',
title: 'Optimisation des coûts',
messages: [],
lastUpdate: new Date('2024-11-20T11:15:00'),
createdAt: new Date('2024-11-20T11:15:00'),
updatedAt: new Date('2024-11-20T11:15:00'),
agentId: 'default'
}
],
@@ -177,21 +194,24 @@ export const MOCK_PROJECTS: Project[] = [
id: 'proj2-conv1',
title: 'Rapport mensuel Novembre',
messages: [],
lastUpdate: new Date('2024-11-24T16:00:00'),
createdAt: new Date('2024-11-24T16:00:00'),
updatedAt: new Date('2024-11-24T16:00:00'),
agentId: 'audit'
},
{
id: 'proj2-conv2',
title: 'Bilan trimestriel Q4 (en cours)',
messages: [],
lastUpdate: new Date('2024-11-23T10:45:00'),
createdAt: new Date('2024-11-23T10:45:00'),
updatedAt: new Date('2024-11-23T10:45:00'),
agentId: 'audit'
},
{
id: 'proj2-conv3',
title: 'Validation des écritures 2024',
messages: [],
lastUpdate: new Date('2024-11-20T11:00:00'),
createdAt: new Date('2024-11-20T11:00:00'),
updatedAt: new Date('2024-11-20T11:00:00'),
agentId: 'audit-docs'
}
],
@@ -205,21 +225,24 @@ export const MOCK_PROJECTS: Project[] = [
id: 'proj3-conv1',
title: 'Audit fiscal Décembre',
messages: [],
lastUpdate: new Date('2024-11-23T13:20:00'),
createdAt: new Date('2024-11-23T13:20:00'),
updatedAt: new Date('2024-11-23T13:20:00'),
agentId: 'audit-docs'
},
{
id: 'proj3-conv2',
title: 'Vérification des comptes fournisseurs',
messages: [],
lastUpdate: new Date('2024-11-22T15:30:00'),
createdAt: new Date('2024-11-22T15:30:00'),
updatedAt: new Date('2024-11-22T15:30:00'),
agentId: 'audit-docs'
},
{
id: 'proj3-conv3',
title: 'Contrôle des procédures internes',
messages: [],
lastUpdate: new Date('2024-11-21T09:00:00'),
createdAt: new Date('2024-11-21T09:00:00'),
updatedAt: new Date('2024-11-21T09:00:00'),
agentId: 'audit'
}
],
@@ -233,14 +256,16 @@ export const MOCK_PROJECTS: Project[] = [
id: 'proj4-conv1',
title: 'Grille salariale 2025',
messages: [],
lastUpdate: new Date('2024-11-24T11:00:00'),
createdAt: new Date('2024-11-24T11:00:00'),
updatedAt: new Date('2024-11-24T11:00:00'),
agentId: 'rh'
},
{
id: 'proj4-conv2',
title: 'Politique de télétravail',
messages: [],
lastUpdate: new Date('2024-11-21T14:15:00'),
updatedAt: new Date('2024-11-21T14:15:00'),
createdAt: new Date('2024-11-21T14:15:00'),
agentId: 'rh'
}
],
@@ -254,14 +279,16 @@ export const MOCK_PROJECTS: Project[] = [
id: 'proj5-conv1',
title: 'Configuration nouvelle imprimante réseau',
messages: [],
lastUpdate: new Date('2024-11-23T10:00:00'),
createdAt: new Date('2024-11-23T10:00:00'),
updatedAt: new Date('2024-11-23T10:00:00'),
agentId: 'it'
},
{
id: 'proj5-conv2',
title: 'Mise à jour des systèmes d\'exploitation',
messages: [],
lastUpdate: new Date('2024-11-20T17:00:00'),
createdAt: new Date('2024-11-20T17:00:00'),
updatedAt: new Date('2024-11-20T17:00:00'),
agentId: 'it'
}
],
@@ -275,14 +302,17 @@ export const MOCK_PROJECTS: Project[] = [
id: 'proj6-conv1',
title: 'Vérification des contrats fournisseurs',
messages: [],
lastUpdate: new Date('2024-11-24T09:30:00'),
createdAt: new Date('2024-11-24T09:30:00'),
updatedAt: new Date('2024-11-24T09:30:00'),

agentId: 'legal'
},
{
id: 'proj6-conv2',
title: 'Accord de confidentialité',
messages: [],
lastUpdate: new Date('2024-11-21T12:00:00'),
createdAt: new Date('2024-11-21T12:00:00'),
updatedAt: new Date('2024-11-21T12:00:00'),
agentId: 'legal'
}
],

+ 20
- 1
src/app/models/data.model.ts Zobrazit soubor

@@ -10,14 +10,33 @@ export interface Message {
content: string;
sender: 'user' | 'agent';
timestamp: Date;
files?: AttachedFile[];
}

export interface AttachedFile {
name: string;
size: number;
type: string;
url?: string;
preview?: string;
}

export interface Conversation {
id: string;
title: string;
agentId: string;
messages: Message[];
createdAt: Date;
updatedAt: Date;
}

export interface Conversation {
id: string;
title: string;
messages: Message[];
lastUpdate: Date;
agentId: string;
createdAt: Date;
updatedAt: Date;
}

export interface Project {

+ 24
- 3
src/app/pages/chat/chat-view.html Zobrazit soubor

@@ -10,7 +10,29 @@
<!-- Message de l'utilisateur -->
<div class="message-content" *ngIf="message.sender === 'user'">
<div class="message-bubble user-bubble">
<p>{{ message.content }}</p>
<!-- Fichiers attachés -->
<div class="message-files" *ngIf="message.files && message.files.length > 0">
<div class="file-item" *ngFor="let file of message.files">
<div class="file-preview" *ngIf="file.preview">
<img [src]="file.preview" [alt]="file.name">
</div>
<div class="file-icon" *ngIf="!file.preview" [innerHTML]="getFileIcon(file.type)">
</div>
<div class="file-details">
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatFileSize(file.size) }}</span>
</div>
<a *ngIf="file.url" [href]="file.url" download [attr.aria-label]="'Télécharger ' + file.name" class="file-download">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
</svg>
</a>
</div>
</div>

<!-- Texte du message -->
<p *ngIf="message.content">{{ message.content }}</p>
</div>
</div>

@@ -38,8 +60,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/>
<path
d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/>
</svg>
</button>
</div>

+ 111
- 3
src/app/pages/chat/chat-view.scss Zobrazit soubor

@@ -2,7 +2,6 @@
display: none;
height: 100%;
flex-direction: column;
background: #f5f7fa;
opacity: 0;
transform: translateY(20px);
transition: all 0.4s ease;
@@ -81,6 +80,117 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;

.message-files {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;

.file-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: rgba(0, 0, 0, 0.03);
border-radius: 8px;
transition: all 0.2s ease;

&:hover {
background: rgba(0, 0, 0, 0.05);
}

.file-preview {
flex-shrink: 0;
width: 50px;
height: 50px;
border-radius: 6px;
overflow: hidden;
background: #ffffff;

img {
width: 100%;
height: 100%;
object-fit: cover;
}
}

.file-icon {
flex-shrink: 0;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: #ffffff;
border-radius: 6px;
}

.file-details {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;

.file-name {
font-size: 13px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.file-size {
font-size: 11px;
opacity: 0.7;
}
}

.file-download {
flex-shrink: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.05);
border-radius: 6px;
color: inherit;
text-decoration: none;
transition: all 0.2s ease;

&:hover {
background: rgba(0, 0, 0, 0.1);
}

svg {
width: 16px;
height: 16px;
}
}
}
}

// Message utilisateur avec fichiers
&.user-bubble {
.message-files .file-item {
background: rgba(255, 255, 255, 0.2);

&:hover {
background: rgba(255, 255, 255, 0.3);
}

.file-download {
background: rgba(255, 255, 255, 0.2);

&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
}
}

&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
@@ -181,8 +291,6 @@

.chat-input-container {
padding: 16px 24px 24px;
background: #f5f7fa;
border-top: 1px solid #e5e7eb;

app-chat-input {
max-width: 800px;

+ 48
- 6
src/app/pages/chat/chat-view.ts Zobrazit soubor

@@ -2,8 +2,9 @@ import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterVie
import { CommonModule } from '@angular/common';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faCopy, faThumbsUp, faThumbsDown, faRotateRight } from '@fortawesome/free-solid-svg-icons';
import { ChatInput } from '../../components/chat-input/chat-input';
import { Conversation } from '../../models/data.model';
import {ChatInput} from '../../components/chat-input/chat-input';
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';

@Component({
selector: 'app-chat-view',
@@ -19,18 +20,17 @@ export class ChatView implements AfterViewChecked {
@Input() isTyping = false;
@Input() agentName = 'Assistant';

@Output() messageSent = new EventEmitter<string>();
@Output() messageSent = new EventEmitter<{ message: string; files: File[] }>();
@Output() attach = new EventEmitter<void>();
@Output() voice = new EventEmitter<void>();
@Output() copyMessage = new EventEmitter<string>();
@Output() regenerateMessage = new EventEmitter<void>();

// Icons
faCopy = faCopy;


private shouldScrollToBottom = false;
constructor(private sanitizer: DomSanitizer) {}
ngAfterViewChecked(): void {
if (this.shouldScrollToBottom) {
this.scrollToBottom();
@@ -38,8 +38,8 @@ export class ChatView implements AfterViewChecked {
}
}

onMessageSent(message: string): void {
this.messageSent.emit(message);
onMessageSent(data: { message: string; files: File[] }): void {
this.messageSent.emit(data);
this.shouldScrollToBottom = true;
}

@@ -60,6 +60,48 @@ export class ChatView implements AfterViewChecked {
this.regenerateMessage.emit();
}

getFileIcon(type: string): SafeHtml {
let svgIcon = '';

if (type.startsWith('image/')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-image" viewBox="0 0 16 16">
<path d="M8.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
<path d="M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M3 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v8l-2.083-2.083a.5.5 0 0 0-.76.063L8 11 5.835 9.7a.5.5 0 0 0-.611.076L3 12z"/>
</svg>`;
} else if (type.includes('pdf')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-filetype-pdf" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM1.6 11.85H0v3.999h.791v-1.342h.803q.43 0 .732-.173.305-.175.463-.474a1.4 1.4 0 0 0 .161-.677q0-.375-.158-.677a1.2 1.2 0 0 0-.46-.477q-.3-.18-.732-.179m.545 1.333a.8.8 0 0 1-.085.38.57.57 0 0 1-.238.241.8.8 0 0 1-.375.082H.788V12.48h.66q.327 0 .512.181.185.183.185.522m1.217-1.333v3.999h1.46q.602 0 .998-.237a1.45 1.45 0 0 0 .595-.689q.196-.45.196-1.084 0-.63-.196-1.075a1.43 1.43 0 0 0-.589-.68q-.396-.234-1.005-.234zm.791.645h.563q.371 0 .609.152a.9.9 0 0 1 .354.454q.118.302.118.753a2.3 2.3 0 0 1-.068.592 1.1 1.1 0 0 1-.196.422.8.8 0 0 1-.334.252 1.3 1.3 0 0 1-.483.082h-.563zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638z"/>
</svg>`;
} else if (type.includes('word') || type.includes('document')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-filetype-doc" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM.5 11.85h1.6q.434 0 .732.179.302.175.46.477.158.302.158.677t-.158.677q-.159.3-.464.474a1.45 1.45 0 0 1-.732.173H.5zm.791 1.984v1.328H.5v-3.999h1.915q.493 0 .729.174.237.173.377.472.14.3.14.677 0 .374-.14.677-.137.299-.375.472-.236.173-.728.173zm4.035-3.306h1.6q.434 0 .732.179.302.175.46.477.158.302.158.677t-.158.677q-.159.3-.464.474a1.45 1.45 0 0 1-.732.173h-1.6zm.791 1.984v1.328h-.791v-3.999h1.915q.493 0 .729.174.237.173.377.472.14.3.14.677 0 .374-.14.677-.137.299-.375.472-.236.173-.728.173z"/>
</svg>`;
} else if (type.includes('excel') || type.includes('sheet') || type.includes('spreadsheet')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-filetype-xls" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM6.472 15.29a1.2 1.2 0 0 1-.111-.449h.765a.58.58 0 0 0 .254.384q.106.073.25.114.143.041.319.041.246 0 .413-.07a.56.56 0 0 0 .255-.193.5.5 0 0 0 .085-.29.39.39 0 0 0-.153-.326q-.152-.12-.462-.193l-.619-.143a1.7 1.7 0 0 1-.539-.214 1 1 0 0 1-.351-.367 1.1 1.1 0 0 1-.123-.524q0-.366.19-.639.19-.272.527-.422.338-.15.777-.149.457 0 .78.152.324.153.5.41.18.255.2.566h-.75a.56.56 0 0 0-.12-.258.6.6 0 0 0-.247-.181.9.9 0 0 0-.369-.068q-.325 0-.513.152a.47.47 0 0 0-.184.384q0 .18.143.3a1 1 0 0 0 .405.175l.62.143q.326.075.566.211a1 1 0 0 1 .375.358q.135.222.135.56 0 .37-.188.656a1.2 1.2 0 0 1-.539.439q-.351.158-.858.158-.381 0-.665-.09a1.4 1.4 0 0 1-.478-.252 1.1 1.1 0 0 1-.29-.375m-2.945-3.358h-.893L1.81 13.37h-.036l-.832-1.438h-.93l1.227 1.983L0 15.931h.861l.853-1.415h.035l.85 1.415h.908L2.253 13.94zm2.727 3.325H4.557v-3.325h-.79v4h2.487z"/>
</svg>`;
} else if (type.startsWith('text/')) {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-text" viewBox="0 0 16 16">
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/>
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
</svg>`;
} else {
svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/>
</svg>`;
}

return this.sanitizer.bypassSecurityTrustHtml(svgIcon);
}

formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}

private scrollToBottom(): void {
try {
const messagesElement = this.messagesContainer?.nativeElement;

+ 3
- 6
src/app/pages/dashboard/dashboard.scss Zobrazit soubor

@@ -10,17 +10,14 @@
flex-direction: column;
overflow: hidden;
position: relative;
background-image: url("/assets/images/image.png");

// dashboard.scss (extrait - partie header avec back button)
.content-header {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
padding: 16px 24px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-bottom: 1px solid #e5e7eb;
z-index: 10;

.back-button {
@@ -142,7 +139,7 @@
animation: fadeInDown 0.6s ease;

.welcome-title {
font-size: 48px;
font-size: 60px;
font-weight: 700;
color: #1f2937;
margin: 0 0 12px 0;
@@ -154,7 +151,7 @@
}

.welcome-subtitle {
font-size: 20px;
font-size: 40px;
color: #6b7280;
margin: 0;
font-weight: 400;

+ 61
- 8
src/app/pages/dashboard/dashboard.ts Zobrazit soubor

@@ -6,7 +6,7 @@ import { Sidebar } from '../../components/sidebar/sidebar';
import { AgentCard } from '../../components/agent-card/agent-card';
import { ChatInput } from '../../components/chat-input/chat-input';
import { DataService } from '../../services/data.service';
import { Agent, Conversation } from '../../models/data.model';
import { Agent, Conversation, AttachedFile } from '../../models/data.model';
import {ChatView} from '../chat/chat-view';
import {RouterLink} from '@angular/router';

@@ -84,15 +84,17 @@ export class Dashboard implements OnInit {
this.dataService.selectConversation(conversationId);
}

onMessageSent(message: string): void {
onMessageSent(data: { message: string; files: File[] } | string): void {
const message = typeof data === 'string' ? data : data.message;
const files = typeof data === 'string' ? [] : data.files;

if (!this.currentConversation) {
this.dataService.createNewConversation('default');

setTimeout(() => {
this.sendMessage(message);
this.sendMessage(message, files);
}, 100);
} else {
this.sendMessage(message);
this.sendMessage(message, files);
}
}

@@ -147,12 +149,20 @@ export class Dashboard implements OnInit {
}
}

private sendMessage(message: string): void {
private async sendMessage(message: string, files: File[] = []): Promise<void> {
if (!this.currentConversation) return;

const attachedFiles: AttachedFile[] = await this.processFiles(files);

let messageContent = message;
if (!messageContent && attachedFiles.length > 0) {
messageContent = `${attachedFiles.length} fichier(s) joint(s)`;
}

this.dataService.addMessage({
content: message,
content: messageContent,
sender: 'user',
files: attachedFiles.length > 0 ? attachedFiles : undefined
});

this.isTyping = true;
@@ -163,7 +173,7 @@ export class Dashboard implements OnInit {
setTimeout(() => {
this.dataService.simulateAgentResponse(
this.currentConversation!.agentId,
message
messageContent
);

this.isTyping = false;
@@ -173,6 +183,49 @@ export class Dashboard implements OnInit {
}, 1500);
}


private async processFiles(files: File[]): Promise<AttachedFile[]> {
const attachedFiles: AttachedFile[] = [];

for (const file of files) {
const attachedFile: AttachedFile = {
name: file.name,
size: file.size,
type: file.type,
url: URL.createObjectURL(file)
};

if (file.type.startsWith('image/')) {
try {
attachedFile.preview = await this.generateImagePreview(file);
} catch (error) {
console.error('Erreur lors de la génération de la preview:', error);
}
}

attachedFiles.push(attachedFile);
}

return attachedFiles;
}

private generateImagePreview(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = (e: ProgressEvent<FileReader>) => {
if (e.target?.result) {
resolve(e.target.result as string);
} else {
reject(new Error('Impossible de lire le fichier'));
}
};

reader.onerror = () => reject(new Error('Erreur de lecture du fichier'));
reader.readAsDataURL(file);
});
}

get hasActiveConversation(): boolean {
return this.currentConversation !== null &&
this.currentConversation.messages &&

+ 3
- 2
src/app/services/data.service.ts Zobrazit soubor

@@ -33,7 +33,8 @@ export class DataService {
id: this.generateId(),
title: 'Nouvelle conversation...',
messages: [],
lastUpdate: new Date(),
createdAt: new Date(),
updatedAt:new Date(),
agentId
};

@@ -54,7 +55,7 @@ export class DataService {
};

current.messages.push(newMessage);
current.lastUpdate = new Date();
current.updatedAt = new Date();

// Update title with first user message
if (current.messages.length === 1 && message.sender === 'user') {

Načítá se…
Zrušit
Uložit