@nextclaw/ui 0.6.1 → 0.6.2

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/assets/{ChannelsList-CkCpHSto.js → ChannelsList-Bga6n85j.js} +1 -1
  3. package/dist/assets/ChatPage-B-Yk3kkv.js +32 -0
  4. package/dist/assets/{DocBrowser-B5Aqiz6W.js → DocBrowser-dv57PRp5.js} +1 -1
  5. package/dist/assets/{MarketplacePage-BIi0bBdW.js → MarketplacePage-j6p73Hjo.js} +1 -1
  6. package/dist/assets/{ModelConfig-BTFiEAxQ.js → ModelConfig-BiKSDp5h.js} +1 -1
  7. package/dist/assets/{ProvidersList-cdk1d-G_.js → ProvidersList-B7ZfRUkD.js} +1 -1
  8. package/dist/assets/{RuntimeConfig-CFqFsXmR.js → RuntimeConfig-Bpt9UNb6.js} +1 -1
  9. package/dist/assets/{SecretsConfig-CIKasCek.js → SecretsConfig-Ds00G-_O.js} +2 -2
  10. package/dist/assets/{SessionsConfig-mnCLFtbo.js → SessionsConfig-Mjet4opU.js} +1 -1
  11. package/dist/assets/{card-C1BUfR85.js → card-C7JJ5BGA.js} +1 -1
  12. package/dist/assets/index-BiJ2xs5X.css +1 -0
  13. package/dist/assets/{index-Dxas8MJ9.js → index-Cb9xiqC5.js} +2 -2
  14. package/dist/assets/{label-CwWfYbuj.js → label-DHJKdaUl.js} +1 -1
  15. package/dist/assets/{logos-DDyjHSEU.js → logos-fPO_amyL.js} +1 -1
  16. package/dist/assets/{page-layout-DKTRKcHL.js → page-layout-CF0JQsWW.js} +1 -1
  17. package/dist/assets/{switch-Bi3yeYiC.js → switch-C1hgy-fE.js} +1 -1
  18. package/dist/assets/{tabs-custom-HZFNZrc0.js → tabs-custom-OyoLf5ZM.js} +1 -1
  19. package/dist/assets/useConfig-D_G46zbo.js +6 -0
  20. package/dist/assets/{useConfirmDialog-DwD21HlD.js → useConfirmDialog-_0u6i3cI.js} +1 -1
  21. package/dist/index.html +2 -2
  22. package/package.json +1 -1
  23. package/src/api/config.ts +77 -12
  24. package/src/api/types.ts +24 -0
  25. package/src/components/chat/ChatConversationPanel.tsx +5 -20
  26. package/src/components/chat/ChatPage.tsx +49 -8
  27. package/src/components/chat/useChatStreamController.ts +192 -115
  28. package/src/hooks/useConfig.ts +29 -0
  29. package/src/hooks/useWebSocket.ts +21 -0
  30. package/dist/assets/ChatPage-DM4XNsrW.js +0 -32
  31. package/dist/assets/index-P4YzN9iS.css +0 -1
  32. package/dist/assets/useConfig-CgzVQTZl.js +0 -6
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
3
- import type { SessionEventView } from '@/api/types';
4
- import { sendChatTurnStream, stopChatTurn } from '@/api/config';
3
+ import type { ChatRunView, SessionEventView } from '@/api/types';
4
+ import { sendChatTurnStream, stopChatTurn, streamChatRun } from '@/api/config';
5
5
 
