Pārlūkot izejas kodu

fixed state message issue in the chat box

Dr-Swopt 4 stundas atpakaļ
vecāks
revīzija
1cf8050ba0

+ 4 - 4
src/app/components/chatbot/chatbot.component.html

@@ -15,7 +15,7 @@
 
         <!-- Message list -->
         <div class="message-list" #messageList>
-          @for (msg of messages(); track msg.timestamp) {
+          @for (msg of chatSocket.messages(); track msg.timestamp) {
             <div class="message" [class]="'message--' + msg.role">
               <div class="message-bubble">
                 <p class="message-text">{{ msg.text }}</p>
@@ -29,7 +29,7 @@
             </div>
           }
 
-          @if (loading()) {
+          @if (chatSocket.loading()) {
             <div class="message message--bot">
               <div class="message-bubble">
                 <div class="thinking-dots">
@@ -56,12 +56,12 @@
             (keydown)="onEnter($event)"
             placeholder="Ask about palm oil ripeness, site data, operational reports..."
             rows="2"
-            [disabled]="loading() || chatSocket.sending() || visionSocket.nestStatus() === 'OFFLINE'">
+            [disabled]="chatSocket.loading() || chatSocket.sending() || visionSocket.nestStatus() === 'OFFLINE'">
           </textarea>
           <button
             class="btn btn-primary send-btn"
             (click)="sendMessage()"
-            [disabled]="!inputText.trim() || loading() || chatSocket.sending() || visionSocket.nestStatus() === 'OFFLINE'">
+            [disabled]="!inputText.trim() || chatSocket.loading() || chatSocket.sending() || visionSocket.nestStatus() === 'OFFLINE'">
             {{ chatSocket.sending() ? '...' : 'Send' }}
           </button>
         </div>

+ 10 - 89
src/app/components/chatbot/chatbot.component.ts

@@ -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 {

+ 84 - 12
src/app/services/chat-socket.service.ts

@@ -5,10 +5,9 @@
  * NestJS forwards the message server-to-server to n8n (no CORS constraint),
  * then emits chat:result back here.
  *
- * This service does NOT open a second socket — it shares the /vision namespace
- * socket with VisionSocketService by injecting VisionSocketService and calling
- * its exposed socket methods. Instead, we open our own /vision socket here
- * specifically for chat so lifecycle is independently managed.
+ * Messages and loading state live here (not in the component) so they survive
+ * tab navigation — the component is destroyed/recreated on each visit, but this
+ * service is a singleton that persists for the app lifetime.
  *
  * Events:
  *   emit  → chat:send   { message: string }
@@ -20,6 +19,13 @@ import { Injectable, signal, OnDestroy } from '@angular/core';
 import { io, Socket } from 'socket.io-client';
 import { environment } from '../../environments/environment';
 
+export interface ChatMessage {
+  role: 'user' | 'bot' | 'error';
+  text: string;
+  timestamp: Date;
+  durationMs?: number;
+}
+
 export interface ChatResult {
   output?: string;
   answer?: string;
@@ -28,11 +34,15 @@ export interface ChatResult {
   [key: string]: any;
 }
 
+const STORAGE_KEY = 'palm_ai_chat_history';
+
 @Injectable({ providedIn: 'root' })
 export class ChatSocketService implements OnDestroy {
 
   readonly sending = signal<boolean>(false);
+  readonly loading = signal<boolean>(false);
   readonly lastError = signal<string | null>(null);
+  readonly messages = signal<ChatMessage[]>(this.loadMessages());
 
   private socket: Socket;
 
@@ -46,16 +56,61 @@ export class ChatSocketService implements OnDestroy {
     });
   }
 
-  /**
-   * Send a chat message to NestJS. NestJS proxies it to n8n server-to-server.
-   * Returns a Promise that resolves with the n8n response or rejects on error.
-   */
-  send(message: string): Promise<ChatResult> {
+  async sendMessage(text: string): Promise<void> {
+    if (!text || this.loading() || this.sending()) return;
+
+    this.pushMessage({ role: 'user', text, timestamp: new Date() });
+    this.loading.set(true);
+
+    const start = Date.now();
+
+    try {
+      const response = await this.send(text);
+      const durationMs = Date.now() - start;
+
+      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);
+    }
+  }
+
+  clearChat(): void {
+    const reset: ChatMessage[] = [{
+      role: 'bot',
+      text: 'Chat cleared. RAG pipeline ready.',
+      timestamp: new Date(),
+    }];
+    this.messages.set(reset);
+    this.saveMessages(reset);
+    this.socket.emit('chat:clear');
+  }
+
+  pushMessage(msg: ChatMessage): void {
+    this.messages.update(msgs => {
+      const updated = [...msgs, msg];
+      this.saveMessages(updated);
+      return updated;
+    });
+  }
+
+  private send(message: string): Promise<ChatResult> {
     return new Promise((resolve, reject) => {
       this.sending.set(true);
       this.lastError.set(null);
 
-      // One-shot listeners — removed immediately after first fire
       const onResult = (data: ChatResult) => {
         cleanup();
         this.sending.set(false);
@@ -82,8 +137,25 @@ export class ChatSocketService implements OnDestroy {
     });
   }
 
-  clearBackendSession(): void {
-    this.socket.emit('chat:clear');
+  private loadMessages(): ChatMessage[] {
+    try {
+      const raw = localStorage.getItem(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(STORAGE_KEY, JSON.stringify(msgs));
+    } catch {}
   }
 
   ngOnDestroy(): void {