chat-socket.service.ts 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. /**
  2. * Lego 03 / Lego 06 — RAG Chat via NestJS Socket Proxy
  3. *
  4. * Sends chat messages to NestJS over the existing /vision Socket.io connection.
  5. * NestJS forwards the message server-to-server to n8n (no CORS constraint),
  6. * then emits chat:result back here.
  7. *
  8. * This service does NOT open a second socket — it shares the /vision namespace
  9. * socket with VisionSocketService by injecting VisionSocketService and calling
  10. * its exposed socket methods. Instead, we open our own /vision socket here
  11. * specifically for chat so lifecycle is independently managed.
  12. *
  13. * Events:
  14. * emit → chat:send { message: string }
  15. * on ← chat:result <n8n response object>
  16. * on ← chat:error { message: string }
  17. */
  18. import { Injectable, signal, OnDestroy } from '@angular/core';
  19. import { io, Socket } from 'socket.io-client';
  20. import { environment } from '../../environments/environment';
  21. export interface ChatResult {
  22. output?: string;
  23. answer?: string;
  24. response?: string;
  25. text?: string;
  26. [key: string]: any;
  27. }
  28. @Injectable({ providedIn: 'root' })
  29. export class ChatSocketService implements OnDestroy {
  30. readonly sending = signal<boolean>(false);
  31. readonly lastError = signal<string | null>(null);
  32. private socket: Socket;
  33. constructor() {
  34. this.socket = io(`${environment.nestWsUrl}/vision`, {
  35. transports: ['websocket'],
  36. reconnection: true,
  37. reconnectionDelay: 1000,
  38. secure: true,
  39. rejectUnauthorized: false,
  40. });
  41. }
  42. /**
  43. * Send a chat message to NestJS. NestJS proxies it to n8n server-to-server.
  44. * Returns a Promise that resolves with the n8n response or rejects on error.
  45. */
  46. send(message: string): Promise<ChatResult> {
  47. return new Promise((resolve, reject) => {
  48. this.sending.set(true);
  49. this.lastError.set(null);
  50. // One-shot listeners — removed immediately after first fire
  51. const onResult = (data: ChatResult) => {
  52. cleanup();
  53. this.sending.set(false);
  54. resolve(data);
  55. };
  56. const onError = (err: { message: string }) => {
  57. cleanup();
  58. this.sending.set(false);
  59. const msg = err?.message ?? 'n8n proxy error';
  60. this.lastError.set(msg);
  61. reject(new Error(msg));
  62. };
  63. const cleanup = () => {
  64. this.socket.off('chat:result', onResult);
  65. this.socket.off('chat:error', onError);
  66. };
  67. this.socket.once('chat:result', onResult);
  68. this.socket.once('chat:error', onError);
  69. this.socket.emit('chat:send', { message });
  70. });
  71. }
  72. clearBackendSession(): void {
  73. this.socket.emit('chat:clear');
  74. }
  75. ngOnDestroy(): void {
  76. this.socket.disconnect();
  77. }
  78. }