6
6
  type PendingChatMessage = {
7
7
  id: number;
@@ -17,7 +17,7 @@ type PendingChatMessage = {
17
17
  type ActiveRunState = {
18
18
  localRunId: number;
19
19
  sessionKey: string;
20
- agentId: string;
20
+ agentId?: string;
21
21
  requestAbortController: AbortController;
22
22
  backendRunId?: string;
23
23
  backendStopSupported: boolean;
@@ -141,62 +141,86 @@ async function refetchIfSessionVisible(params: {
141
141
  }
142
142
  }
143
143
 
144
- async function executeSendRun(params: {
145
- item: PendingChatMessage;
144
+ function upsertStreamingEvent(
145
+ setStreamingSessionEvents: Dispatch<SetStateAction<SessionEventView[]>>,
146
+ event: SessionEventView
147
+ ) {
148
+ setStreamingSessionEvents((prev) => {
149
+ const next = [...prev];
150
+ const hit = next.findIndex((streamEvent) => streamEvent.seq === event.seq);
151
+ if (hit >= 0) {
152
+ next[hit] = event;
153
+ } else {
154
+ next.push(event);
155
+ }
156
+ return next;
157
+ });
158
+ }
159
+
160
+ type ExecuteStreamRunParams = {
146
161
  runId: number;
147
162
  runIdRef: MutableRefObject<number>;
148
163
  activeRunRef: MutableRefObject<ActiveRunState | null>;
149
- nextOptimisticUserSeq: number;
150
164
  selectedSessionKeyRef: MutableRefObject<string | null>;
151
165
  setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
152
166
  setDraft: Dispatch<SetStateAction<string>>;
153
167
  refetchSessions: () => Promise<unknown>;
154
168
  refetchHistory: () => Promise<unknown>;
155
169
  restoreDraftOnError?: boolean;
170
+ sourceSessionKey: string;
171
+ sourceAgentId?: string;
172
+ sourceMessage?: string;
173
+ sourceStopSupported?: boolean;
174
+ sourceStopReason?: string;
175
+ optimisticUserEvent: SessionEventView | null;
176
+ openStream: (params: {
177
+ signal: AbortSignal;
178
+ onReady: (event: { runId?: string; stopSupported?: boolean; stopReason?: string; sessionKey: string }) => void;
179
+ onDelta: (event: { delta: string }) => void;
180
+ onSessionEvent: (event: { data: SessionEventView }) => void;
181
+ }) => Promise<{ sessionKey: string }>;
156
182
  setters: StreamSetters;
157
- }): Promise<void> {
183
+ };
184
+
185
+ async function executeStreamRun(params: ExecuteStreamRunParams): Promise<void> {
158
186
  const {
159
- item,
160
187
  runId,
161
188
  runIdRef,
162
189
  activeRunRef,
163
- nextOptimisticUserSeq,
164
190
  selectedSessionKeyRef,
165
191
  setSelectedSessionKey,
166
192
  setDraft,
167
193
  refetchSessions,
168
194
  refetchHistory,
169
195
  restoreDraftOnError,
196
+ sourceSessionKey,
197
+ sourceAgentId,
198
+ sourceMessage,
199
+ sourceStopSupported,
200
+ sourceStopReason,
201
+ optimisticUserEvent,
202
+ openStream,
170
203
  setters
171
204
  } = params;
172
205
 
173
206
  const requestAbortController = new AbortController();
174
207
  activeRunRef.current = {
175
208
  localRunId: runId,
176
- sessionKey: item.sessionKey,
177
- agentId: item.agentId,
209
+ sessionKey: sourceSessionKey,
210
+ ...(sourceAgentId ? { agentId: sourceAgentId } : {}),
178
211
  requestAbortController,
179
- backendStopSupported: Boolean(item.stopSupported),
180
- ...(item.stopReason ? { backendStopReason: item.stopReason } : {})
212
+ backendStopSupported: Boolean(sourceStopSupported),
213
+ ...(sourceStopReason ? { backendStopReason: sourceStopReason } : {})
181
214
  };
182
215
 
183
216
  setters.setStreamingSessionEvents([]);
184
217
  setters.setStreamingAssistantText('');
185
218
  setters.setStreamingAssistantTimestamp(null);
186
- setters.setOptimisticUserEvent({
187
- seq: nextOptimisticUserSeq,
188
- type: 'message.user.optimistic',
189
- timestamp: new Date().toISOString(),
190
- message: {
191
- role: 'user',
192
- content: item.message,
193
- timestamp: new Date().toISOString()
194
- }
195
- });
219
+ setters.setOptimisticUserEvent(optimisticUserEvent);
196
220
  setters.setIsSending(true);
197
221
  setters.setIsAwaitingAssistantOutput(true);
198
222
  setters.setCanStopCurrentRun(false);
199
- setters.setStopDisabledReason(item.stopSupported ? '__preparing__' : item.stopReason ?? null);
223
+ setters.setStopDisabledReason(sourceStopSupported ? '__preparing__' : sourceStopReason ?? null);
200
224
  setters.setLastSendError(null);
201
225
 
202
226
  let streamText = '';
@@ -205,96 +229,69 @@ async function executeSendRun(params: {
205
229
  const streamTimestamp = new Date().toISOString();
206
230
  setters.setStreamingAssistantTimestamp(streamTimestamp);
207
231
 
208
- const requestedSkills = normalizeRequestedSkills(item.requestedSkills);
209
- const result = await sendChatTurnStream(
210
- {
211
- message: item.message,
212
- sessionKey: item.sessionKey,
213
- agentId: item.agentId,
214
- ...(item.model ? { model: item.model } : {}),
215
- ...(requestedSkills.length > 0
216
- ? {
217
- metadata: {
218
- requested_skills: requestedSkills
219
- }
220
- }
221
- : {}),
222
- channel: 'ui',
223
- chatId: 'web-ui'
224
- },
225
- {
226
- signal: requestAbortController.signal,
227
- onReady: (event) => {
228
- if (runId !== runIdRef.current) {
229
- return;
230
- }
231
- const activeRun = activeRunRef.current;
232
- if (activeRun && activeRun.localRunId === runId) {
233
- activeRun.backendRunId = event.runId?.trim() || undefined;
234
- if (typeof event.stopSupported === 'boolean') {
235
- activeRun.backendStopSupported = event.stopSupported;
236
- }
237
- if (typeof event.stopReason === 'string' && event.stopReason.trim().length > 0) {
238
- activeRun.backendStopReason = event.stopReason.trim();
239
- }
240
- const canStopNow = Boolean(activeRun.backendStopSupported && activeRun.backendRunId);
241
- setters.setCanStopCurrentRun(canStopNow);
242
- setters.setStopDisabledReason(
243
- canStopNow
244
- ? null
245
- : activeRun.backendStopReason ?? (activeRun.backendStopSupported ? '__preparing__' : null)
246
- );
247
- }
248
- if (event.sessionKey) {
249
- setSelectedSessionKey((prev) => (prev === event.sessionKey ? prev : event.sessionKey));
232
+ const result = await openStream({
233
+ signal: requestAbortController.signal,
234
+ onReady: (event) => {
235
+ if (runId !== runIdRef.current) {
236
+ return;
237
+ }
238
+ const activeRun = activeRunRef.current;
239
+ if (activeRun && activeRun.localRunId === runId) {
240
+ activeRun.backendRunId = event.runId?.trim() || undefined;
241
+ if (typeof event.stopSupported === 'boolean') {
242
+ activeRun.backendStopSupported = event.stopSupported;
250
243
  }
251
- },
252
- onDelta: (event) => {
253
- if (runId !== runIdRef.current) {
254
- return;
244
+ if (typeof event.stopReason === 'string' && event.stopReason.trim().length > 0) {
245
+ activeRun.backendStopReason = event.stopReason.trim();
255
246
  }
256
- streamText += event.delta;
257
- setters.setStreamingAssistantText(streamText);
247
+ const canStopNow = Boolean(activeRun.backendStopSupported && activeRun.backendRunId);
248
+ setters.setCanStopCurrentRun(canStopNow);
249
+ setters.setStopDisabledReason(
250
+ canStopNow
251
+ ? null
252
+ : activeRun.backendStopReason ?? (activeRun.backendStopSupported ? '__preparing__' : null)
253
+ );
254
+ }
255
+ if (event.sessionKey) {
256
+ setSelectedSessionKey((prev) => (prev === event.sessionKey ? prev : event.sessionKey));
257
+ }
258
+ },
259
+ onDelta: (event) => {
260
+ if (runId !== runIdRef.current) {
261
+ return;
262
+ }
263
+ streamText += event.delta;
264
+ setters.setStreamingAssistantText(streamText);
265
+ setters.setIsAwaitingAssistantOutput(false);
266
+ },
267
+ onSessionEvent: (event) => {
268
+ if (runId !== runIdRef.current) {
269
+ return;
270
+ }
271
+ if (event.data.message?.role === 'user') {
272
+ setters.setOptimisticUserEvent(null);
273
+ }
274
+ upsertStreamingEvent(setters.setStreamingSessionEvents, event.data);
275
+ if (event.data.message?.role === 'assistant') {
276
+ hasAssistantSessionEvent = true;
277
+ streamText = '';
278
+ setters.setStreamingAssistantText('');
258
279
  setters.setIsAwaitingAssistantOutput(false);
259
- },
260
- onSessionEvent: (event) => {
261
- if (runId !== runIdRef.current) {
262
- return;
263
- }
264
- if (event.data.message?.role === 'user') {
265
- setters.setOptimisticUserEvent(null);
266
- }
267
- setters.setStreamingSessionEvents((prev) => {
268
- const next = [...prev];
269
- const hit = next.findIndex((streamEvent) => streamEvent.seq === event.data.seq);
270
- if (hit >= 0) {
271
- next[hit] = event.data;
272
- } else {
273
- next.push(event.data);
274
- }
275
- return next;
276
- });
277
- if (event.data.message?.role === 'assistant') {
278
- hasAssistantSessionEvent = true;
279
- streamText = '';
280
- setters.setStreamingAssistantText('');
281
- setters.setIsAwaitingAssistantOutput(false);
282
- }
283
280
  }
284
281
  }
285
- );
282
+ });
286
283
  if (runId !== runIdRef.current) {
287
284
  return;
288
285
  }
289
286
  setters.setOptimisticUserEvent(null);
290
- if (result.sessionKey !== item.sessionKey) {
287
+ if (result.sessionKey !== sourceSessionKey) {
291
288
  setSelectedSessionKey(result.sessionKey);
292
289
  }
293
290
 
294
291
  const localAssistantText = !hasAssistantSessionEvent ? streamText.trim() : '';
295
292
  await refetchIfSessionVisible({
296
293
  selectedSessionKeyRef,
297
- currentSessionKey: item.sessionKey,
294
+ currentSessionKey: sourceSessionKey,
298
295
  resultSessionKey: result.sessionKey,
299
296
  refetchSessions,
300
297
  refetchHistory
@@ -317,23 +314,14 @@ async function executeSendRun(params: {
317
314
  const wasAborted = requestAbortController.signal.aborted || isAbortLikeError(error);
318
315
  runIdRef.current += 1;
319
316
  if (wasAborted) {
320
- const localAssistantText = streamText.trim();
321
- setters.setOptimisticUserEvent(null);
322
- setters.setStreamingAssistantText('');
323
- setters.setStreamingAssistantTimestamp(null);
324
- setters.setIsSending(false);
325
- setters.setIsAwaitingAssistantOutput(false);
326
- setters.setCanStopCurrentRun(false);
327
- setters.setStopDisabledReason(null);
328
- setters.setLastSendError(null);
317
+ clearStreamingState(setters);
329
318
  activeRunRef.current = null;
330
319
  await refetchIfSessionVisible({
331
320
  selectedSessionKeyRef,
332
- currentSessionKey: item.sessionKey,
321
+ currentSessionKey: sourceSessionKey,
333
322
  refetchSessions,
334
323
  refetchHistory
335
324
  });
336
- setters.setStreamingSessionEvents(localAssistantText ? [buildLocalAssistantEvent(localAssistantText)] : []);
337
325
  return;
338
326
  }
339
327
 
@@ -343,7 +331,7 @@ async function executeSendRun(params: {
343
331
  setters.setStreamingSessionEvents([buildLocalAssistantEvent(sendError, 'message.assistant.error.local')]);
344
332
  activeRunRef.current = null;
345
333
  if (restoreDraftOnError) {
346
- setDraft((prev) => (prev.trim().length === 0 ? item.message : prev));
334
+ setDraft((prev) => (prev.trim().length === 0 && sourceMessage ? sourceMessage : prev));
347
335
  }
348
336
  }
349
337
  }
@@ -394,18 +382,51 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
394
382
  async (item: PendingChatMessage, options?: { restoreDraftOnError?: boolean }) => {
395
383
  setLastSendError(null);
396
384
  streamRunIdRef.current += 1;
397
- await executeSendRun({
398
- item,
385
+ const requestedSkills = normalizeRequestedSkills(item.requestedSkills);
386
+ await executeStreamRun({
399
387
  runId: streamRunIdRef.current,
400
388
  runIdRef: streamRunIdRef,
401
389
  activeRunRef,
402
- nextOptimisticUserSeq: params.nextOptimisticUserSeq,
403
390
  selectedSessionKeyRef: params.selectedSessionKeyRef,
404
391
  setSelectedSessionKey: params.setSelectedSessionKey,
405
392
  setDraft: params.setDraft,
406
393
  refetchSessions: params.refetchSessions,
407
394
  refetchHistory: params.refetchHistory,
408
395
  restoreDraftOnError: options?.restoreDraftOnError,
396
+ sourceSessionKey: item.sessionKey,
397
+ sourceAgentId: item.agentId,
398
+ sourceMessage: item.message,
399
+ sourceStopSupported: item.stopSupported,
400
+ sourceStopReason: item.stopReason,
401
+ optimisticUserEvent: {
402
+ seq: params.nextOptimisticUserSeq,
403
+ type: 'message.user.optimistic',
404
+ timestamp: new Date().toISOString(),
405
+ message: {
406
+ role: 'user',
407
+ content: item.message,
408
+ timestamp: new Date().toISOString()
409
+ }
410
+ },
411
+ openStream: ({ signal, onReady, onDelta, onSessionEvent }) =>
412
+ sendChatTurnStream(
413
+ {
414
+ message: item.message,
415
+ sessionKey: item.sessionKey,
416
+ agentId: item.agentId,
417
+ ...(item.model ? { model: item.model } : {}),
418
+ ...(requestedSkills.length > 0
419
+ ? {
420
+ metadata: {
421
+ requested_skills: requestedSkills
422
+ }
423
+ }
424
+ : {}),
425
+ channel: 'ui',
426
+ chatId: 'web-ui'
427
+ },
428
+ { signal, onReady, onDelta, onSessionEvent }
429
+ ),
409
430
  setters: {
410
431
  setOptimisticUserEvent,
411
432
  setStreamingSessionEvents,
@@ -422,6 +443,60 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
422
443
  [params]
423
444
  );
424
445
 
446
+ const resumeRun = useCallback(
447
+ async (run: ChatRunView) => {
448
+ const runId = run.runId?.trim();
449
+ const sessionKey = run.sessionKey?.trim();
450
+ if (!runId || !sessionKey) {
451
+ return;
452
+ }
453
+ const active = activeRunRef.current;
454
+ if (active?.backendRunId === runId) {
455
+ return;
456
+ }
457
+ if (isSending && active) {
458
+ return;
459
+ }
460
+
461
+ setLastSendError(null);
462
+ streamRunIdRef.current += 1;
463
+ await executeStreamRun({
464
+ runId: streamRunIdRef.current,
465
+ runIdRef: streamRunIdRef,
466
+ activeRunRef,
467
+ selectedSessionKeyRef: params.selectedSessionKeyRef,
468
+ setSelectedSessionKey: params.setSelectedSessionKey,
469
+ setDraft: params.setDraft,
470
+ refetchSessions: params.refetchSessions,
471
+ refetchHistory: params.refetchHistory,
472
+ sourceSessionKey: sessionKey,
473
+ sourceAgentId: run.agentId,
474
+ sourceStopSupported: run.stopSupported,
475
+ sourceStopReason: run.stopReason,
476
+ optimisticUserEvent: null,
477
+ openStream: ({ signal, onReady, onDelta, onSessionEvent }) =>
478
+ streamChatRun(
479
+ {
480
+ runId
481
+ },
482
+ { signal, onReady, onDelta, onSessionEvent }
483
+ ),
484
+ setters: {
485
+ setOptimisticUserEvent,
486
+ setStreamingSessionEvents,
487
+ setStreamingAssistantText,
488
+ setStreamingAssistantTimestamp,
489
+ setIsSending,
490
+ setIsAwaitingAssistantOutput,
491
+ setCanStopCurrentRun,
492
+ setStopDisabledReason,
493
+ setLastSendError
494
+ }
495
+ });
496
+ },
497
+ [isSending, params]
498
+ );
499
+
425
500
  useEffect(() => {
426
501
  if (isSending || queuedMessages.length === 0) {
427
502
  return;
@@ -472,7 +547,7 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
472
547
  await stopChatTurn({
473
548
  runId: activeRun.backendRunId,
474
549
  sessionKey: activeRun.sessionKey,
475
- agentId: activeRun.agentId
550
+ ...(activeRun.agentId ? { agentId: activeRun.agentId } : {})
476
551
  });
477
552
  } catch {
478
553
  // Keep local abort as fallback even if stop API fails.
@@ -492,7 +567,9 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
492
567
  canStopCurrentRun,
493
568
  stopDisabledReason,
494
569
  lastSendError,
570
+ activeBackendRunId: activeRunRef.current?.backendRunId ?? null,
495
571
  sendMessage,
572
+ resumeRun,
496
573
  stopCurrentRun,
497
574
  resetStreamState
498
575
  };
@@ -17,6 +17,8 @@ import {
17
17
  updateSession,
18
18
  deleteSession,
19
19
  sendChatTurn,
20
+ fetchChatRun,
21
+ fetchChatRuns,
20
22
  fetchChatCapabilities,
21
23
  fetchCronJobs,
22
24
  deleteCronJob,
@@ -260,6 +262,33 @@ export function useChatCapabilities(params?: { sessionKey?: string | null; agent
260
262
  });
261
263
  }
262
264
 
265
+ export function useChatRuns(params?: { sessionKey?: string | null; states?: Array<'queued' | 'running' | 'completed' | 'failed' | 'aborted'>; limit?: number }) {
266
+ const sessionKey = params?.sessionKey?.trim() || undefined;
267
+ const states = Array.isArray(params?.states) && params.states.length > 0 ? params.states : undefined;
268
+ return useQuery({
269
+ queryKey: ['chat-runs', sessionKey ?? null, states ?? null, params?.limit ?? null],
270
+ queryFn: () => fetchChatRuns({
271
+ ...(sessionKey ? { sessionKey } : {}),
272
+ ...(states ? { states } : {}),
273
+ ...(typeof params?.limit === 'number' ? { limit: params.limit } : {})
274
+ }),
275
+ enabled: Boolean(sessionKey) || Boolean(states),
276
+ staleTime: 5_000,
277
+ retry: false
278
+ });
279
+ }
280
+
281
+ export function useChatRun(runId: string | null) {
282
+ const normalizedRunId = runId?.trim() || null;
283
+ return useQuery({
284
+ queryKey: ['chat-run', normalizedRunId],
285
+ queryFn: () => fetchChatRun(normalizedRunId as string),
286
+ enabled: Boolean(normalizedRunId),
287
+ staleTime: 5_000,
288
+ retry: false
289
+ });
290
+ }
291
+
263
292
  export function useCronJobs(params: { all?: boolean } = { all: true }) {
264
293
  return useQuery({
265
294
  queryKey: ['cron', params],
@@ -49,6 +49,27 @@ export function useWebSocket(queryClient?: QueryClient) {
49
49
  }
50
50
  });
51
51
 
52
+ client.on('run.updated', (event) => {
53
+ if (event.type !== 'run.updated') {
54
+ return;
55
+ }
56
+ if (!queryClient) {
57
+ return;
58
+ }
59
+ const sessionKey = event.payload.run.sessionKey;
60
+ const runId = event.payload.run.runId;
61
+ queryClient.invalidateQueries({ queryKey: ['chat-runs'] });
62
+ if (sessionKey) {
63
+ queryClient.invalidateQueries({ queryKey: ['sessions'] });
64
+ queryClient.invalidateQueries({ queryKey: ['session-history', sessionKey] });
65
+ } else {
66
+ queryClient.invalidateQueries({ queryKey: ['session-history'] });
67
+ }
68
+ if (runId) {
69
+ queryClient.invalidateQueries({ queryKey: ['chat-run', runId] });
70
+ }
71
+ });
72
+
52
73
  client.on('error', (event) => {
53
74
  if (event.type === 'error') {
54
75
  console.error('WebSocket error:', event.payload.message);