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: `
@for (msg of messages(); track msg.id) {
{{ msg.sender === 'user' ? 'You' : 'Intelligence' }}
{{ msg.content }}
}
@if (loading()) {
Processing…
}
`,
})
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;
}
}