import { AfterViewChecked, Component, ElementRef, inject, OnDestroy, OnInit, signal, ViewChild, } from '@angular/core'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { DpService } from 'dp-ui/dp.service'; import { FisAppMessage, MessageHeader, AppMessageType } from 'dp-ui/fisappmessage/apprequestmessagetype'; interface ChatMessage { id: string; sender: 'user' | 'assistant'; content: string; timestamp: number; } function makeWelcome(): ChatMessage { return { id: 'system-welcome', sender: 'assistant', content: 'Welcome to the Industrial Intelligence Portal. Ask me about batch yield summaries, ripeness distributions, ABW trends, or anomaly flags from your production data.', timestamp: Date.now(), }; } @Component({ selector: 'app-chatbot', standalone: true, imports: [ ReactiveFormsModule, MatButtonModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, MatTooltipModule, ], styleUrl: './chatbot.component.scss', template: `
psychology Intelligence Portal SID·{{ sessionId().slice(-6) }}
@for (msg of messages(); track msg.id) {
{{ msg.sender === 'user' ? 'You' : 'Intelligence' }}
{{ msg.content }}
} @if (loading()) {
Processing…
}
Ask the intelligence layer…
`, }) export class ChatbotComponent implements OnInit, AfterViewChecked, OnDestroy { private readonly dpService = inject(DpService); @ViewChild('messagesEl') messagesEl!: ElementRef; inputControl = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.minLength(1)], }); messages = signal([]); loading = signal(false); sessionId = signal(crypto.randomUUID()); private readonly destroy$ = new Subject(); private pendingScroll = false; ngOnInit(): void { this.messages.set([makeWelcome()]); } ngAfterViewChecked(): void { if (this.pendingScroll) { const el = this.messagesEl?.nativeElement; if (el) el.scrollTop = el.scrollHeight; this.pendingScroll = false; } } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } resetSession(): void { this.fisStream('Chat', 'clear', {}).pipe(takeUntil(this.destroy$)).subscribe(); this.sessionId.set(crypto.randomUUID()); this.messages.set([makeWelcome()]); this.inputControl.reset(); } send(event: Event): void { event.preventDefault(); const text = this.inputControl.value.trim(); if (!text || this.loading()) return; this.inputControl.reset(); this.messages.update(m => [ ...m, { id: crypto.randomUUID(), sender: 'user', content: text, timestamp: Date.now(), }, ]); this.loading.set(true); this.pendingScroll = true; this.fisStream('Chat', 'send', { message: text, sessionId: this.sessionId() }) .pipe(takeUntil(this.destroy$)) .subscribe({ next: (res: any) => { const content: string = typeof res === 'string' ? res : Array.isArray(res) ? (res[0]?.output ?? res[0]?.text ?? res[0]?.message ?? JSON.stringify(res)) : (res?.output ?? res?.text ?? res?.message ?? JSON.stringify(res)); this.messages.update(m => [ ...m, { id: crypto.randomUUID(), sender: 'assistant', content, timestamp: Date.now(), }, ]); this.loading.set(false); this.pendingScroll = true; }, error: () => this.appendError(), }); } private fisStream(serviceId: string, operation: string, payload: unknown) { const messageID = crypto.randomUUID(); const message: FisAppMessage = { header: { messageID, serviceId, messageName: operation, messageType: AppMessageType.Command, } as unknown as MessageHeader, data: payload, }; return this.dpService.stream(message); } private appendError(): void { this.messages.update(m => [ ...m, { id: crypto.randomUUID(), sender: 'assistant', content: 'Unable to reach the intelligence layer. Verify the WebSocket connection is active and the backend is running.', timestamp: Date.now(), }, ]); this.loading.set(false); this.pendingScroll = true; } }