| import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; | |||||
| import { | |||||
| Component, | |||||
| Input, | |||||
| Output, | |||||
| EventEmitter, | |||||
| ViewChild, | |||||
| ElementRef, | |||||
| AfterViewChecked, | |||||
| OnDestroy, OnInit | |||||
| } from '@angular/core'; | |||||
| import { CommonModule } from '@angular/common'; | import { CommonModule } from '@angular/common'; | ||||
| import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | ||||
| import { faCopy, faThumbsUp, faThumbsDown, faRotateRight } from '@fortawesome/free-solid-svg-icons'; | import { faCopy, faThumbsUp, faThumbsDown, faRotateRight } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { ChatInput } from '../../components/chat-input/chat-input'; | import { ChatInput } from '../../components/chat-input/chat-input'; | ||||
| import { Conversation } from '../../models/data.model'; | import { Conversation } from '../../models/data.model'; | ||||
| import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | ||||
| import {Subscription} from 'rxjs'; | |||||
| import {DataService} from '../../services/data.service'; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-chat-view', | selector: 'app-chat-view', | ||||
| templateUrl: './chat-view.html', | templateUrl: './chat-view.html', | ||||
| styleUrls: ['./chat-view.scss'] | styleUrls: ['./chat-view.scss'] | ||||
| }) | }) | ||||
| export class ChatView implements AfterViewChecked { | |||||
| export class ChatView implements AfterViewChecked, OnInit, OnDestroy { | |||||
| @ViewChild('messagesContainer') messagesContainer!: ElementRef; | @ViewChild('messagesContainer') messagesContainer!: ElementRef; | ||||
| @Input() conversation: Conversation | null = null; | @Input() conversation: Conversation | null = null; | ||||
| @Input() isTyping = false; | |||||
| @Input() agentName = 'Assistant'; | @Input() agentName = 'Assistant'; | ||||
| @Output() messageSent = new EventEmitter<{ message: string; files: File[] }>(); | @Output() messageSent = new EventEmitter<{ message: string; files: File[] }>(); | ||||
| faCopy = faCopy; | faCopy = faCopy; | ||||
| isTyping = false; | |||||
| private isTypingSubscription?: Subscription; | |||||
| private shouldScrollToBottom = false; | private shouldScrollToBottom = false; | ||||
| constructor(private sanitizer: DomSanitizer) {} | |||||
| constructor( | |||||
| private sanitizer: DomSanitizer, | |||||
| private dataService: DataService | |||||
| ) {} | |||||
| ngOnInit(): void { | |||||
| this.isTypingSubscription = this.dataService.getIsTyping().subscribe( | |||||
| isTyping => { | |||||
| this.isTyping = isTyping; | |||||
| if (isTyping) { | |||||
| this.shouldScrollToBottom = true; | |||||
| } | |||||
| } | |||||
| ); | |||||
| } | |||||
| ngAfterViewChecked(): void { | ngAfterViewChecked(): void { | ||||
| if (this.shouldScrollToBottom) { | if (this.shouldScrollToBottom) { | ||||
| this.scrollToBottom(); | this.scrollToBottom(); | ||||
| this.shouldScrollToBottom = false; | this.shouldScrollToBottom = false; | ||||
| } | } | ||||
| } | } | ||||
| ngOnDestroy(): void { | |||||
| this.isTypingSubscription?.unsubscribe(); | |||||
| } | |||||
| onMessageSentV1(data: { message: string; files: File[] }): void { | |||||
| this.messageSent.emit(data); | |||||
| this.shouldScrollToBottom = true; | |||||
| } | |||||
| onMessageSent(data: { message: string; files: File[] }): void { | onMessageSent(data: { message: string; files: File[] }): void { | ||||
| this.messageSent.emit(data); | |||||
| if (!this.conversation) return; | |||||
| //this.messageSent.emit(data); | |||||
| this.dataService.addMessage({ | |||||
| content: data.message, | |||||
| sender: 'user', | |||||
| files: data.files.length > 0 ? data.files.map(f => ({ | |||||
| name: f.name, | |||||
| size: f.size, | |||||
| type: f.type | |||||
| })) : undefined | |||||
| }); | |||||
| this.dataService.sendMessageToGroq(this.conversation.agentId, data.message); | |||||
| this.shouldScrollToBottom = true; | this.shouldScrollToBottom = true; | ||||
| } | } | ||||
| this.copyMessage.emit(content); | this.copyMessage.emit(content); | ||||
| } | } | ||||
| onRegenerateMessage(): void { | |||||
| onRegenerateMessageV1(): void { | |||||
| this.regenerateMessage.emit(); | this.regenerateMessage.emit(); | ||||
| } | } | ||||
| onRegenerateMessage(): void { | |||||
| if (!this.conversation || this.conversation.messages.length < 2) return; | |||||
| const messages = this.conversation.messages; | |||||
| const lastUserMessageIndex = messages.map(m => m.sender).lastIndexOf('user'); | |||||
| if (lastUserMessageIndex === -1) return; | |||||
| const lastUserMessage = messages[lastUserMessageIndex]; | |||||
| if (messages[messages.length - 1].sender === 'agent') { | |||||
| messages.pop(); | |||||
| } | |||||
| this.dataService.sendMessageToGroq(this.conversation.agentId, lastUserMessage.content); | |||||
| //this.regenerateMessage.emit(); | |||||
| } | |||||
| getFileIcon(type: string): SafeHtml { | getFileIcon(type: string): SafeHtml { | ||||
| let svgIcon = ''; | let svgIcon = ''; | ||||
| import { BehaviorSubject, Observable } from 'rxjs'; | import { BehaviorSubject, Observable } from 'rxjs'; | ||||
| import { Agent, Conversation, Message, Project } from '../models/data.model'; | import { Agent, Conversation, Message, Project } from '../models/data.model'; | ||||
| import {MOCK_AGENTS, MOCK_CONVERSATIONS, MOCK_PROJECTS} from '../data/mock-data'; | import {MOCK_AGENTS, MOCK_CONVERSATIONS, MOCK_PROJECTS} from '../data/mock-data'; | ||||
| import { GroqService, GroqMessage } from './groq.service'; | |||||
| @Injectable({ | @Injectable({ | ||||
| providedIn: 'root' | providedIn: 'root' | ||||
| private conversations$ = new BehaviorSubject<Conversation[]>(MOCK_CONVERSATIONS); | private conversations$ = new BehaviorSubject<Conversation[]>(MOCK_CONVERSATIONS); | ||||
| private currentConversation$ = new BehaviorSubject<Conversation | null>(null); | private currentConversation$ = new BehaviorSubject<Conversation | null>(null); | ||||
| private projects$ = new BehaviorSubject<Project[]>(MOCK_PROJECTS); | private projects$ = new BehaviorSubject<Project[]>(MOCK_PROJECTS); | ||||
| private isTyping$ = new BehaviorSubject<boolean>(false); | |||||
| constructor(private groqService: GroqService) {} | |||||
| getAgents(): Observable<Agent[]> { | getAgents(): Observable<Agent[]> { | ||||
| return this.agents$.asObservable(); | return this.agents$.asObservable(); | ||||
| return this.projects$.asObservable(); | return this.projects$.asObservable(); | ||||
| } | } | ||||
| getIsTyping(): Observable<boolean> { | |||||
| return this.isTyping$.asObservable(); | |||||
| } | |||||
| createNewConversation(agentId: string): void { | createNewConversation(agentId: string): void { | ||||
| const conversation: Conversation = { | const conversation: Conversation = { | ||||
| id: this.generateId(), | id: this.generateId(), | ||||
| title: 'Nouvelle conversation...', | title: 'Nouvelle conversation...', | ||||
| messages: [], | messages: [], | ||||
| createdAt: new Date(), | createdAt: new Date(), | ||||
| updatedAt:new Date(), | |||||
| updatedAt: new Date(), | |||||
| agentId | agentId | ||||
| }; | }; | ||||
| this.conversations$.next([...conversations]); | this.conversations$.next([...conversations]); | ||||
| } | } | ||||
| } | } | ||||
| resetCurrentConversation(): void { | resetCurrentConversation(): void { | ||||
| this.currentConversation$.next(null); | this.currentConversation$.next(null); | ||||
| } | } | ||||
| private generateId(): string { | private generateId(): string { | ||||
| return Math.random().toString(36).substr(2, 9); | return Math.random().toString(36).substr(2, 9); | ||||
| } | } | ||||
| sendMessageToGroq(agentId: string, userMessage: string): void { | |||||
| const current = this.currentConversation$.value; | |||||
| if (!current) return; | |||||
| if (!this.groqService.isConfigured()) { | |||||
| console.error('Groq API key not configured'); | |||||
| this.addMessage({ | |||||
| content: 'Erreur : Clé API Groq non configurée. Veuillez configurer votre clé API dans groq.service.ts', | |||||
| sender: 'agent', | |||||
| }); | |||||
| return; | |||||
| } | |||||
| const agent = this.agents$.value.find(a => a.id === agentId); | |||||
| const agentName = agent?.name || 'Assistant'; | |||||
| const agentDescription = agent?.description || 'un assistant IA utile'; | |||||
| const groqMessages: GroqMessage[] = [ | |||||
| { | |||||
| role: 'system', | |||||
| content: `Tu es ${agentName}, ${agentDescription}. Tu réponds en français de manière claire, professionnelle et utile.` | |||||
| } | |||||
| ]; | |||||
| const recentMessages = current.messages.slice(-10); | |||||
| for (const msg of recentMessages) { | |||||
| groqMessages.push({ | |||||
| role: msg.sender === 'user' ? 'user' : 'assistant', | |||||
| content: msg.content | |||||
| }); | |||||
| } | |||||
| groqMessages.push({ | |||||
| role: 'user', | |||||
| content: userMessage | |||||
| }); | |||||
| this.isTyping$.next(true); | |||||
| let fullResponse = ''; | |||||
| let messageId = ''; | |||||
| this.groqService.sendMessageStream( | |||||
| groqMessages, | |||||
| (chunk: string) => { | |||||
| fullResponse += chunk; | |||||
| const current = this.currentConversation$.value; | |||||
| if (!current) return; | |||||
| if (!messageId) { | |||||
| messageId = this.generateId(); | |||||
| const newMessage: Message = { | |||||
| id: messageId, | |||||
| content: fullResponse, | |||||
| sender: 'agent', | |||||
| timestamp: new Date() | |||||
| }; | |||||
| current.messages.push(newMessage); | |||||
| } else { | |||||
| const messageIndex = current.messages.findIndex(m => m.id === messageId); | |||||
| if (messageIndex !== -1) { | |||||
| current.messages[messageIndex].content = fullResponse; | |||||
| } | |||||
| } | |||||
| current.updatedAt = new Date(); | |||||
| this.currentConversation$.next({ ...current }); | |||||
| this.updateConversationsList(current); | |||||
| }, | |||||
| () => { | |||||
| this.isTyping$.next(false); | |||||
| console.log('Réponse complète reçue de Groq'); | |||||
| }, | |||||
| (error: any) => { | |||||
| this.isTyping$.next(false); | |||||
| console.error('Erreur Groq:', error); | |||||
| this.addMessage({ | |||||
| content: `Désolé, une erreur est survenue lors de la communication avec l'IA : ${error.message || 'Erreur inconnue'}`, | |||||
| sender: 'agent', | |||||
| }); | |||||
| } | |||||
| ); | |||||
| } | |||||
| simulateAgentResponse(agentId: string, userMessage: string): void { | simulateAgentResponse(agentId: string, userMessage: string): void { | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| const agent = this.agents$.value.find(a => a.id === agentId); | const agent = this.agents$.value.find(a => a.id === agentId); |
| import { Injectable } from '@angular/core'; | |||||
| import { HttpClient, HttpHeaders } from '@angular/common/http'; | |||||
| import { Observable } from 'rxjs'; | |||||
| export interface GroqMessage { | |||||
| role: 'system' | 'user' | 'assistant'; | |||||
| content: string; | |||||
| } | |||||
| export interface GroqResponse { | |||||
| id: string; | |||||
| object: string; | |||||
| created: number; | |||||
| model: string; | |||||
| choices: Array<{ | |||||
| index: number; | |||||
| message: { | |||||
| role: string; | |||||
| content: string; | |||||
| }; | |||||
| finish_reason: string; | |||||
| }>; | |||||
| usage: { | |||||
| prompt_tokens: number; | |||||
| completion_tokens: number; | |||||
| total_tokens: number; | |||||
| }; | |||||
| } | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class GroqService { | |||||
| private readonly API_URL = 'https://api.groq.com/openai/v1/chat/completions'; | |||||
| private readonly API_KEY = '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'; // Remplacez par votre clé API | |||||
| constructor(private http: HttpClient) {} | |||||
| /** | |||||
| * Envoie un message au chatbot Groq | |||||
| * @param messages Historique complet de la conversation | |||||
| * @param model Modèle à utiliser (par défaut: llama-3.3-70b-versatile) | |||||
| * @returns Observable avec la réponse de Groq | |||||
| */ | |||||
| sendMessage( | |||||
| messages: GroqMessage[], | |||||
| model: string = 'openai/gpt-oss-120b' | |||||
| ): Observable<GroqResponse> { | |||||
| const headers = new HttpHeaders({ | |||||
| 'Content-Type': 'application/json', | |||||
| 'Authorization': `Bearer ${this.API_KEY}` | |||||
| }); | |||||
| const body = { | |||||
| model: model, | |||||
| messages: messages, | |||||
| temperature: 0.7, | |||||
| max_tokens: 2048, | |||||
| top_p: 1, | |||||
| stream: false | |||||
| }; | |||||
| return this.http.post<GroqResponse>(this.API_URL, body, { headers }); | |||||
| } | |||||
| /** | |||||
| * Envoie un message avec streaming (affichage progressif) | |||||
| * @param messages Historique de la conversation | |||||
| * @param onChunk Callback appelé pour chaque morceau de texte reçu | |||||
| * @param onComplete Callback appelé quand la génération est terminée | |||||
| * @param onError Callback appelé en cas d'erreur | |||||
| */ | |||||
| sendMessageStream( | |||||
| messages: GroqMessage[], | |||||
| onChunk: (text: string) => void, | |||||
| onComplete: () => void, | |||||
| onError: (error: any) => void | |||||
| ): void { | |||||
| const headers = new HttpHeaders({ | |||||
| 'Content-Type': 'application/json', | |||||
| 'Authorization': `Bearer ${this.API_KEY}` | |||||
| }); | |||||
| const body = { | |||||
| model: 'openai/gpt-oss-120b', | |||||
| messages: messages, | |||||
| temperature: 0.7, | |||||
| max_tokens: 2048, | |||||
| stream: true | |||||
| }; | |||||
| fetch(this.API_URL, { | |||||
| method: 'POST', | |||||
| headers: { | |||||
| 'Content-Type': 'application/json', | |||||
| 'Authorization': `Bearer ${this.API_KEY}` | |||||
| }, | |||||
| body: JSON.stringify(body) | |||||
| }) | |||||
| .then(response => { | |||||
| if (!response.ok) { | |||||
| throw new Error(`HTTP error! status: ${response.status}`); | |||||
| } | |||||
| return response.body; | |||||
| }) | |||||
| .then(body => { | |||||
| const reader = body!.getReader(); | |||||
| const decoder = new TextDecoder(); | |||||
| const processStream = ({ done, value }: ReadableStreamReadResult<Uint8Array>): Promise<void> => { | |||||
| if (done) { | |||||
| onComplete(); | |||||
| return Promise.resolve(); | |||||
| } | |||||
| const chunk = decoder.decode(value); | |||||
| const lines = chunk.split('\n').filter(line => line.trim() !== ''); | |||||
| for (const line of lines) { | |||||
| if (line.startsWith('data: ')) { | |||||
| const data = line.slice(6); | |||||
| if (data === '[DONE]') { | |||||
| onComplete(); | |||||
| return Promise.resolve(); | |||||
| } | |||||
| try { | |||||
| const parsed = JSON.parse(data); | |||||
| const content = parsed.choices[0]?.delta?.content; | |||||
| if (content) { | |||||
| onChunk(content); | |||||
| } | |||||
| } catch (e) { | |||||
| console.error('Erreur de parsing JSON:', e); | |||||
| } | |||||
| } | |||||
| } | |||||
| return reader.read().then(processStream); | |||||
| }; | |||||
| return reader.read().then(processStream); | |||||
| }) | |||||
| .catch(error => { | |||||
| console.error('Erreur lors de l\'appel à l\'API:', error); | |||||
| onError(error); | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * Vérifie si la clé API est configurée | |||||
| */ | |||||
| isConfigured(): boolean { | |||||
| return true; | |||||
| } | |||||
| } |