|
@@ -5,6 +5,7 @@ import { MatCardModule } from '@angular/material/card';
|
|
|
import { MatInputModule } from '@angular/material/input';
|
|
import { MatInputModule } from '@angular/material/input';
|
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
|
import { MatListModule } from '@angular/material/list';
|
|
import { MatListModule } from '@angular/material/list';
|
|
|
|
|
+import { MatSelectModule } from '@angular/material/select';
|
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { io, Socket } from 'socket.io-client';
|
|
|
import { webConfig } from '../config';
|
|
import { webConfig } from '../config';
|
|
|
import { ThoughtPayload } from '../interfaces/interface';
|
|
import { ThoughtPayload } from '../interfaces/interface';
|
|
@@ -17,21 +18,30 @@ interface ChatMessage {
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-chat',
|
|
selector: 'app-chat',
|
|
|
standalone: true,
|
|
standalone: true,
|
|
|
- imports: [CommonModule, FormsModule, MatCardModule, MatInputModule, MatButtonModule, MatListModule],
|
|
|
|
|
|
|
+ imports: [
|
|
|
|
|
+ CommonModule,
|
|
|
|
|
+ FormsModule,
|
|
|
|
|
+ MatCardModule,
|
|
|
|
|
+ MatInputModule,
|
|
|
|
|
+ MatButtonModule,
|
|
|
|
|
+ MatListModule,
|
|
|
|
|
+ MatSelectModule
|
|
|
|
|
+ ],
|
|
|
templateUrl: './chat.component.html',
|
|
templateUrl: './chat.component.html',
|
|
|
styleUrls: ['./chat.component.css']
|
|
styleUrls: ['./chat.component.css']
|
|
|
})
|
|
})
|
|
|
export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|
export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|
|
@ViewChild('scrollContainer') private scrollContainer!: ElementRef;
|
|
@ViewChild('scrollContainer') private scrollContainer!: ElementRef;
|
|
|
|
|
|
|
|
- // Configuration constants
|
|
|
|
|
- public readonly mandatoryKeys = ['node', 'status'];
|
|
|
|
|
-
|
|
|
|
|
messages: ChatMessage[] = [];
|
|
messages: ChatMessage[] = [];
|
|
|
inputMessage = '';
|
|
inputMessage = '';
|
|
|
loading = false;
|
|
loading = false;
|
|
|
agentThoughts: ThoughtPayload[] = [];
|
|
agentThoughts: ThoughtPayload[] = [];
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Models
|
|
|
|
|
+ models: ('openai' | 'gemini')[] = ['openai', 'gemini'];
|
|
|
|
|
+ currentProvider: 'openai' | 'gemini' = 'openai';
|
|
|
|
|
+ modelName: string = ''
|
|
|
private socket!: Socket;
|
|
private socket!: Socket;
|
|
|
|
|
|
|
|
ngOnInit() {
|
|
ngOnInit() {
|
|
@@ -39,16 +49,9 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ngOnDestroy() {
|
|
ngOnDestroy() {
|
|
|
- if (this.socket) {
|
|
|
|
|
- this.socket.disconnect();
|
|
|
|
|
- console.log('Socket disconnected and cleaned up.');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (this.socket) this.socket.disconnect();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Reality Check: Auto-scrolling is essential for chat.
|
|
|
|
|
- * This ensures the user doesn't have to manually scroll for every response.
|
|
|
|
|
- */
|
|
|
|
|
ngAfterViewChecked() {
|
|
ngAfterViewChecked() {
|
|
|
this.scrollToBottom();
|
|
this.scrollToBottom();
|
|
|
}
|
|
}
|
|
@@ -56,7 +59,14 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|
|
private initSocketConnection() {
|
|
private initSocketConnection() {
|
|
|
this.socket = io(`${webConfig.exposedUrl}/ffb`);
|
|
this.socket = io(`${webConfig.exposedUrl}/ffb`);
|
|
|
|
|
|
|
|
- this.socket.on('connect', () => console.log('Connected to FFB Gateway'));
|
|
|
|
|
|
|
+ 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.socket.on('agent_thought', (payload: ThoughtPayload) => {
|
|
|
this.agentThoughts.push(payload);
|
|
this.agentThoughts.push(payload);
|
|
@@ -67,45 +77,65 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|
|
this.loading = false;
|
|
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));
|
|
this.socket.on('error', (err) => console.error('Socket error:', err));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // --- Helper Methods ---
|
|
|
|
|
-
|
|
|
|
|
sendMessage() {
|
|
sendMessage() {
|
|
|
const trimmedMessage = this.inputMessage.trim();
|
|
const trimmedMessage = this.inputMessage.trim();
|
|
|
if (!trimmedMessage || this.loading) return;
|
|
if (!trimmedMessage || this.loading) return;
|
|
|
|
|
|
|
|
this.messages.push({ content: trimmedMessage, sender: 'user' });
|
|
this.messages.push({ content: trimmedMessage, sender: 'user' });
|
|
|
this.loading = true;
|
|
this.loading = true;
|
|
|
- this.agentThoughts = []; // Reset trace for new turn
|
|
|
|
|
|
|
+ this.agentThoughts = [];
|
|
|
|
|
|
|
|
this.socket.emit('chat', { message: trimmedMessage });
|
|
this.socket.emit('chat', { message: trimmedMessage });
|
|
|
this.inputMessage = '';
|
|
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 {
|
|
private scrollToBottom(): void {
|
|
|
try {
|
|
try {
|
|
|
const el = this.scrollContainer.nativeElement;
|
|
const el = this.scrollContainer.nativeElement;
|
|
|
el.scrollTop = el.scrollHeight;
|
|
el.scrollTop = el.scrollHeight;
|
|
|
- } catch (err) {}
|
|
|
|
|
|
|
+ } catch { }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // --- Template Logic Helpers ---
|
|
|
|
|
-
|
|
|
|
|
|
|
+ // --- Template Helpers ---
|
|
|
asString(key: unknown): string {
|
|
asString(key: unknown): string {
|
|
|
return String(key);
|
|
return String(key);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
shouldShowAttribute(key: unknown, value: any): boolean {
|
|
shouldShowAttribute(key: unknown, value: any): boolean {
|
|
|
const sKey = String(key);
|
|
const sKey = String(key);
|
|
|
- return !this.mandatoryKeys.includes(sKey) &&
|
|
|
|
|
- value !== null &&
|
|
|
|
|
- value !== undefined &&
|
|
|
|
|
- value !== '';
|
|
|
|
|
|
|
+ return !['node', 'status'].includes(sKey) && value != null && value !== '';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
isObject(value: any): boolean {
|
|
isObject(value: any): boolean {
|
|
|
return value !== null && typeof value === 'object';
|
|
return value !== null && typeof value === 'object';
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+}
|