|
|
@@ -1,31 +1,23 @@
|
|
|
/**
|
|
|
* Lego 03 / Lego 14 — n8n RAG Chatbot Tab (Intelligence)
|
|
|
*
|
|
|
- * Chat messages are sent to NestJS via Socket.io (chat:send).
|
|
|
- * NestJS proxies them server-to-server to n8n — no CORS constraint.
|
|
|
- * The n8n webhook URL lives in NestJS .env (N8N_WEBHOOK_URL).
|
|
|
+ * Thin view layer — all chat state (messages, loading) lives in ChatSocketService
|
|
|
+ * so it survives tab navigation. This component only owns scroll behaviour.
|
|
|
*/
|
|
|
|
|
|
import {
|
|
|
Component,
|
|
|
- signal,
|
|
|
ViewChild,
|
|
|
ElementRef,
|
|
|
AfterViewChecked,
|
|
|
OnInit,
|
|
|
+ inject,
|
|
|
} from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { VisionSocketService } from '../../services/vision-socket.service';
|
|
|
import { ChatSocketService } from '../../services/chat-socket.service';
|
|
|
|
|
|
-export interface ChatMessage {
|
|
|
- role: 'user' | 'bot' | 'error';
|
|
|
- text: string;
|
|
|
- timestamp: Date;
|
|
|
- durationMs?: number;
|
|
|
-}
|
|
|
-
|
|
|
@Component({
|
|
|
selector: 'app-chatbot',
|
|
|
standalone: true,
|
|
|
@@ -34,47 +26,18 @@ export interface ChatMessage {
|
|
|
styleUrls: ['./chatbot.component.scss'],
|
|
|
})
|
|
|
export class ChatbotComponent implements OnInit, AfterViewChecked {
|
|
|
- private static readonly STORAGE_KEY = 'palm_ai_chat_history';
|
|
|
+ readonly chatSocket = inject(ChatSocketService);
|
|
|
+ readonly visionSocket = inject(VisionSocketService);
|
|
|
|
|
|
@ViewChild('messageList') messageListRef!: ElementRef<HTMLDivElement>;
|
|
|
|
|
|
- messages = signal<ChatMessage[]>(this.loadMessages());
|
|
|
-
|
|
|
inputText = '';
|
|
|
- loading = signal<boolean>(false);
|
|
|
-
|
|
|
private shouldScrollToBottom = false;
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
this.shouldScrollToBottom = true;
|
|
|
}
|
|
|
|
|
|
- constructor(
|
|
|
- public chatSocket: ChatSocketService,
|
|
|
- public visionSocket: VisionSocketService,
|
|
|
- ) {}
|
|
|
-
|
|
|
- private loadMessages(): ChatMessage[] {
|
|
|
- try {
|
|
|
- const raw = localStorage.getItem(ChatbotComponent.STORAGE_KEY);
|
|
|
- if (raw) {
|
|
|
- const parsed: ChatMessage[] = JSON.parse(raw);
|
|
|
- return parsed.map(m => ({ ...m, timestamp: new Date(m.timestamp) }));
|
|
|
- }
|
|
|
- } catch {}
|
|
|
- return [{
|
|
|
- role: 'bot',
|
|
|
- text: 'RAG pipeline ready. Ask me about palm oil operational data.',
|
|
|
- timestamp: new Date(),
|
|
|
- }];
|
|
|
- }
|
|
|
-
|
|
|
- private saveMessages(msgs: ChatMessage[]): void {
|
|
|
- try {
|
|
|
- localStorage.setItem(ChatbotComponent.STORAGE_KEY, JSON.stringify(msgs));
|
|
|
- } catch {}
|
|
|
- }
|
|
|
-
|
|
|
ngAfterViewChecked(): void {
|
|
|
if (this.shouldScrollToBottom) {
|
|
|
this.scrollToBottom();
|
|
|
@@ -84,37 +47,11 @@ export class ChatbotComponent implements OnInit, AfterViewChecked {
|
|
|
|
|
|
async sendMessage(): Promise<void> {
|
|
|
const text = this.inputText.trim();
|
|
|
- if (!text || this.loading() || this.chatSocket.sending()) return;
|
|
|
-
|
|
|
- // Push user message immediately
|
|
|
- this.pushMessage({ role: 'user', text, timestamp: new Date() });
|
|
|
+ if (!text || this.chatSocket.loading() || this.chatSocket.sending()) return;
|
|
|
this.inputText = '';
|
|
|
- this.loading.set(true);
|
|
|
-
|
|
|
- const start = Date.now();
|
|
|
-
|
|
|
- try {
|
|
|
- const response = await this.chatSocket.send(text);
|
|
|
- const durationMs = Date.now() - start;
|
|
|
-
|
|
|
- // n8n may return { output }, { answer }, { response }, { text }, or a raw string
|
|
|
- const botText =
|
|
|
- response?.output ??
|
|
|
- response?.answer ??
|
|
|
- response?.response ??
|
|
|
- response?.text ??
|
|
|
- (typeof response === 'string' ? response : JSON.stringify(response));
|
|
|
-
|
|
|
- this.pushMessage({ role: 'bot', text: botText, timestamp: new Date(), durationMs });
|
|
|
- } catch (err: any) {
|
|
|
- this.pushMessage({
|
|
|
- role: 'error',
|
|
|
- text: `Proxy error: ${err.message ?? 'Unknown error'}. Check NestJS → n8n connection.`,
|
|
|
- timestamp: new Date(),
|
|
|
- });
|
|
|
- } finally {
|
|
|
- this.loading.set(false);
|
|
|
- }
|
|
|
+ this.shouldScrollToBottom = true;
|
|
|
+ await this.chatSocket.sendMessage(text);
|
|
|
+ this.shouldScrollToBottom = true;
|
|
|
}
|
|
|
|
|
|
onEnter(event: KeyboardEvent): void {
|
|
|
@@ -125,23 +62,7 @@ export class ChatbotComponent implements OnInit, AfterViewChecked {
|
|
|
}
|
|
|
|
|
|
onClearChat(): void {
|
|
|
- const reset: ChatMessage[] = [{
|
|
|
- role: 'bot',
|
|
|
- text: 'Chat cleared. RAG pipeline ready.',
|
|
|
- timestamp: new Date(),
|
|
|
- }];
|
|
|
- this.messages.set(reset);
|
|
|
- this.saveMessages(reset);
|
|
|
- this.chatSocket.clearBackendSession();
|
|
|
- }
|
|
|
-
|
|
|
- private pushMessage(msg: ChatMessage): void {
|
|
|
- this.messages.update(msgs => {
|
|
|
- const updated = [...msgs, msg];
|
|
|
- this.saveMessages(updated);
|
|
|
- return updated;
|
|
|
- });
|
|
|
- this.shouldScrollToBottom = true;
|
|
|
+ this.chatSocket.clearChat();
|
|
|
}
|
|
|
|
|
|
private scrollToBottom(): void {
|