| @@ -1,10 +1,21 @@ | |||
| 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 { 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 {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | |||
| import {Subscription} from 'rxjs'; | |||
| import {DataService} from '../../services/data.service'; | |||
| @Component({ | |||
| selector: 'app-chat-view', | |||
| @@ -13,11 +24,10 @@ import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | |||
| templateUrl: './chat-view.html', | |||
| styleUrls: ['./chat-view.scss'] | |||
| }) | |||
| export class ChatView implements AfterViewChecked { | |||
| export class ChatView implements AfterViewChecked, OnInit, OnDestroy { | |||
| @ViewChild('messagesContainer') messagesContainer!: ElementRef; | |||
| @Input() conversation: Conversation | null = null; | |||
| @Input() isTyping = false; | |||
| @Input() agentName = 'Assistant'; | |||
| @Output() messageSent = new EventEmitter<{ message: string; files: File[] }>(); | |||
| @@ -28,18 +38,54 @@ export class ChatView implements AfterViewChecked { | |||
| faCopy = faCopy; | |||
| isTyping = false; | |||
| private isTypingSubscription?: Subscription; | |||
| 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 { | |||
| if (this.shouldScrollToBottom) { | |||
| this.scrollToBottom(); | |||
| 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 { | |||
| 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; | |||
| } | |||
| @@ -56,10 +102,29 @@ export class ChatView implements AfterViewChecked { | |||
| this.copyMessage.emit(content); | |||
| } | |||
| onRegenerateMessage(): void { | |||
| onRegenerateMessageV1(): void { | |||
| 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 { | |||
| let svgIcon = ''; | |||
| @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; | |||
| import { BehaviorSubject, Observable } from 'rxjs'; | |||
| import { Agent, Conversation, Message, Project } from '../models/data.model'; | |||
| import {MOCK_AGENTS, MOCK_CONVERSATIONS, MOCK_PROJECTS} from '../data/mock-data'; | |||
| import { GroqService, GroqMessage } from './groq.service'; | |||
| @Injectable({ | |||
| providedIn: 'root' | |||
| @@ -11,6 +12,9 @@ export class DataService { | |||
| private conversations$ = new BehaviorSubject<Conversation[]>(MOCK_CONVERSATIONS); | |||
| private currentConversation$ = new BehaviorSubject<Conversation | null>(null); | |||
| private projects$ = new BehaviorSubject<Project[]>(MOCK_PROJECTS); | |||
| private isTyping$ = new BehaviorSubject<boolean>(false); | |||
| constructor(private groqService: GroqService) {} | |||
| getAgents(): Observable<Agent[]> { | |||
| return this.agents$.asObservable(); | |||
| @@ -28,13 +32,17 @@ export class DataService { | |||
| return this.projects$.asObservable(); | |||
| } | |||
| getIsTyping(): Observable<boolean> { | |||
| return this.isTyping$.asObservable(); | |||
| } | |||
| createNewConversation(agentId: string): void { | |||
| const conversation: Conversation = { | |||
| id: this.generateId(), | |||
| title: 'Nouvelle conversation...', | |||
| messages: [], | |||
| createdAt: new Date(), | |||
| updatedAt:new Date(), | |||
| updatedAt: new Date(), | |||
| agentId | |||
| }; | |||
| @@ -91,13 +99,103 @@ export class DataService { | |||
| this.conversations$.next([...conversations]); | |||
| } | |||
| } | |||
| resetCurrentConversation(): void { | |||
| this.currentConversation$.next(null); | |||
| } | |||
| private generateId(): string { | |||
| 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 { | |||
| setTimeout(() => { | |||
| const agent = this.agents$.value.find(a => a.id === agentId); | |||
| @@ -0,0 +1,156 @@ | |||
| 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; | |||
| } | |||
| } | |||