trauchessec 1 неделю назад
Родитель
Сommit
0d2c36ff21
3 измененных файлов: 327 добавлений и 8 удалений
  1. +72
    -7
      src/app/pages/chat/chat-view.ts
  2. +99
    -1
      src/app/services/data.service.ts
  3. +156
    -0
      src/app/services/groq.service.ts

+ 72
- 7
src/app/pages/chat/chat-view.ts Просмотреть файл

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


+ 99
- 1
src/app/services/data.service.ts Просмотреть файл

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

+ 156
- 0
src/app/services/groq.service.ts Просмотреть файл

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

Загрузка…
Отмена
Сохранить