| @@ -0,0 +1,9 @@ | |||
| <div class="agent-card" (click)="onSelect()"> | |||
| <div class="agent-icon"> | |||
| <fa-icon [icon]="getIcon()"></fa-icon> | |||
| </div> | |||
| <div class="agent-content"> | |||
| <h3 class="agent-name">{{ agent.name }}</h3> | |||
| <p class="agent-description">{{ agent.description }}</p> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,105 @@ | |||
| .agent-card { | |||
| display: flex; | |||
| flex-direction: row; | |||
| gap: 16px; | |||
| padding: 20px 20px 50px 20px; | |||
| background: #ffffff; | |||
| border: 1px solid #e5e7eb; | |||
| border-radius: 12px; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| min-height: 100px; | |||
| align-items: flex-start; | |||
| &:hover { | |||
| border-color: #3498db; | |||
| box-shadow: 0 4px 12px rgba(52, 152, 219, 0.1); | |||
| transform: translateY(-2px); | |||
| .agent-icon { | |||
| background-color: #ecf9ff; | |||
| transform: scale(1.05); | |||
| fa-icon { | |||
| color: #3498db; | |||
| } | |||
| } | |||
| } | |||
| &:active { | |||
| transform: translateY(0); | |||
| } | |||
| .agent-icon { | |||
| flex-shrink: 0; | |||
| width: 48px; | |||
| height: 48px; | |||
| border-radius: 10px; | |||
| background-color: #ecf9ff; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| transition: all 0.2s ease; | |||
| fa-icon { | |||
| font-size: 24px; | |||
| color: #8abecf; | |||
| transition: color 0.2s ease; | |||
| } | |||
| } | |||
| .agent-content { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 8px; | |||
| min-width: 0; | |||
| .agent-name { | |||
| margin: 0; | |||
| font-size: 16px; | |||
| font-weight: 600; | |||
| color: #1f2937; | |||
| line-height: 1.4; | |||
| } | |||
| .agent-description { | |||
| margin: 0; | |||
| font-size: 14px; | |||
| font-weight: 400; | |||
| color: #6b7280; | |||
| line-height: 1.5; | |||
| display: -webkit-box; | |||
| -webkit-line-clamp: 2; | |||
| -webkit-box-orient: vertical; | |||
| overflow: hidden; | |||
| } | |||
| } | |||
| } | |||
| // Responsive | |||
| @media (max-width: 768px) { | |||
| .agent-card { | |||
| padding: 16px; | |||
| .agent-icon { | |||
| width: 40px; | |||
| height: 40px; | |||
| fa-icon { | |||
| font-size: 20px; | |||
| } | |||
| } | |||
| .agent-content { | |||
| .agent-name { | |||
| font-size: 15px; | |||
| } | |||
| .agent-description { | |||
| font-size: 13px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { AgentCard } from './agent-card'; | |||
| describe('AgentCard', () => { | |||
| let component: AgentCard; | |||
| let fixture: ComponentFixture<AgentCard>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| imports: [AgentCard] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(AgentCard); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,38 @@ | |||
| import { Component, Input, Output, EventEmitter } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | |||
| import { | |||
| faEdit, | |||
| faAlignLeft, | |||
| faFileAlt, | |||
| faEnvelope, | |||
| IconDefinition | |||
| } from '@fortawesome/free-solid-svg-icons'; | |||
| import { Agent } from '../../models/data.model'; | |||
| @Component({ | |||
| selector: 'app-agent-card', | |||
| standalone: true, | |||
| imports: [CommonModule, FontAwesomeModule], | |||
| templateUrl: './agent-card.html', | |||
| styleUrls: ['./agent-card.scss'] | |||
| }) | |||
| export class AgentCard { | |||
| @Input() agent!: Agent; | |||
| @Output() selected = new EventEmitter<Agent>(); | |||
| private iconMap: { [key: string]: IconDefinition } = { | |||
| 'edit': faEdit, | |||
| 'align-left': faAlignLeft, | |||
| 'file-alt': faFileAlt, | |||
| 'envelope': faEnvelope | |||
| }; | |||
| getIcon(): IconDefinition { | |||
| return this.iconMap[this.agent.icon] || faEdit; | |||
| } | |||
| onSelect(): void { | |||
| this.selected.emit(this.agent); | |||
| } | |||
| } | |||
| @@ -0,0 +1,37 @@ | |||
| <div class="chat-input-container"> | |||
| <div class="input-wrapper"> | |||
| <textarea | |||
| #messageInput | |||
| [(ngModel)]="message" | |||
| placeholder="Nouvelle conversation..." | |||
| rows="1" | |||
| [disabled]="disabled" | |||
| (input)="onInputChange()" | |||
| ></textarea> | |||
| <div class="actions-row"> | |||
| <button class="attach-button" (click)="onAttach()" [attr.aria-label]="'Importer un fichier'"> | |||
| <fa-icon [icon]="faPaperclip"></fa-icon> | |||
| <span>Importer un fichier</span> | |||
| </button> | |||
| <div class="right-actions"> | |||
| <button class="voice-button" (click)="onVoice()" [attr.aria-label]="'Message vocal'"> | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-soundwave" viewBox="0 0 16 16"> | |||
| <path fill-rule="evenodd" d="M8.5 2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-1 0v-11a.5.5 0 0 1 .5-.5m-2 2a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5m4 0a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5m-6 1.5A.5.5 0 0 1 5 6v4a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m8 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m-10 1A.5.5 0 0 1 3 7v2a.5.5 0 0 1-1 0V7a.5.5 0 0 1 .5-.5m12 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0V7a.5.5 0 0 1 .5-.5"/> | |||
| </svg> | |||
| </button> | |||
| <button | |||
| class="send-button" | |||
| (click)="onSend()" | |||
| [disabled]="!message.trim() || 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> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,198 @@ | |||
| .chat-input-container { | |||
| width: 100%; | |||
| padding: 16px 24px; | |||
| background: transparent; | |||
| display: flex; | |||
| justify-content: center; | |||
| .input-wrapper { | |||
| width: 100%; | |||
| max-width: 1000px; | |||
| background: #ffffff; | |||
| border: 1px solid #e5e7eb; | |||
| border-radius: 16px; | |||
| padding: 16px 20px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 12px; | |||
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); | |||
| transition: all 0.2s ease; | |||
| &:focus-within { | |||
| border-color: #3498db; | |||
| box-shadow: 0 4px 12px rgba(52, 152, 219, 0.12); | |||
| } | |||
| textarea { | |||
| width: 100%; | |||
| border: none; | |||
| outline: none; | |||
| resize: none; | |||
| font-size: 15px; | |||
| font-family: inherit; | |||
| color: #1f2937; | |||
| background: transparent; | |||
| min-height: 24px; | |||
| max-height: 200px; | |||
| line-height: 1.5; | |||
| padding: 0; | |||
| &::placeholder { | |||
| color: #9ca3af; | |||
| } | |||
| &:disabled { | |||
| opacity: 0.6; | |||
| cursor: not-allowed; | |||
| } | |||
| } | |||
| .actions-row { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| padding-top: 4px; | |||
| .attach-button { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 8px; | |||
| padding: 0; | |||
| background: transparent; | |||
| border: none; | |||
| color: #6b7280; | |||
| cursor: pointer; | |||
| font-size: 13px; | |||
| font-weight: 500; | |||
| transition: color 0.2s ease; | |||
| &:hover { | |||
| color: #3498db; | |||
| } | |||
| fa-icon { | |||
| font-size: 16px; | |||
| } | |||
| span { | |||
| @media (max-width: 480px) { | |||
| display: none; | |||
| } | |||
| } | |||
| } | |||
| .right-actions { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 8px; | |||
| .voice-button { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 36px; | |||
| height: 36px; | |||
| background: transparent; | |||
| border: none; | |||
| color: #6b7280; | |||
| cursor: pointer; | |||
| border-radius: 8px; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| background: #f3f4f6; | |||
| color: #1f2937; | |||
| } | |||
| fa-icon { | |||
| font-size: 18px; | |||
| } | |||
| } | |||
| .send-button { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 36px; | |||
| height: 36px; | |||
| background: #f63e63; | |||
| border: none; | |||
| color: #ffffff; | |||
| cursor: pointer; | |||
| border-radius: 8px; | |||
| transition: all 0.2s ease; | |||
| &:hover:not(:disabled) { | |||
| transform: scale(1.05); | |||
| box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); | |||
| } | |||
| &:active:not(:disabled) { | |||
| transform: scale(0.98); | |||
| } | |||
| &:disabled { | |||
| background: #e5e7eb; | |||
| cursor: not-allowed; | |||
| opacity: 0.6; | |||
| fa-icon { | |||
| color: #9ca3af; | |||
| } | |||
| } | |||
| fa-icon { | |||
| font-size: 16px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // Responsive | |||
| @media (max-width: 768px) { | |||
| .chat-input-container { | |||
| padding: 12px 16px; | |||
| .input-wrapper { | |||
| padding: 12px 16px; | |||
| border-radius: 14px; | |||
| textarea { | |||
| font-size: 14px; | |||
| } | |||
| .actions-row { | |||
| .attach-button { | |||
| font-size: 12px; | |||
| fa-icon { | |||
| font-size: 14px; | |||
| } | |||
| } | |||
| .right-actions { | |||
| gap: 6px; | |||
| .voice-button, | |||
| .send-button { | |||
| width: 32px; | |||
| height: 32px; | |||
| fa-icon { | |||
| font-size: 15px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // Auto-expand textarea | |||
| @media (min-width: 769px) { | |||
| .chat-input-container .input-wrapper textarea { | |||
| overflow-y: auto; | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { ChatInput } from './chat-input'; | |||
| describe('ChatInput', () => { | |||
| let component: ChatInput; | |||
| let fixture: ComponentFixture<ChatInput>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| imports: [ChatInput] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(ChatInput); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,56 @@ | |||
| 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, | |||
| } from '@fortawesome/free-solid-svg-icons'; | |||
| @Component({ | |||
| selector: 'app-chat-input', | |||
| standalone: true, | |||
| imports: [CommonModule, FormsModule, FontAwesomeModule], | |||
| templateUrl: './chat-input.html', | |||
| styleUrls: ['./chat-input.scss'] | |||
| }) | |||
| export class ChatInput { | |||
| @Input() disabled = false; | |||
| @Output() messageSent = new EventEmitter<string>(); | |||
| @Output() fileAttached = new EventEmitter<void>(); | |||
| @Output() voiceActivated = new EventEmitter<void>(); | |||
| @ViewChild('messageInput') messageInput!: ElementRef<HTMLTextAreaElement>; | |||
| message = ''; | |||
| faPaperclip = faPaperclip; | |||
| onSend(): void { | |||
| if (this.message.trim() && !this.disabled) { | |||
| this.messageSent.emit(this.message.trim()); | |||
| this.message = ''; | |||
| this.adjustTextareaHeight(); | |||
| } | |||
| } | |||
| onInputChange(): void { | |||
| const textarea = this.messageInput.nativeElement; | |||
| textarea.style.height = 'auto'; | |||
| textarea.style.height = textarea.scrollHeight + 'px'; | |||
| } | |||
| onAttach(): void { | |||
| this.fileAttached.emit(); | |||
| } | |||
| onVoice(): void { | |||
| this.voiceActivated.emit(); | |||
| } | |||
| private adjustTextareaHeight(): void { | |||
| if (this.messageInput) { | |||
| const textarea = this.messageInput.nativeElement; | |||
| textarea.style.height = 'auto'; | |||
| textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,108 @@ | |||
| <div class="sidebar" [class.collapsed]="isCollapsed"> | |||
| <div class="sidebar-header"> | |||
| <div class="logo" *ngIf="!isCollapsed"> | |||
| <h1>AIT</h1> | |||
| </div> | |||
| <button class="toggle-button" (click)="onToggleSidebar()" | |||
| [attr.aria-label]="isCollapsed ? 'Ouvrir la barre latérale' : 'Réduire la barre latérale'"> | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-layout-sidebar" | |||
| viewBox="0 0 16 16"> | |||
| <path | |||
| d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm5-1v12h9a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1zM4 2H2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2z"/> | |||
| </svg> | |||
| </button> | |||
| </div> | |||
| <!-- Bouton nouvelle discussion --> | |||
| <button class="new-chat-button" (click)="onNewChat()" [class.collapsed]="isCollapsed" | |||
| [attr.aria-label]="'Nouvelle discussion'"> | |||
| <fa-icon [icon]="faComments"></fa-icon> | |||
| <span *ngIf="!isCollapsed">Nouvelle discussion</span> | |||
| </button> | |||
| <!-- Section Discussions passées --> | |||
| <div class="sidebar-section"> | |||
| <h3 class="section-title" *ngIf="!isCollapsed">Discussions passées</h3> | |||
| <div class="conversations-section" *ngIf="!isCollapsed"> | |||
| <div class="conversations-list"> | |||
| <div | |||
| *ngFor="let conversation of conversations" | |||
| class="conversation-item" | |||
| [class.active]="currentConversationId === conversation.id" | |||
| (click)="onSelectConversation(conversation.id)" | |||
| > | |||
| <span class="conversation-title">{{ conversation.title }}</span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <!-- Icône en mode réduit | |||
| <button class="sidebar-icon-button" *ngIf="isCollapsed" [attr.aria-label]="'Discussions passées'"> | |||
| <fa-icon [icon]="faComments"></fa-icon> | |||
| </button>--> | |||
| </div> | |||
| <!-- Section Mes projets --> | |||
| <div class="sidebar-section"> | |||
| <h3 class="section-title" *ngIf="!isCollapsed">Mes projets</h3> | |||
| <div class="projects-section" *ngIf="!isCollapsed"> | |||
| <div class="projects-list"> | |||
| <div *ngFor="let project of projects" class="project-wrapper"> | |||
| <div class="project-item" (click)="onToggleProject(project.id)"> | |||
| <fa-icon [icon]="faFolder" class="project-icon"></fa-icon> | |||
| <span class="project-name">{{ project.name }}</span> | |||
| <fa-icon | |||
| [icon]="project.expanded ? faChevronDown : faChevronRight" | |||
| class="expand-icon" | |||
| ></fa-icon> | |||
| </div> | |||
| <div class="project-conversations" | |||
| *ngIf="project.expanded && project.conversations && project.conversations.length > 0"> | |||
| <div | |||
| *ngFor="let conversation of project.conversations" | |||
| class="conversation-item sub-item" | |||
| [class.active]="currentConversationId === conversation.id" | |||
| (click)="onSelectConversation(conversation.id); $event.stopPropagation()" | |||
| > | |||
| <span class="conversation-title">{{ conversation.title }}</span> | |||
| </div> | |||
| </div> | |||
| <div class="project-empty" | |||
| *ngIf="project.expanded && (!project.conversations || project.conversations.length === 0)"> | |||
| <span>Aucune conversation</span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <!-- Icône en mode réduit | |||
| <button class="sidebar-icon-button" *ngIf="isCollapsed" [attr.aria-label]="'Mes projets'"> | |||
| <fa-icon [icon]="faFolder"></fa-icon> | |||
| </button>--> | |||
| </div> | |||
| @if (isCollapsed) { | |||
| <div class="sidebar-footer sidebar-footer-colapsed"> | |||
| <button class="footer-button" [attr.aria-label]="'Aide'"> | |||
| <fa-icon [icon]="faQuestionCircle"></fa-icon> | |||
| </button> | |||
| <button class="footer-button" [attr.aria-label]="'Paramètres'"> | |||
| <fa-icon [icon]="faCog"></fa-icon> | |||
| </button> | |||
| </div> | |||
| } @else { | |||
| <div class="sidebar-footer"> | |||
| <button class="footer-button" [attr.aria-label]="'Aide'"> | |||
| <fa-icon [icon]="faQuestionCircle"></fa-icon> | |||
| </button> | |||
| <button class="footer-button" [attr.aria-label]="'Paramètres'"> | |||
| <fa-icon [icon]="faCog"></fa-icon> | |||
| </button> | |||
| </div> | |||
| } | |||
| </div> | |||
| @@ -0,0 +1,332 @@ | |||
| .sidebar { | |||
| width: 280px; | |||
| height: 100vh; | |||
| background: #243b5d; | |||
| color: #ffffff; | |||
| display: flex; | |||
| flex-direction: column; | |||
| transition: width 0.3s ease; | |||
| position: relative; | |||
| overflow-x: hidden; | |||
| &.collapsed { | |||
| width: 64px; | |||
| .sidebar-section { | |||
| margin-bottom: 8px; | |||
| } | |||
| } | |||
| // Header | |||
| .sidebar-header { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| padding: 16px; | |||
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |||
| min-height: 64px; | |||
| .logo { | |||
| flex: 1; | |||
| h1 { | |||
| margin: 0; | |||
| font-size: 24px; | |||
| font-weight: 700; | |||
| color: #3498db; | |||
| } | |||
| } | |||
| .toggle-button { | |||
| background: transparent; | |||
| border: none; | |||
| color: #ffffff; | |||
| cursor: pointer; | |||
| padding: 8px; | |||
| border-radius: 4px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| transition: background-color 0.2s ease; | |||
| &:hover { | |||
| background-color: rgba(255, 255, 255, 0.1); | |||
| } | |||
| fa-icon { | |||
| font-size: 20px; | |||
| } | |||
| } | |||
| } | |||
| // Bouton nouvelle discussion | |||
| .new-chat-button { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 12px; | |||
| padding: 12px 16px; | |||
| margin: 16px; | |||
| background: #2984a1; | |||
| border: none; | |||
| border-radius: 8px; | |||
| color: #ffffff; | |||
| cursor: pointer; | |||
| font-size: 14px; | |||
| font-weight: 600; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| transform: translateY(-2px); | |||
| box-shadow: 0 4px 12px rgba(52, 152, 219, 0.4); | |||
| } | |||
| &.collapsed { | |||
| justify-content: center; | |||
| padding: 12px; | |||
| margin: 16px 8px; | |||
| fa-icon { | |||
| margin: 0; | |||
| } | |||
| } | |||
| fa-icon { | |||
| font-size: 18px; | |||
| } | |||
| } | |||
| // Sections | |||
| .sidebar-section { | |||
| margin-bottom: 24px; | |||
| max-height: 35vh; | |||
| } | |||
| .section-title { | |||
| font-size: 12px; | |||
| font-weight: 600; | |||
| text-transform: uppercase; | |||
| color: rgba(255, 255, 255, 0.5); | |||
| padding: 0 16px; | |||
| margin: 0 0 12px 0; | |||
| letter-spacing: 0.5px; | |||
| } | |||
| // Icônes en mode réduit | |||
| .sidebar-icon-button { | |||
| width: 100%; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| padding: 14px 0; | |||
| background: transparent; | |||
| border: none; | |||
| color: rgba(255, 255, 255, 0.7); | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| position: relative; | |||
| &::before { | |||
| content: ''; | |||
| position: absolute; | |||
| left: 0; | |||
| top: 50%; | |||
| transform: translateY(-50%); | |||
| width: 3px; | |||
| height: 0; | |||
| background: #3498db; | |||
| transition: height 0.2s ease; | |||
| } | |||
| &:hover { | |||
| background-color: rgba(255, 255, 255, 0.05); | |||
| color: #ffffff; | |||
| &::before { | |||
| height: 60%; | |||
| } | |||
| } | |||
| fa-icon { | |||
| font-size: 20px; | |||
| } | |||
| } | |||
| // Conversations | |||
| .conversations-section { | |||
| max-height: 300px; | |||
| overflow-y: auto; | |||
| padding: 0 8px; | |||
| &::-webkit-scrollbar { | |||
| width: 4px; | |||
| } | |||
| &::-webkit-scrollbar-track { | |||
| background: rgba(255, 255, 255, 0.05); | |||
| border-radius: 2px; | |||
| } | |||
| &::-webkit-scrollbar-thumb { | |||
| background: rgba(255, 255, 255, 0.2); | |||
| border-radius: 2px; | |||
| &:hover { | |||
| background: rgba(255, 255, 255, 0.3); | |||
| } | |||
| } | |||
| } | |||
| .conversations-list { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 4px; | |||
| } | |||
| .conversation-item { | |||
| padding: 10px 12px; | |||
| border-radius: 6px; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| display: flex; | |||
| align-items: center; | |||
| &:hover { | |||
| background-color: rgba(255, 255, 255, 0.1); | |||
| } | |||
| &.active { | |||
| background-color: rgba(52, 152, 219, 0.3); | |||
| border-left: 3px solid #3498db; | |||
| } | |||
| &.sub-item { | |||
| margin-left: 20px; | |||
| padding: 8px 12px; | |||
| font-size: 13px; | |||
| } | |||
| .conversation-title { | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| font-size: 14px; | |||
| } | |||
| } | |||
| // Projets | |||
| .projects-section { | |||
| max-height: 400px; | |||
| overflow-y: auto; | |||
| padding: 0 8px; | |||
| &::-webkit-scrollbar { | |||
| width: 4px; | |||
| } | |||
| &::-webkit-scrollbar-track { | |||
| background: rgba(255, 255, 255, 0.05); | |||
| border-radius: 2px; | |||
| } | |||
| &::-webkit-scrollbar-thumb { | |||
| background: rgba(255, 255, 255, 0.2); | |||
| border-radius: 2px; | |||
| &:hover { | |||
| background: rgba(255, 255, 255, 0.3); | |||
| } | |||
| } | |||
| } | |||
| .projects-list { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 4px; | |||
| } | |||
| .project-wrapper { | |||
| margin-bottom: 4px; | |||
| } | |||
| .project-item { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 10px; | |||
| padding: 10px 12px; | |||
| border-radius: 6px; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| background-color: rgba(255, 255, 255, 0.1); | |||
| } | |||
| .project-icon { | |||
| font-size: 16px; | |||
| } | |||
| .project-name { | |||
| flex: 1; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| font-size: 14px; | |||
| font-weight: 500; | |||
| } | |||
| .expand-icon { | |||
| font-size: 12px; | |||
| color: rgba(255, 255, 255, 0.5); | |||
| } | |||
| } | |||
| .project-conversations { | |||
| margin-top: 4px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 2px; | |||
| } | |||
| .project-empty { | |||
| padding: 8px 12px 8px 42px; | |||
| font-size: 13px; | |||
| color: rgba(255, 255, 255, 0.5); | |||
| font-style: italic; | |||
| } | |||
| // Footer | |||
| .sidebar-footer { | |||
| margin-top: auto; | |||
| padding: 16px; | |||
| border-top: 1px solid rgba(255, 255, 255, 0.1); | |||
| display: flex; | |||
| justify-content: center; | |||
| gap: 12px; | |||
| .footer-button { | |||
| background: transparent; | |||
| border: none; | |||
| color: rgba(255, 255, 255, 0.6); | |||
| cursor: pointer; | |||
| padding: 10px; | |||
| border-radius: 6px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| background-color: rgba(255, 255, 255, 0.1); | |||
| color: #ffffff; | |||
| } | |||
| fa-icon { | |||
| font-size: 18px; | |||
| } | |||
| } | |||
| } | |||
| .sidebar-footer-colapsed{ | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { Sidebar } from './sidebar'; | |||
| describe('Sidebar', () => { | |||
| let component: Sidebar; | |||
| let fixture: ComponentFixture<Sidebar>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| imports: [Sidebar] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(Sidebar); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,72 @@ | |||
| import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | |||
| import { | |||
| faBars, | |||
| faComments, | |||
| faFolder, | |||
| faChevronDown, | |||
| faChevronRight, | |||
| faQuestionCircle, | |||
| faCog | |||
| } from '@fortawesome/free-solid-svg-icons'; | |||
| import { Conversation, Project } from '../../models/data.model'; | |||
| import { DataService } from '../../services/data.service'; | |||
| @Component({ | |||
| selector: 'app-sidebar', | |||
| standalone: true, | |||
| imports: [CommonModule, FontAwesomeModule], | |||
| templateUrl: './sidebar.html', | |||
| styleUrls: ['./sidebar.scss'] | |||
| }) | |||
| export class Sidebar implements OnInit { | |||
| @Input() isCollapsed = false; | |||
| @Output() newChat = new EventEmitter<void>(); | |||
| @Output() conversationSelected = new EventEmitter<string>(); | |||
| @Output() toggleSidebar = new EventEmitter<void>(); | |||
| conversations: Conversation[] = []; | |||
| projects: Project[] = []; | |||
| currentConversationId: string | null = null; | |||
| faComments = faComments; | |||
| faFolder = faFolder; | |||
| faChevronDown = faChevronDown; | |||
| faChevronRight = faChevronRight; | |||
| faQuestionCircle = faQuestionCircle; | |||
| faCog = faCog; | |||
| constructor(private dataService: DataService) {} | |||
| ngOnInit(): void { | |||
| this.dataService.getConversations().subscribe(conversations => { | |||
| this.conversations = conversations; | |||
| }); | |||
| this.dataService.getProjects().subscribe(projects => { | |||
| this.projects = projects; | |||
| }); | |||
| this.dataService.getCurrentConversation().subscribe(conversation => { | |||
| this.currentConversationId = conversation?.id || null; | |||
| }); | |||
| } | |||
| onNewChat(): void { | |||
| this.newChat.emit(); | |||
| } | |||
| onSelectConversation(id: string): void { | |||
| console.log(id); | |||
| this.conversationSelected.emit(id); | |||
| } | |||
| onToggleSidebar(): void { | |||
| this.toggleSidebar.emit(); | |||
| } | |||
| onToggleProject(projectId: string): void { | |||
| this.dataService.toggleProject(projectId); | |||
| } | |||
| } | |||
| @@ -0,0 +1,70 @@ | |||
| <div class="chat-view" [class.active]="conversation"> | |||
| <div class="messages-wrapper"> | |||
| <div class="messages-container" #messagesContainer> | |||
| <div | |||
| *ngFor="let message of conversation?.messages; let isLast = last" | |||
| class="message" | |||
| [class.user-message]="message.sender === 'user'" | |||
| [class.agent-message]="message.sender === 'agent'" | |||
| > | |||
| <!-- Message de l'utilisateur --> | |||
| <div class="message-content" *ngIf="message.sender === 'user'"> | |||
| <div class="message-bubble user-bubble"> | |||
| <p>{{ message.content }}</p> | |||
| </div> | |||
| </div> | |||
| <!-- Message de l'agent --> | |||
| <div class="message-content" *ngIf="message.sender === 'agent'"> | |||
| <div class="message-bubble agent-bubble"> | |||
| <p>{{ message.content }}</p> | |||
| </div> | |||
| <!-- Actions pour les messages de l'agent --> | |||
| <div class="message-actions"> | |||
| <button | |||
| class="action-button" | |||
| (click)="onCopyMessage(message.content)" | |||
| [attr.aria-label]="'Copier le message'" | |||
| > | |||
| <fa-icon [icon]="faCopy"></fa-icon> | |||
| </button> | |||
| <button | |||
| class="action-button" | |||
| (click)="onRegenerateMessage()" | |||
| [attr.aria-label]="'Régénérer la réponse'" | |||
| *ngIf="isLast" | |||
| > | |||
| <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"/> | |||
| </svg> | |||
| </button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <!-- Indicateur de saisie --> | |||
| <div class="message agent-message" *ngIf="isTyping"> | |||
| <div class="message-content"> | |||
| <div class="message-bubble agent-bubble typing-indicator"> | |||
| <span></span> | |||
| <span></span> | |||
| <span></span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <!-- Input de chat --> | |||
| <div class="chat-input-container"> | |||
| <app-chat-input | |||
| (messageSent)="onMessageSent($event)" | |||
| (attach)="onAttach()" | |||
| (voice)="onVoice()" | |||
| ></app-chat-input> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,251 @@ | |||
| .chat-view { | |||
| display: none; | |||
| height: 100%; | |||
| flex-direction: column; | |||
| background: #f5f7fa; | |||
| opacity: 0; | |||
| transform: translateY(20px); | |||
| transition: all 0.4s ease; | |||
| &.active { | |||
| display: flex; | |||
| opacity: 1; | |||
| transform: translateY(0); | |||
| } | |||
| .messages-wrapper { | |||
| flex: 1; | |||
| overflow-y: auto; | |||
| overflow-x: hidden; | |||
| position: relative; | |||
| &::-webkit-scrollbar { | |||
| width: 6px; | |||
| } | |||
| &::-webkit-scrollbar-track { | |||
| background: transparent; | |||
| } | |||
| &::-webkit-scrollbar-thumb { | |||
| background: #cbd5e1; | |||
| border-radius: 3px; | |||
| &:hover { | |||
| background: #94a3b8; | |||
| } | |||
| } | |||
| .messages-container { | |||
| max-width: 800px; | |||
| margin: 0 auto; | |||
| padding: 32px 24px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 24px; | |||
| .message { | |||
| display: flex; | |||
| flex-direction: column; | |||
| animation: messageSlideIn 0.3s ease; | |||
| &.user-message { | |||
| align-items: flex-end; | |||
| .message-content { | |||
| align-items: flex-end; | |||
| } | |||
| } | |||
| &.agent-message { | |||
| align-items: flex-start; | |||
| .message-content { | |||
| align-items: flex-start; | |||
| } | |||
| } | |||
| .message-content { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 8px; | |||
| max-width: 85%; | |||
| @media (min-width: 768px) { | |||
| max-width: 75%; | |||
| } | |||
| .message-bubble { | |||
| padding: 16px 20px; | |||
| border-radius: 16px; | |||
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |||
| } | |||
| p { | |||
| margin: 0; | |||
| line-height: 1.6; | |||
| font-size: 15px; | |||
| word-wrap: break-word; | |||
| white-space: pre-wrap; | |||
| color: #1f2937; | |||
| } | |||
| &.user-bubble { | |||
| background: white; | |||
| border-bottom-right-radius: 4px; | |||
| } | |||
| &.agent-bubble { | |||
| //background: #ffffff; | |||
| //border: 1px solid #e5e7eb; | |||
| //border-bottom-left-radius: 4px; | |||
| } | |||
| &.typing-indicator { | |||
| display: flex; | |||
| gap: 6px; | |||
| padding: 20px 24px; | |||
| justify-content: center; | |||
| min-width: 80px; | |||
| span { | |||
| width: 8px; | |||
| height: 8px; | |||
| border-radius: 50%; | |||
| background: #94a3b8; | |||
| animation: typingDot 1.4s infinite; | |||
| &:nth-child(2) { | |||
| animation-delay: 0.2s; | |||
| } | |||
| &:nth-child(3) { | |||
| animation-delay: 0.4s; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .message-actions { | |||
| display: flex; | |||
| gap: 4px; | |||
| transform: translateY(-4px); | |||
| transition: all 0.2s ease; | |||
| padding-left: 4px; | |||
| .action-button { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 32px; | |||
| height: 32px; | |||
| background: transparent; | |||
| border: 1px solid transparent; | |||
| border-radius: 8px; | |||
| color: #6b7280; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| font-size: 14px; | |||
| &:hover { | |||
| background: #f3f4f6; | |||
| border-color: #e5e7eb; | |||
| color: #374151; | |||
| } | |||
| &:active { | |||
| transform: scale(0.95); | |||
| } | |||
| fa-icon { | |||
| font-size: 14px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &:hover .message-actions { | |||
| opacity: 1; | |||
| transform: translateY(0); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .chat-input-container { | |||
| padding: 16px 24px 24px; | |||
| background: #f5f7fa; | |||
| border-top: 1px solid #e5e7eb; | |||
| app-chat-input { | |||
| max-width: 800px; | |||
| margin: 0 auto; | |||
| display: block; | |||
| } | |||
| } | |||
| } | |||
| // Animations | |||
| @keyframes messageSlideIn { | |||
| from { | |||
| opacity: 0; | |||
| transform: translateY(10px); | |||
| } | |||
| to { | |||
| opacity: 1; | |||
| transform: translateY(0); | |||
| } | |||
| } | |||
| @keyframes typingDot { | |||
| 0%, 60%, 100% { | |||
| transform: translateY(0); | |||
| opacity: 0.7; | |||
| } | |||
| 30% { | |||
| transform: translateY(-8px); | |||
| opacity: 1; | |||
| } | |||
| } | |||
| // Responsive | |||
| @media (max-width: 768px) { | |||
| .chat-view { | |||
| .messages-wrapper { | |||
| .messages-container { | |||
| padding: 24px 16px; | |||
| gap: 20px; | |||
| .message { | |||
| .message-content { | |||
| max-width: 90%; | |||
| .message-bubble { | |||
| padding: 14px 16px; | |||
| font-size: 14px; | |||
| } | |||
| .message-actions { | |||
| .action-button { | |||
| width: 28px; | |||
| height: 28px; | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .chat-input-container { | |||
| padding: 12px 16px 16px; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { ChatList } from './chat-list'; | |||
| describe('ChatList', () => { | |||
| let component: ChatList; | |||
| let fixture: ComponentFixture<ChatList>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| imports: [ChatList] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(ChatList); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,79 @@ | |||
| import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterViewChecked } 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 { Conversation } from '../../models/data.model'; | |||
| import {ChatInput} from '../../components/chat-input/chat-input'; | |||
| @Component({ | |||
| selector: 'app-chat-view', | |||
| standalone: true, | |||
| imports: [CommonModule, FontAwesomeModule, ChatInput], | |||
| templateUrl: './chat-view.html', | |||
| styleUrls: ['./chat-view.scss'] | |||
| }) | |||
| export class ChatView implements AfterViewChecked { | |||
| @ViewChild('messagesContainer') messagesContainer!: ElementRef; | |||
| @Input() conversation: Conversation | null = null; | |||
| @Input() isTyping = false; | |||
| @Input() agentName = 'Assistant'; | |||
| @Output() messageSent = new EventEmitter<string>(); | |||
| @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; | |||
| ngAfterViewChecked(): void { | |||
| if (this.shouldScrollToBottom) { | |||
| this.scrollToBottom(); | |||
| this.shouldScrollToBottom = false; | |||
| } | |||
| } | |||
| onMessageSent(message: string): void { | |||
| this.messageSent.emit(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); | |||
| } | |||
| onRegenerateMessage(): void { | |||
| this.regenerateMessage.emit(); | |||
| } | |||
| private scrollToBottom(): void { | |||
| try { | |||
| const messagesElement = this.messagesContainer?.nativeElement; | |||
| if (messagesElement) { | |||
| setTimeout(() => { | |||
| messagesElement.scrollTop = messagesElement.scrollHeight; | |||
| }, 100); | |||
| } | |||
| } catch (err) { | |||
| console.error('Error scrolling to bottom:', err); | |||
| } | |||
| } | |||
| markScrollNeeded(): void { | |||
| this.shouldScrollToBottom = true; | |||
| } | |||
| } | |||
| @@ -0,0 +1,71 @@ | |||
| <div class="dashboard"> | |||
| <app-sidebar | |||
| [isCollapsed]="sidebarCollapsed" | |||
| (newChat)="onNewChat()" | |||
| (conversationSelected)="onConversationSelected($event)" | |||
| (toggleSidebar)="onToggleSidebar()" | |||
| ></app-sidebar> | |||
| <div class="main-content"> | |||
| <div class="content-header"> | |||
| <button | |||
| class="back-button" | |||
| *ngIf="currentConversation" | |||
| (click)="onNewChat()" | |||
| [attr.aria-label]="'Retour'" | |||
| > | |||
| <fa-icon [icon]="faArrowLeft"></fa-icon> | |||
| </button> | |||
| <div class="header-spacer"></div> | |||
| <button class="share-button" [attr.aria-label]="'Partager'"> | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload" 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 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/> | |||
| </svg> </button> | |||
| <button class="login-button">Se connecter</button> | |||
| </div> | |||
| <div class="content-body" #contentBody [class.chat-active]="currentConversation"> | |||
| <!-- Welcome Screen --> | |||
| <div class="welcome-screen" [class.fade-out]="currentConversation"> | |||
| <div class="welcome-header"> | |||
| <h1 class="welcome-title">Bonjour !</h1> | |||
| <p class="welcome-subtitle">Qu'est-ce qu'on fait aujourd'hui ?</p> | |||
| </div> | |||
| <div class="center-input-wrapper" [class.animate-down]="currentConversation"> | |||
| <app-chat-input | |||
| (messageSent)="onMessageSent($event)" | |||
| (attach)="onAttach()" | |||
| (voice)="onVoice()" | |||
| ></app-chat-input> | |||
| </div> | |||
| <div class="agents-grid"> | |||
| <app-agent-card | |||
| *ngFor="let agent of agents" | |||
| [agent]="agent" | |||
| (selected)="onAgentSelected($event)" | |||
| ></app-agent-card> | |||
| </div> | |||
| </div> | |||
| <!-- Chat View Component --> | |||
| @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> | |||
| } | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,286 @@ | |||
| .dashboard { | |||
| display: flex; | |||
| height: 100vh; | |||
| overflow: hidden; | |||
| background: #f5f7fa; | |||
| .main-content { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| overflow: hidden; | |||
| position: relative; | |||
| // 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 { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 40px; | |||
| height: 40px; | |||
| background: transparent; | |||
| border: none; | |||
| border-radius: 10px; | |||
| color: #6b7280; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| margin-right: auto; | |||
| &:hover { | |||
| background: #f3f4f6; | |||
| border-color: #d1d5db; | |||
| color: #1f2937; | |||
| } | |||
| fa-icon { | |||
| font-size: 18px; | |||
| } | |||
| } | |||
| .header-spacer { | |||
| flex: 1; | |||
| } | |||
| .share-button { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 40px; | |||
| height: 40px; | |||
| border: none; | |||
| background: transparent; | |||
| border-radius: 10px; | |||
| color: #6b7280; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| background: #f3f4f6; | |||
| border-color: #d1d5db; | |||
| color: #1f2937; | |||
| } | |||
| fa-icon { | |||
| font-size: 18px; | |||
| } | |||
| } | |||
| .login-button { | |||
| padding: 10px 20px; | |||
| background: #ffffff; | |||
| border-radius: 10px; | |||
| border: none; | |||
| color: #1f2937; | |||
| font-size: 14px; | |||
| font-weight: 600; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| background: #f9fafb; | |||
| border-color: #d1d5db; | |||
| } | |||
| } | |||
| } | |||
| .content-body { | |||
| flex: 1; | |||
| overflow: hidden; | |||
| position: relative; | |||
| display: flex; | |||
| flex-direction: column; | |||
| &::-webkit-scrollbar { | |||
| width: 8px; | |||
| } | |||
| &::-webkit-scrollbar-track { | |||
| background: #f1f1f1; | |||
| } | |||
| &::-webkit-scrollbar-thumb { | |||
| background: #cbd5e1; | |||
| border-radius: 4px; | |||
| &:hover { | |||
| background: #94a3b8; | |||
| } | |||
| } | |||
| // Welcome Screen | |||
| .welcome-screen { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| padding: 40px 24px; | |||
| opacity: 1; | |||
| transition: opacity 0.3s ease; | |||
| overflow-y: auto; | |||
| &.fade-out { | |||
| opacity: 0; | |||
| pointer-events: none; | |||
| } | |||
| .welcome-header { | |||
| text-align: center; | |||
| margin-bottom: 40px; | |||
| animation: fadeInDown 0.6s ease; | |||
| .welcome-title { | |||
| font-size: 48px; | |||
| font-weight: 700; | |||
| color: #1f2937; | |||
| margin: 0 0 12px 0; | |||
| line-height: 1.2; | |||
| @media (max-width: 768px) { | |||
| font-size: 36px; | |||
| } | |||
| } | |||
| .welcome-subtitle { | |||
| font-size: 20px; | |||
| color: #6b7280; | |||
| margin: 0; | |||
| font-weight: 400; | |||
| @media (max-width: 768px) { | |||
| font-size: 18px; | |||
| } | |||
| } | |||
| } | |||
| .center-input-wrapper { | |||
| width: 100%; | |||
| max-width: 800px; | |||
| margin-bottom: 48px; | |||
| animation: fadeInUp 0.6s ease 0.2s both; | |||
| position: relative; | |||
| z-index: 5; | |||
| &.animate-down { | |||
| animation: slideDown 0.6s ease forwards; | |||
| } | |||
| } | |||
| .agents-grid { | |||
| width: 100%; | |||
| max-width: 1000px; | |||
| display: grid; | |||
| grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); | |||
| gap: 16px; | |||
| animation: fadeInUp 0.6s ease 0.4s both; | |||
| @media (max-width: 768px) { | |||
| grid-template-columns: 1fr; | |||
| } | |||
| } | |||
| } | |||
| // Le chat-view prend tout l'espace quand actif | |||
| app-chat-view { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| overflow: hidden; | |||
| } | |||
| &.chat-active { | |||
| .welcome-screen { | |||
| display: none; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // Animations | |||
| @keyframes fadeInDown { | |||
| from { | |||
| opacity: 0; | |||
| transform: translateY(-30px); | |||
| } | |||
| to { | |||
| opacity: 1; | |||
| transform: translateY(0); | |||
| } | |||
| } | |||
| @keyframes fadeInUp { | |||
| from { | |||
| opacity: 0; | |||
| transform: translateY(30px); | |||
| } | |||
| to { | |||
| opacity: 1; | |||
| transform: translateY(0); | |||
| } | |||
| } | |||
| @keyframes slideDown { | |||
| 0% { | |||
| transform: translateY(0); | |||
| opacity: 1; | |||
| } | |||
| 50% { | |||
| opacity: 0.5; | |||
| } | |||
| 100% { | |||
| transform: translateY(calc(100vh - 200px)); | |||
| opacity: 0; | |||
| } | |||
| } | |||
| // Responsive | |||
| @media (max-width: 768px) { | |||
| .dashboard { | |||
| .main-content { | |||
| .content-header { | |||
| padding: 12px 16px; | |||
| .share-button { | |||
| width: 36px; | |||
| height: 36px; | |||
| fa-icon { | |||
| font-size: 16px; | |||
| } | |||
| } | |||
| .login-button { | |||
| padding: 8px 16px; | |||
| font-size: 13px; | |||
| } | |||
| } | |||
| .content-body { | |||
| .welcome-screen { | |||
| padding: 24px 16px; | |||
| .welcome-header { | |||
| margin-bottom: 32px; | |||
| } | |||
| .center-input-wrapper { | |||
| margin-bottom: 32px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { Dashboard } from './dashboard'; | |||
| describe('Dashboard', () => { | |||
| let component: Dashboard; | |||
| let fixture: ComponentFixture<Dashboard>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| imports: [Dashboard] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(Dashboard); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,188 @@ | |||
| import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | |||
| import {faArrowLeft, faShare} from '@fortawesome/free-solid-svg-icons'; | |||
| 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 {ChatView} from '../chat/chat-view'; | |||
| @Component({ | |||
| selector: 'app-dashboard', | |||
| standalone: true, | |||
| imports: [ | |||
| CommonModule, | |||
| FontAwesomeModule, | |||
| Sidebar, | |||
| AgentCard, | |||
| ChatInput, | |||
| ChatView | |||
| ], | |||
| templateUrl: './dashboard.html', | |||
| styleUrls: ['./dashboard.scss'] | |||
| }) | |||
| export class Dashboard implements OnInit { | |||
| @ViewChild('contentBody') contentBody!: ElementRef; | |||
| @ViewChild(ChatView) chatView!: ChatView; | |||
| agents: Agent[] = []; | |||
| currentConversation: Conversation | null = null; | |||
| sidebarCollapsed = false; | |||
| isTyping = false; | |||
| // Icons | |||
| faShare = faShare; | |||
| private animatingInput = false; | |||
| constructor(private dataService: DataService) {} | |||
| ngOnInit(): void { | |||
| this.dataService.getAgents().subscribe(agents => { | |||
| this.agents = agents; | |||
| }); | |||
| this.dataService.getCurrentConversation().subscribe(conversation => { | |||
| const hadConversation = this.currentConversation !== null; | |||
| this.currentConversation = conversation; | |||
| if (hadConversation && conversation && this.chatView) { | |||
| this.chatView.markScrollNeeded(); | |||
| } | |||
| if (conversation !== null) { | |||
| this.isTyping = false; | |||
| } | |||
| }); | |||
| } | |||
| onNewChat(): void { | |||
| this.dataService.resetCurrentConversation(); | |||
| this.animatingInput = false; | |||
| setTimeout(() => { | |||
| if (this.contentBody?.nativeElement) { | |||
| this.contentBody.nativeElement.scrollTop = 0; | |||
| } | |||
| }, 100); | |||
| } | |||
| onAgentSelected(agent: Agent): void { | |||
| this.animatingInput = true; | |||
| setTimeout(() => { | |||
| this.dataService.createNewConversation(agent.id); | |||
| this.animatingInput = false; | |||
| }, 600); | |||
| } | |||
| onConversationSelected(conversationId: string): void { | |||
| this.dataService.selectConversation(conversationId); | |||
| } | |||
| onMessageSent(message: string): void { | |||
| if (!this.currentConversation) { | |||
| this.dataService.createNewConversation('default'); | |||
| setTimeout(() => { | |||
| this.sendMessage(message); | |||
| }, 100); | |||
| } else { | |||
| this.sendMessage(message); | |||
| } | |||
| } | |||
| 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 sendMessage(message: string): void { | |||
| if (!this.currentConversation) return; | |||
| this.dataService.addMessage({ | |||
| content: message, | |||
| sender: 'user', | |||
| }); | |||
| this.isTyping = true; | |||
| if (this.chatView) { | |||
| this.chatView.markScrollNeeded(); | |||
| } | |||
| setTimeout(() => { | |||
| this.dataService.simulateAgentResponse( | |||
| this.currentConversation!.agentId, | |||
| message | |||
| ); | |||
| this.isTyping = false; | |||
| if (this.chatView) { | |||
| this.chatView.markScrollNeeded(); | |||
| } | |||
| }, 1500); | |||
| } | |||
| get hasActiveConversation(): boolean { | |||
| return this.currentConversation !== null && | |||
| this.currentConversation.messages && | |||
| this.currentConversation.messages.length > 0; | |||
| } | |||
| get currentAgentName(): string { | |||
| if (!this.currentConversation) return ''; | |||
| const agent = this.agents.find(a => a.id === this.currentConversation?.agentId); | |||
| return agent?.name || 'Assistant'; | |||
| } | |||
| protected readonly faArrowLeft = faArrowLeft; | |||
| } | |||
| @@ -0,0 +1,97 @@ | |||
| <div class="login-page-container"> | |||
| <div class="login-card"> | |||
| <div class="login-header"> | |||
| <div class="logo">AIT</div> | |||
| <h1 class="login-title">Connectez-vous à votre espace</h1> | |||
| <p class="login-subtitle">Entrez vos identifiants pour continuer.</p> | |||
| </div> | |||
| <div class="login-content"> | |||
| <form [formGroup]="loginForm" (ngSubmit)="login()" class="login-form"> | |||
| <!-- Email Field --> | |||
| <div class="form-field"> | |||
| <label for="email" class="form-label">Email</label> | |||
| <div class="input-wrapper"> | |||
| <input | |||
| id="email" | |||
| type="email" | |||
| formControlName="username" | |||
| class="form-input" | |||
| placeholder="votre.email@exemple.com" | |||
| [class.error]="loginForm.get('email')?.invalid && loginForm.get('email')?.touched" | |||
| > | |||
| <svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |||
| <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path> | |||
| <polyline points="22,6 12,13 2,6"></polyline> | |||
| </svg> | |||
| </div> | |||
| <span class="error-message" *ngIf="loginForm.get('email')?.invalid && loginForm.get('email')?.touched"> | |||
| Veuillez entrer une adresse email valide. | |||
| </span> | |||
| </div> | |||
| <!-- Password Field --> | |||
| <div class="form-field"> | |||
| <label for="password" class="form-label">Mot de passe</label> | |||
| <div class="input-wrapper"> | |||
| <input | |||
| id="password" | |||
| [type]="showPassword ? 'text' : 'password'" | |||
| formControlName="password" | |||
| class="form-input" | |||
| placeholder="••••••••" | |||
| [class.error]="loginForm.get('password')?.invalid && loginForm.get('password')?.touched" | |||
| > | |||
| <svg class="input-icon clickable" | |||
| (click)="togglePasswordVisibility()" | |||
| xmlns="http://www.w3.org/2000/svg" | |||
| width="20" | |||
| height="20" | |||
| viewBox="0 0 24 24" | |||
| fill="none" | |||
| stroke="currentColor" | |||
| stroke-width="2" | |||
| stroke-linecap="round" | |||
| stroke-linejoin="round"> | |||
| <path *ngIf="!showPassword" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> | |||
| <circle *ngIf="!showPassword" cx="12" cy="12" r="3"></circle> | |||
| <path *ngIf="showPassword" d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path> | |||
| <line *ngIf="showPassword" x1="1" y1="1" x2="23" y2="23"></line> | |||
| </svg> | |||
| </div> | |||
| <span class="error-message" *ngIf="loginForm.get('password')?.hasError('required') && loginForm.get('password')?.touched"> | |||
| Le mot de passe est requis. | |||
| </span> | |||
| <span class="error-message" *ngIf="loginForm.get('password')?.hasError('minlength') && loginForm.get('password')?.touched"> | |||
| Le mot de passe doit contenir au moins 6 caractères. | |||
| </span> | |||
| </div> | |||
| <!-- Form Options --> | |||
| <div class="form-options"> | |||
| <label class="checkbox-wrapper"> | |||
| <input type="checkbox" formControlName="rememberMe"> | |||
| <span class="checkbox-label">Se souvenir de moi</span> | |||
| </label> | |||
| <a href="#" class="forgot-password-link">Mot de passe oublié ?</a> | |||
| </div> | |||
| <!-- Submit Button --> | |||
| <button | |||
| type="submit" | |||
| [disabled]="loginForm.invalid" | |||
| class="login-btn" | |||
| > | |||
| <span >Se connecter</span> | |||
| </button> | |||
| </form> | |||
| </div> | |||
| <!-- Footer --> | |||
| <div class="login-footer"> | |||
| <p>Vous n'avez pas de compte ? <a href="#" class="register-link">S'inscrire</a></p> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,266 @@ | |||
| .sidebar { | |||
| width: 280px; | |||
| height: 100vh; | |||
| background: linear-gradient(180deg, #1e3a8a 0%, #312e81 100%); | |||
| display: flex; | |||
| flex-direction: column; | |||
| color: white; | |||
| overflow-y: auto; | |||
| transition: width 0.3s ease; | |||
| &.collapsed { | |||
| width: 70px; | |||
| .sidebar-header { | |||
| justify-content: center; | |||
| } | |||
| .new-chat-button, | |||
| .section-title, | |||
| .conversation-title, | |||
| .project-name { | |||
| display: none; | |||
| } | |||
| } | |||
| } | |||
| .sidebar-header { | |||
| padding: 20px; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |||
| flex-shrink: 0; | |||
| } | |||
| .logo h1 { | |||
| margin: 0; | |||
| font-size: 28px; | |||
| font-weight: 700; | |||
| letter-spacing: 2px; | |||
| } | |||
| .toggle-button { | |||
| background: none; | |||
| border: none; | |||
| color: white; | |||
| cursor: pointer; | |||
| padding: 8px; | |||
| border-radius: 8px; | |||
| transition: background 0.2s ease; | |||
| &:hover { | |||
| background: rgba(255, 255, 255, 0.1); | |||
| } | |||
| fa-icon { | |||
| font-size: 20px; | |||
| } | |||
| } | |||
| .new-chat-button, | |||
| .new-chat-button-icon { | |||
| margin: 20px; | |||
| padding: 14px 20px; | |||
| background: rgba(59, 130, 246, 0.2); | |||
| border: 2px solid rgba(59, 130, 246, 0.3); | |||
| border-radius: 12px; | |||
| color: white; | |||
| cursor: pointer; | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 12px; | |||
| font-size: 15px; | |||
| font-weight: 500; | |||
| transition: all 0.2s ease; | |||
| border: none; | |||
| &:hover { | |||
| background: rgba(59, 130, 246, 0.3); | |||
| transform: translateY(-2px); | |||
| } | |||
| fa-icon { | |||
| font-size: 18px; | |||
| } | |||
| } | |||
| .new-chat-button-icon { | |||
| justify-content: center; | |||
| padding: 14px; | |||
| width: calc(100% - 40px); | |||
| } | |||
| .conversations-section, | |||
| .projects-section { | |||
| padding: 0 20px; | |||
| margin-bottom: 24px; | |||
| flex-shrink: 0; | |||
| } | |||
| .section-title { | |||
| font-size: 13px; | |||
| font-weight: 600; | |||
| text-transform: uppercase; | |||
| letter-spacing: 1px; | |||
| margin: 0 0 12px 0; | |||
| opacity: 0.7; | |||
| } | |||
| .conversations-list { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 4px; | |||
| } | |||
| .conversation-item { | |||
| padding: 10px 12px; | |||
| border-radius: 8px; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| font-size: 14px; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| &:hover { | |||
| background: rgba(255, 255, 255, 0.1); | |||
| } | |||
| &.active { | |||
| background: rgba(59, 130, 246, 0.3); | |||
| font-weight: 500; | |||
| } | |||
| &.sub-item { | |||
| margin-left: 20px; | |||
| font-size: 13px; | |||
| opacity: 0.9; | |||
| } | |||
| } | |||
| .conversation-title { | |||
| display: block; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| .projects-list { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 4px; | |||
| } | |||
| .project-wrapper { | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| .project-item { | |||
| padding: 10px 12px; | |||
| border-radius: 8px; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 10px; | |||
| font-size: 14px; | |||
| &:hover { | |||
| background: rgba(255, 255, 255, 0.1); | |||
| } | |||
| } | |||
| .project-icon { | |||
| opacity: 0.7; | |||
| flex-shrink: 0; | |||
| } | |||
| .project-name { | |||
| flex: 1; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| .expand-icon { | |||
| opacity: 0.5; | |||
| font-size: 12px; | |||
| color: #60a5fa; | |||
| flex-shrink: 0; | |||
| transition: transform 0.2s ease; | |||
| } | |||
| .project-conversations { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 4px; | |||
| margin-top: 4px; | |||
| animation: slideDown 0.2s ease; | |||
| } | |||
| .project-empty { | |||
| padding: 8px 12px 8px 44px; | |||
| font-size: 12px; | |||
| opacity: 0.5; | |||
| font-style: italic; | |||
| } | |||
| @keyframes slideDown { | |||
| from { | |||
| opacity: 0; | |||
| transform: translateY(-5px); | |||
| } | |||
| to { | |||
| opacity: 1; | |||
| transform: translateY(0); | |||
| } | |||
| } | |||
| .sidebar-footer { | |||
| margin-top: auto; | |||
| padding: 20px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 12px; | |||
| border-top: 1px solid rgba(255, 255, 255, 0.1); | |||
| flex-shrink: 0; | |||
| } | |||
| .footer-button { | |||
| width: 100%; | |||
| padding: 12px; | |||
| background: none; | |||
| border: 1px solid rgba(255, 255, 255, 0.2); | |||
| border-radius: 8px; | |||
| color: white; | |||
| cursor: pointer; | |||
| transition: all 0.2s ease; | |||
| &:hover { | |||
| background: rgba(255, 255, 255, 0.1); | |||
| } | |||
| fa-icon { | |||
| font-size: 18px; | |||
| } | |||
| } | |||
| /* Scrollbar styling */ | |||
| .sidebar::-webkit-scrollbar { | |||
| width: 6px; | |||
| } | |||
| .sidebar::-webkit-scrollbar-track { | |||
| background: rgba(0, 0, 0, 0.1); | |||
| } | |||
| .sidebar::-webkit-scrollbar-thumb { | |||
| background: rgba(255, 255, 255, 0.2); | |||
| border-radius: 3px; | |||
| &:hover { | |||
| background: rgba(255, 255, 255, 0.3); | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { Login } from './login'; | |||
| describe('Login', () => { | |||
| let component: Login; | |||
| let fixture: ComponentFixture<Login>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| imports: [Login] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(Login); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,69 @@ | |||
| import {Component, ElementRef, inject, OnInit, signal, viewChild} from '@angular/core'; | |||
| import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; | |||
| import { Router } from '@angular/router'; | |||
| import {NgIf} from '@angular/common'; | |||
| import {LoginService} from './login.service'; | |||
| import {AccountService} from '../../core/auth/account.service'; | |||
| @Component({ | |||
| selector: 'app-login', | |||
| templateUrl: './login.html', | |||
| imports: [ | |||
| ReactiveFormsModule, | |||
| NgIf | |||
| ], | |||
| styleUrl: './login.scss' | |||
| }) | |||
| export class Login implements OnInit { | |||
| username = viewChild.required<ElementRef>('username'); | |||
| authenticationError = signal(false); | |||
| loginForm = new FormGroup({ | |||
| username: new FormControl('', { nonNullable: true, validators: [Validators.required] }), | |||
| password: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.minLength(4)] }), | |||
| rememberMe: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), | |||
| }); | |||
| private readonly accountService = inject(AccountService); | |||
| private readonly loginService = inject(LoginService); | |||
| private readonly router = inject(Router); | |||
| protected showPassword: boolean = false; | |||
| ngOnInit(): void { | |||
| // if already authenticated then navigate to home page | |||
| /* this.accountService.identity().subscribe(() => { | |||
| if (this.accountService.isAuthenticated()) { | |||
| this.router.navigate(['']); | |||
| } | |||
| });*/ | |||
| } | |||
| /** | |||
| * Bascule la visibilité du mot de passe | |||
| */ | |||
| togglePasswordVisibility(): void { | |||
| this.showPassword = !this.showPassword; | |||
| } | |||
| /** | |||
| * Gère la soumission du formulaire | |||
| */ | |||
| login(): void { | |||
| this.loginService.login(this.loginForm.getRawValue()).subscribe({ | |||
| next: () => { | |||
| this.authenticationError.set(false); | |||
| if (!this.router.getCurrentNavigation()) { | |||
| // There were no routing during login (eg from navigationToStoredUrl) | |||
| this.router.navigate(['chantier/dashboard']); | |||
| } | |||
| }, | |||
| error: () => this.authenticationError.set(true), | |||
| }); | |||
| } | |||
| } | |||