import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; import { MatListModule } from '@angular/material/list'; import { MatSelectModule } from '@angular/material/select'; import { io, Socket } from 'socket.io-client'; import { webConfig } from '../config'; import { ThoughtPayload } from '../interfaces/interface'; interface ChatMessage { content: string; sender: 'user' | 'bot'; } @Component({ selector: 'app-chat', standalone: true, imports: [ CommonModule, FormsModule, MatCardModule, MatInputModule, MatButtonModule, MatListModule, MatSelectModule ], templateUrl: './chat.component.html', styleUrls: ['./chat.component.css'] }) export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked { @ViewChild('scrollContainer') private scrollContainer!: ElementRef; messages: ChatMessage[] = []; inputMessage = ''; loading = false; agentThoughts: ThoughtPayload[] = []; // Models models: ('openai' | 'gemini')[] = ['openai', 'gemini']; currentProvider: 'openai' | 'gemini' = 'openai'; modelName: string = '' private socket!: Socket; ngOnInit() { this.initSocketConnection(); } ngOnDestroy() { if (this.socket) this.socket.disconnect(); } ngAfterViewChecked() { this.scrollToBottom(); } private initSocketConnection() { this.socket = io(`${webConfig.exposedUrl}/ffb`); this.socket.on('connect', () => { console.log('Connected to FFB Gateway'); // Request current model for this session this.socket.emit('get_model', {}, (res: any) => { // nothing happens. Response shoudl be in 'current model' event }); }); this.socket.on('agent_thought', (payload: ThoughtPayload) => { this.agentThoughts.push(payload); }); this.socket.on('chat_response', (payload: { message: string }) => { this.messages.push({ content: payload.message, sender: 'bot' }); this.loading = false; }); this.socket.on('current_model', (data: any) => { // Update dropdown if server sends a model change if (data?.provider) { this.currentProvider = data.provider; this.modelName = data.modelName } }); this.socket.on('error', (err) => console.error('Socket error:', err)); } sendMessage() { const trimmedMessage = this.inputMessage.trim(); if (!trimmedMessage || this.loading) return; this.messages.push({ content: trimmedMessage, sender: 'user' }); this.loading = true; this.agentThoughts = []; this.socket.emit('chat', { message: trimmedMessage }); this.inputMessage = ''; } switchModel(model: 'openai' | 'gemini') { console.log('Switching model to:', model); // Debug log if (model === this.currentProvider) return; // Prevent switching to same model // Emit to backend this.socket.emit('switch_model', { provider: model }, (res: any) => { console.log('Switching model:', res?.data?.provider); }); this.socket.emit('get_model', {}, (res: any) => { }); } // Called by mat-select on selection change onModelSelect(event: any) { console.log('Dropdown selection:', event.value); // Debug log this.switchModel(event.value); } private scrollToBottom(): void { try { const el = this.scrollContainer.nativeElement; el.scrollTop = el.scrollHeight; } catch { } } // --- Template Helpers --- asString(key: unknown): string { return String(key); } shouldShowAttribute(key: unknown, value: any): boolean { const sKey = String(key); return !['node', 'status'].includes(sKey) && value != null && value !== ''; } isObject(value: any): boolean { return value !== null && typeof value === 'object'; } }