@elqnt/chat 1.0.9 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,6 +51,7 @@ var DEFAULT_QUEUE_CONFIG = {
51
51
  };
52
52
  var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
53
53
  var DEFAULT_HEARTBEAT_TIMEOUT = 5e3;
54
+ var DEFAULT_TRANSPORT = "websocket";
54
55
  function isChatEvent(data) {
55
56
  return data && typeof data === "object" && (typeof data.type === "string" || data.message);
56
57
  }
@@ -65,7 +66,8 @@ var useWebSocketChatBase = ({
65
66
  debug = false,
66
67
  logger = createDefaultLogger(debug),
67
68
  heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,
68
- heartbeatTimeout = DEFAULT_HEARTBEAT_TIMEOUT
69
+ heartbeatTimeout = DEFAULT_HEARTBEAT_TIMEOUT,
70
+ transport = DEFAULT_TRANSPORT
69
71
  }) => {
70
72
  const [connectionState, setConnectionState] = (0, import_react.useState)("disconnected");
71
73
  const [error, setError] = (0, import_react.useState)(void 0);
@@ -75,9 +77,15 @@ var useWebSocketChatBase = ({
75
77
  messagesSent: 0,
76
78
  messagesReceived: 0,
77
79
  messagesQueued: 0,
78
- reconnectCount: 0
80
+ reconnectCount: 0,
81
+ transportType: transport
79
82
  });
80
83
  const wsRef = (0, import_react.useRef)(void 0);
84
+ const sseRef = (0, import_react.useRef)(void 0);
85
+ const transportRef = (0, import_react.useRef)(transport);
86
+ (0, import_react.useEffect)(() => {
87
+ transportRef.current = transport;
88
+ }, [transport]);
81
89
  const reconnectTimeoutRef = (0, import_react.useRef)(void 0);
82
90
  const retryCountRef = (0, import_react.useRef)(0);
83
91
  const messageQueueRef = (0, import_react.useRef)([]);
@@ -207,6 +215,10 @@ var useWebSocketChatBase = ({
207
215
  wsRef.current.close(1e3, "Cleanup");
208
216
  }
209
217
  wsRef.current = void 0;
218
+ if (sseRef.current) {
219
+ sseRef.current.close();
220
+ sseRef.current = void 0;
221
+ }
210
222
  if (reconnectTimeoutRef.current) {
211
223
  clearTimeout(reconnectTimeoutRef.current);
212
224
  reconnectTimeoutRef.current = void 0;
@@ -224,6 +236,35 @@ var useWebSocketChatBase = ({
224
236
  });
225
237
  loadChatRetryMapRef.current.clear();
226
238
  }, [stopHeartbeat]);
239
+ const getRestApiUrl = (0, import_react.useCallback)((endpoint) => {
240
+ const httpUrl = serverBaseUrl.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
241
+ return `${httpUrl}/${endpoint}`;
242
+ }, [serverBaseUrl]);
243
+ const sendRestMessage = (0, import_react.useCallback)(
244
+ async (endpoint, body) => {
245
+ const url = getRestApiUrl(endpoint);
246
+ logger.debug(`SSE REST API call: POST ${endpoint}`, body);
247
+ try {
248
+ const response = await fetch(url, {
249
+ method: "POST",
250
+ headers: {
251
+ "Content-Type": "application/json"
252
+ },
253
+ body: JSON.stringify(body)
254
+ });
255
+ if (!response.ok) {
256
+ const errorText = await response.text();
257
+ throw new Error(`REST API error: ${response.status} - ${errorText}`);
258
+ }
259
+ const data = await response.json();
260
+ return data;
261
+ } catch (error2) {
262
+ logger.error(`SSE REST API error for ${endpoint}:`, error2);
263
+ throw error2;
264
+ }
265
+ },
266
+ [getRestApiUrl, logger]
267
+ );
227
268
  const connect = (0, import_react.useCallback)(
228
269
  async (userId) => {
229
270
  if (!mountedRef.current) {
@@ -241,7 +282,11 @@ var useWebSocketChatBase = ({
241
282
  return Promise.reject(error2);
242
283
  }
243
284
  if (wsRef.current?.readyState === WebSocket.OPEN) {
244
- logger.debug("Already connected");
285
+ logger.debug("Already connected (WebSocket)");
286
+ return Promise.resolve();
287
+ }
288
+ if (sseRef.current?.readyState === EventSource.OPEN) {
289
+ logger.debug("Already connected (SSE)");
245
290
  return Promise.resolve();
246
291
  }
247
292
  if (connectionState === "connecting" || connectionState === "reconnecting") {
@@ -267,8 +312,169 @@ var useWebSocketChatBase = ({
267
312
  intentionalDisconnectRef.current = false;
268
313
  return new Promise((resolve, reject) => {
269
314
  try {
270
- const wsUrl = `${serverBaseUrl}?orgId=${orgId}&userId=${userId}&clientType=${clientType}&product=${product}`;
271
315
  const connectionStartTime = Date.now();
316
+ logger.info(`\u{1F504} Connecting with transport: ${transportRef.current}`);
317
+ if (transportRef.current === "sse") {
318
+ const sseUrl = getRestApiUrl(`stream?orgId=${orgId}&userId=${userId}&clientType=${clientType}&chatId=${currentChatKeyRef.current || ""}`);
319
+ logger.debug("Connecting to SSE:", sseUrl);
320
+ console.log(`\u23F3 Initiating SSE connection to ${sseUrl}...`);
321
+ const eventSource = new EventSource(sseUrl);
322
+ eventSource.onopen = () => {
323
+ if (!mountedRef.current) {
324
+ eventSource.close();
325
+ reject(new Error("Component unmounted"));
326
+ return;
327
+ }
328
+ const connectionTimeMs = Date.now() - connectionStartTime;
329
+ const connectionTimeSec = (connectionTimeMs / 1e3).toFixed(2);
330
+ logger.info("\u2705 SSE connected", {
331
+ userId,
332
+ retryCount: retryCountRef.current,
333
+ connectionTime: `${connectionTimeSec}s (${connectionTimeMs}ms)`
334
+ });
335
+ console.log(`\u{1F50C} SSE connection established in ${connectionTimeSec} seconds`);
336
+ setConnectionState("connected");
337
+ setError(void 0);
338
+ const wasReconnecting = retryCountRef.current > 0;
339
+ retryCountRef.current = 0;
340
+ updateMetrics({
341
+ connectedAt: Date.now(),
342
+ latency: connectionTimeMs,
343
+ transportType: "sse",
344
+ reconnectCount: wasReconnecting ? metrics.reconnectCount + 1 : metrics.reconnectCount
345
+ });
346
+ currentUserIdRef.current = userId;
347
+ if (currentChatKeyRef.current) {
348
+ logger.info("Loading chat after SSE reconnection:", currentChatKeyRef.current);
349
+ sendRestMessage("load", {
350
+ orgId,
351
+ chatKey: currentChatKeyRef.current,
352
+ userId
353
+ }).then((response) => {
354
+ if (response.success && response.data?.chat) {
355
+ const chatEvent = {
356
+ type: "load_chat_response",
357
+ orgId,
358
+ chatKey: currentChatKeyRef.current,
359
+ userId,
360
+ timestamp: Date.now(),
361
+ data: response.data
362
+ };
363
+ emit("load_chat_response", chatEvent);
364
+ if (onMessageRef.current) {
365
+ onMessageRef.current(chatEvent);
366
+ }
367
+ }
368
+ }).catch((err) => {
369
+ logger.error("Failed to load chat after SSE reconnection:", err);
370
+ });
371
+ }
372
+ emit("connected", { userId, wasReconnecting, transport: "sse" });
373
+ resolve();
374
+ };
375
+ const handleSSEMessage = (event) => {
376
+ if (!mountedRef.current) return;
377
+ try {
378
+ const data = JSON.parse(event.data);
379
+ if (!isChatEvent(data)) {
380
+ logger.warn("Received invalid SSE message format:", data);
381
+ return;
382
+ }
383
+ const chatEvent = data;
384
+ logger.debug("SSE message received:", chatEvent.type);
385
+ updateMetrics({
386
+ messagesReceived: metrics.messagesReceived + 1,
387
+ lastMessageAt: Date.now()
388
+ });
389
+ switch (chatEvent.type) {
390
+ case "new_chat_created":
391
+ const newChatKey = chatEvent.data?.chatKey;
392
+ if (newChatKey) {
393
+ logger.info("New chat created with key:", newChatKey);
394
+ currentChatKeyRef.current = newChatKey;
395
+ if (chatCreationPromiseRef.current) {
396
+ chatCreationPromiseRef.current.resolve(newChatKey);
397
+ chatCreationPromiseRef.current = null;
398
+ }
399
+ }
400
+ break;
401
+ case "load_chat_response":
402
+ const chat = chatEvent.data?.chat;
403
+ if (chat && chat.key) {
404
+ logger.info("Chat loaded with key:", chat.key);
405
+ currentChatKeyRef.current = chat.key;
406
+ }
407
+ break;
408
+ case "chat_ended":
409
+ logger.info("Chat ended, clearing key");
410
+ currentChatKeyRef.current = void 0;
411
+ break;
412
+ }
413
+ emit(chatEvent.type || "message", chatEvent);
414
+ if (onMessageRef.current) {
415
+ onMessageRef.current(chatEvent);
416
+ }
417
+ } catch (error2) {
418
+ logger.error("Failed to parse SSE message:", error2);
419
+ }
420
+ };
421
+ eventSource.addEventListener("message", handleSSEMessage);
422
+ eventSource.addEventListener("reconnected", handleSSEMessage);
423
+ eventSource.addEventListener("typing", handleSSEMessage);
424
+ eventSource.addEventListener("stopped_typing", handleSSEMessage);
425
+ eventSource.addEventListener("waiting", handleSSEMessage);
426
+ eventSource.addEventListener("waiting_for_agent", handleSSEMessage);
427
+ eventSource.addEventListener("human_agent_joined", handleSSEMessage);
428
+ eventSource.addEventListener("human_agent_left", handleSSEMessage);
429
+ eventSource.addEventListener("chat_ended", handleSSEMessage);
430
+ eventSource.addEventListener("chat_updated", handleSSEMessage);
431
+ eventSource.addEventListener("load_chat_response", handleSSEMessage);
432
+ eventSource.addEventListener("new_chat_created", handleSSEMessage);
433
+ eventSource.addEventListener("error", handleSSEMessage);
434
+ eventSource.addEventListener("show_csat_survey", handleSSEMessage);
435
+ eventSource.addEventListener("csat_response", handleSSEMessage);
436
+ eventSource.addEventListener("user_suggested_actions", handleSSEMessage);
437
+ eventSource.addEventListener("agent_execution_started", handleSSEMessage);
438
+ eventSource.addEventListener("agent_execution_ended", handleSSEMessage);
439
+ eventSource.addEventListener("agent_context_update", handleSSEMessage);
440
+ eventSource.addEventListener("plan_pending_approval", handleSSEMessage);
441
+ eventSource.addEventListener("step_started", handleSSEMessage);
442
+ eventSource.addEventListener("step_completed", handleSSEMessage);
443
+ eventSource.addEventListener("step_failed", handleSSEMessage);
444
+ eventSource.addEventListener("plan_completed", handleSSEMessage);
445
+ eventSource.addEventListener("skills_changed", handleSSEMessage);
446
+ eventSource.addEventListener("summary_update", handleSSEMessage);
447
+ eventSource.onerror = (error2) => {
448
+ logger.error("SSE error:", error2);
449
+ if (!mountedRef.current) return;
450
+ if (eventSource.readyState === EventSource.CLOSED) {
451
+ const sseError = {
452
+ code: "CONNECTION_FAILED",
453
+ message: "SSE connection failed",
454
+ retryable: true,
455
+ timestamp: Date.now()
456
+ };
457
+ setError(sseError);
458
+ updateMetrics({ lastError: sseError });
459
+ setConnectionState("disconnected");
460
+ emit("disconnected", { reason: "SSE error" });
461
+ if (!intentionalDisconnectRef.current && mountedRef.current) {
462
+ const retryInterval = calculateRetryInterval(retryCountRef.current);
463
+ retryCountRef.current++;
464
+ logger.info(`SSE reconnecting in ${retryInterval}ms (attempt ${retryCountRef.current})`);
465
+ if (reconnectTimeoutRef.current) {
466
+ clearTimeout(reconnectTimeoutRef.current);
467
+ }
468
+ reconnectTimeoutRef.current = setTimeout(() => {
469
+ connect(userId);
470
+ }, retryInterval);
471
+ }
472
+ }
473
+ };
474
+ sseRef.current = eventSource;
475
+ return;
476
+ }
477
+ const wsUrl = `${serverBaseUrl}?orgId=${orgId}&userId=${userId}&clientType=${clientType}&product=${product}`;
272
478
  logger.debug("Connecting to WebSocket:", wsUrl);
273
479
  console.log(`\u23F3 Initiating WebSocket connection to ${serverBaseUrl}...`);
274
480
  const ws = new WebSocket(wsUrl);
@@ -540,7 +746,7 @@ var useWebSocketChatBase = ({
540
746
  );
541
747
  const sendMessage = (0, import_react.useCallback)(
542
748
  (event, overrideUserId) => {
543
- return new Promise((resolve, reject) => {
749
+ return new Promise(async (resolve, reject) => {
544
750
  if (!mountedRef.current) {
545
751
  reject(new Error("Component not mounted"));
546
752
  return;
@@ -549,8 +755,161 @@ var useWebSocketChatBase = ({
549
755
  ...event,
550
756
  timestamp: Date.now()
551
757
  };
552
- const messageId = `${fullEvent.type}_${fullEvent.timestamp}_${Math.random()}`;
553
758
  logger.debug("Sending message:", fullEvent.type);
759
+ if (transportRef.current === "sse") {
760
+ if (!sseRef.current || sseRef.current.readyState !== EventSource.OPEN) {
761
+ logger.debug("SSE not connected, attempting to connect");
762
+ if (connectionState === "disconnected" && overrideUserId) {
763
+ try {
764
+ await connect(overrideUserId);
765
+ } catch (error2) {
766
+ reject(error2);
767
+ return;
768
+ }
769
+ } else {
770
+ reject(new Error("SSE not connected"));
771
+ return;
772
+ }
773
+ }
774
+ try {
775
+ switch (fullEvent.type) {
776
+ case "message":
777
+ await sendRestMessage("send", {
778
+ orgId: fullEvent.orgId,
779
+ chatKey: fullEvent.chatKey || currentChatKeyRef.current,
780
+ userId: fullEvent.userId,
781
+ message: fullEvent.message
782
+ });
783
+ break;
784
+ case "typing":
785
+ await sendRestMessage("typing", {
786
+ orgId: fullEvent.orgId,
787
+ chatKey: fullEvent.chatKey || currentChatKeyRef.current,
788
+ userId: fullEvent.userId,
789
+ typing: true
790
+ });
791
+ break;
792
+ case "stopped_typing":
793
+ await sendRestMessage("typing", {
794
+ orgId: fullEvent.orgId,
795
+ chatKey: fullEvent.chatKey || currentChatKeyRef.current,
796
+ userId: fullEvent.userId,
797
+ typing: false
798
+ });
799
+ break;
800
+ case "load_chat":
801
+ const loadResponse = await sendRestMessage("load", {
802
+ orgId: fullEvent.orgId,
803
+ chatKey: fullEvent.chatKey,
804
+ userId: fullEvent.userId
805
+ });
806
+ if (loadResponse.success && loadResponse.data?.chat) {
807
+ currentChatKeyRef.current = loadResponse.data.chat.key;
808
+ const chatEvent = {
809
+ type: "load_chat_response",
810
+ orgId: fullEvent.orgId,
811
+ chatKey: loadResponse.data.chat.key,
812
+ userId: fullEvent.userId,
813
+ timestamp: Date.now(),
814
+ data: loadResponse.data
815
+ };
816
+ emit("load_chat_response", chatEvent);
817
+ if (onMessageRef.current) {
818
+ onMessageRef.current(chatEvent);
819
+ }
820
+ }
821
+ break;
822
+ case "new_chat":
823
+ const createResponse = await sendRestMessage("create", {
824
+ orgId: fullEvent.orgId,
825
+ userId: fullEvent.userId,
826
+ metadata: fullEvent.data
827
+ });
828
+ if (createResponse.success && createResponse.data?.chatKey) {
829
+ currentChatKeyRef.current = createResponse.data.chatKey;
830
+ const newChatEvent = {
831
+ type: "new_chat_created",
832
+ orgId: fullEvent.orgId,
833
+ chatKey: createResponse.data.chatKey,
834
+ userId: fullEvent.userId,
835
+ timestamp: Date.now(),
836
+ data: { chatKey: createResponse.data.chatKey }
837
+ };
838
+ emit("new_chat_created", newChatEvent);
839
+ if (onMessageRef.current) {
840
+ onMessageRef.current(newChatEvent);
841
+ }
842
+ if (chatCreationPromiseRef.current) {
843
+ chatCreationPromiseRef.current.resolve(createResponse.data.chatKey);
844
+ chatCreationPromiseRef.current = null;
845
+ }
846
+ }
847
+ break;
848
+ case "end_chat":
849
+ await sendRestMessage("end", {
850
+ orgId: fullEvent.orgId,
851
+ chatKey: fullEvent.chatKey || currentChatKeyRef.current,
852
+ userId: fullEvent.userId,
853
+ data: fullEvent.data
854
+ });
855
+ break;
856
+ case "human_agent_join":
857
+ await sendRestMessage("agent-join", {
858
+ orgId: fullEvent.orgId,
859
+ chatKey: fullEvent.chatKey || currentChatKeyRef.current,
860
+ user: fullEvent.data?.user
861
+ });
862
+ break;
863
+ case "human_agent_leave":
864
+ await sendRestMessage("agent-leave", {
865
+ orgId: fullEvent.orgId,
866
+ chatKey: fullEvent.chatKey || currentChatKeyRef.current,
867
+ user: fullEvent.data?.user
868
+ });
869
+ break;
870
+ // Event types that use the generic /event endpoint
871
+ case "load_agent_context":
872
+ case "skill_activate":
873
+ case "skill_deactivate":
874
+ case "sync_metadata":
875
+ case "plan_approved":
876
+ case "plan_rejected":
877
+ await sendRestMessage("event", {
878
+ type: fullEvent.type,
879
+ orgId: fullEvent.orgId,
880
+ chatKey: fullEvent.chatKey || currentChatKeyRef.current,
881
+ userId: fullEvent.userId,
882
+ data: fullEvent.data
883
+ });
884
+ break;
885
+ default:
886
+ logger.warn("Sending unrecognized event type via generic endpoint:", fullEvent.type);
887
+ await sendRestMessage("event", {
888
+ type: fullEvent.type,
889
+ orgId: fullEvent.orgId,
890
+ chatKey: fullEvent.chatKey || currentChatKeyRef.current,
891
+ userId: fullEvent.userId,
892
+ data: fullEvent.data
893
+ });
894
+ break;
895
+ }
896
+ updateMetrics({ messagesSent: metrics.messagesSent + 1 });
897
+ logger.debug("SSE REST message sent successfully");
898
+ resolve();
899
+ } catch (error2) {
900
+ logger.error("Failed to send SSE REST message:", error2);
901
+ const sendError = {
902
+ code: "SEND_FAILED",
903
+ message: error2 instanceof Error ? error2.message : "Failed to send message",
904
+ retryable: true,
905
+ timestamp: Date.now()
906
+ };
907
+ setError(sendError);
908
+ updateMetrics({ lastError: sendError });
909
+ reject(sendError);
910
+ }
911
+ return;
912
+ }
554
913
  if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
555
914
  if (addToQueue(fullEvent)) {
556
915
  logger.debug("Message queued, attempting to connect");
@@ -587,7 +946,7 @@ var useWebSocketChatBase = ({
587
946
  }
588
947
  });
589
948
  },
590
- [connectionState, connect, addToQueue, logger, metrics, updateMetrics]
949
+ [connectionState, connect, addToQueue, logger, metrics, updateMetrics, sendRestMessage, emit]
591
950
  );
592
951
  const startNewChat = (0, import_react.useCallback)(
593
952
  (userId, data) => {
@@ -625,7 +984,7 @@ var useWebSocketChatBase = ({
625
984
  );
626
985
  const disconnect = (0, import_react.useCallback)(
627
986
  (intentional = true) => {
628
- logger.info("Disconnecting WebSocket", { intentional });
987
+ logger.info("Disconnecting", { intentional, transport: transportRef.current });
629
988
  intentionalDisconnectRef.current = intentional;
630
989
  cleanup();
631
990
  setConnectionState("disconnected");