@thrillee/aegischat 0.1.5 → 0.1.6

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.
package/dist/index.js CHANGED
@@ -285,15 +285,28 @@ var MAX_RECONNECT_ATTEMPTS = 5;
285
285
  var MAX_RECONNECT_DELAY = 3e4;
286
286
  var PING_INTERVAL = 3e4;
287
287
  var SESSION_STORAGE_KEY = "@aegischat/activeChannel";
288
- function useChat(options) {
289
- const { config, role, clientId, initialSession, autoConnect = true, onMessage, onTyping, onConnectionChange } = options;
290
- const [session, setSession] = (0, import_react.useState)(initialSession ?? null);
288
+ function useChat(options = {}) {
289
+ const {
290
+ config,
291
+ role,
292
+ clientId,
293
+ initialSession,
294
+ autoConnect = true,
295
+ onMessage,
296
+ onTyping,
297
+ onConnectionChange
298
+ } = options;
299
+ const [session, setSession] = (0, import_react.useState)(null);
291
300
  const [isConnected, setIsConnected] = (0, import_react.useState)(false);
292
301
  const [isConnecting, setIsConnecting] = (0, import_react.useState)(false);
293
- const [activeChannelId, setActiveChannelIdState] = (0, import_react.useState)(null);
302
+ const [activeChannelId, setActiveChannelIdState] = (0, import_react.useState)(
303
+ null
304
+ );
294
305
  const [channels, setChannels] = (0, import_react.useState)([]);
295
306
  const [messages, setMessages] = (0, import_react.useState)([]);
296
- const [typingUsers, setTypingUsers] = (0, import_react.useState)({});
307
+ const [typingUsers, setTypingUsers] = (0, import_react.useState)(
308
+ {}
309
+ );
297
310
  const [isLoadingChannels, setIsLoadingChannels] = (0, import_react.useState)(false);
298
311
  const [isLoadingMessages, setIsLoadingMessages] = (0, import_react.useState)(false);
299
312
  const [hasMoreMessages, setHasMoreMessages] = (0, import_react.useState)(true);
@@ -306,23 +319,19 @@ function useChat(options) {
306
319
  const isManualDisconnect = (0, import_react.useRef)(false);
307
320
  const oldestMessageId = (0, import_react.useRef)(null);
308
321
  const activeChannelIdRef = (0, import_react.useRef)(null);
309
- const configRef = (0, import_react.useRef)(config);
310
- const sessionRef = (0, import_react.useRef)(initialSession ?? null);
311
- if (initialSession && !config) {
312
- configureApiClient({
313
- baseUrl: initialSession.api_url,
314
- getAccessToken: async () => sessionRef.current?.access_token || ""
315
- });
316
- }
317
- (0, import_react.useEffect)(() => {
318
- configRef.current = config;
319
- }, [config]);
322
+ const sessionRef = (0, import_react.useRef)(null);
323
+ const roleRef = (0, import_react.useRef)(void 0);
324
+ const clientIdRef = (0, import_react.useRef)(void 0);
325
+ const autoConnectRef = (0, import_react.useRef)(true);
326
+ const onMessageRef = (0, import_react.useRef)(void 0);
327
+ const onTypingRef = (0, import_react.useRef)(void 0);
328
+ const onConnectionChangeRef = (0, import_react.useRef)(void 0);
320
329
  (0, import_react.useEffect)(() => {
321
330
  activeChannelIdRef.current = activeChannelId;
322
331
  }, [activeChannelId]);
323
332
  (0, import_react.useEffect)(() => {
324
- sessionRef.current = initialSession ?? null;
325
- }, [initialSession]);
333
+ activeChannelIdRef.current = activeChannelId;
334
+ }, [activeChannelId]);
326
335
  const getActiveChannelId = (0, import_react.useCallback)(() => {
327
336
  if (typeof window === "undefined") return null;
328
337
  return sessionStorage.getItem(SESSION_STORAGE_KEY);
@@ -337,26 +346,29 @@ function useChat(options) {
337
346
  }
338
347
  }
339
348
  }, []);
340
- const fetchFromComms = (0, import_react.useCallback)(async (path, fetchOptions = {}) => {
341
- const currentSession = sessionRef.current;
342
- if (!currentSession) {
343
- throw new Error("Chat session not initialized");
344
- }
345
- const response = await fetch(`${currentSession.api_url}${path}`, {
346
- ...fetchOptions,
347
- headers: {
348
- "Content-Type": "application/json",
349
- Authorization: `Bearer ${currentSession.access_token}`,
350
- ...fetchOptions.headers
349
+ const fetchFromComms = (0, import_react.useCallback)(
350
+ async (path, fetchOptions = {}) => {
351
+ const currentSession = sessionRef.current;
352
+ if (!currentSession) {
353
+ throw new Error("Chat session not initialized");
351
354
  }
352
- });
353
- if (!response.ok) {
354
- const error = await response.json().catch(() => ({}));
355
- throw new Error(error.message || `HTTP ${response.status}`);
356
- }
357
- const data = await response.json();
358
- return data.data || data;
359
- }, []);
355
+ const response = await fetch(`${currentSession.api_url}${path}`, {
356
+ ...fetchOptions,
357
+ headers: {
358
+ "Content-Type": "application/json",
359
+ Authorization: `Bearer ${currentSession.access_token}`,
360
+ ...fetchOptions.headers
361
+ }
362
+ });
363
+ if (!response.ok) {
364
+ const error = await response.json().catch(() => ({}));
365
+ throw new Error(error.message || `HTTP ${response.status}`);
366
+ }
367
+ const data = await response.json();
368
+ return data.data || data;
369
+ },
370
+ []
371
+ );
360
372
  const clearTimers = (0, import_react.useCallback)(() => {
361
373
  if (reconnectTimeout.current) {
362
374
  clearTimeout(reconnectTimeout.current);
@@ -367,111 +379,141 @@ function useChat(options) {
367
379
  pingInterval.current = null;
368
380
  }
369
381
  }, []);
370
- const handleWebSocketMessage = (0, import_react.useCallback)((data) => {
371
- const currentActiveChannelId = activeChannelIdRef.current;
372
- console.log("[AegisChat] WebSocket message received:", data.type, data);
373
- switch (data.type) {
374
- case "message.new": {
375
- const newMessage = data.payload;
376
- if (newMessage.channel_id === currentActiveChannelId) {
377
- setMessages((prev) => {
378
- const existingIndex = prev.findIndex(
379
- (m) => m.tempId && m.content === newMessage.content && m.status === "sending"
382
+ const handleWebSocketMessage = (0, import_react.useCallback)(
383
+ (data) => {
384
+ const currentActiveChannelId = activeChannelIdRef.current;
385
+ console.log("[AegisChat] WebSocket message received:", data.type, data);
386
+ switch (data.type) {
387
+ case "message.new": {
388
+ const newMessage = data.payload;
389
+ if (newMessage.channel_id === currentActiveChannelId) {
390
+ setMessages((prev) => {
391
+ const existingIndex = prev.findIndex(
392
+ (m) => m.tempId && m.content === newMessage.content && m.status === "sending"
393
+ );
394
+ if (existingIndex !== -1) {
395
+ const updated = [...prev];
396
+ updated[existingIndex] = { ...newMessage, status: "sent" };
397
+ return updated;
398
+ }
399
+ if (prev.some((m) => m.id === newMessage.id)) return prev;
400
+ return [...prev, { ...newMessage, status: "delivered" }];
401
+ });
402
+ onMessageRef.current?.(newMessage);
403
+ }
404
+ setChannels((prev) => {
405
+ const updated = prev.map(
406
+ (ch) => ch.id === newMessage.channel_id ? {
407
+ ...ch,
408
+ last_message: {
409
+ id: newMessage.id,
410
+ content: newMessage.content,
411
+ created_at: newMessage.created_at,
412
+ sender: {
413
+ id: newMessage.sender_id,
414
+ display_name: "Unknown",
415
+ status: "online"
416
+ }
417
+ },
418
+ unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
419
+ } : ch
380
420
  );
381
- if (existingIndex !== -1) {
382
- const updated = [...prev];
383
- updated[existingIndex] = { ...newMessage, status: "sent" };
384
- return updated;
385
- }
386
- if (prev.some((m) => m.id === newMessage.id)) return prev;
387
- return [...prev, { ...newMessage, status: "delivered" }];
421
+ return updated.sort((a, b) => {
422
+ const timeA = a.last_message?.created_at || "";
423
+ const timeB = b.last_message?.created_at || "";
424
+ return timeB.localeCompare(timeA);
425
+ });
388
426
  });
389
- onMessage?.(newMessage);
427
+ break;
390
428
  }
391
- setChannels((prev) => {
392
- const updated = prev.map(
393
- (ch) => ch.id === newMessage.channel_id ? {
394
- ...ch,
395
- last_message: {
396
- id: newMessage.id,
397
- content: newMessage.content,
398
- created_at: newMessage.created_at,
399
- sender: { id: newMessage.sender_id, display_name: "Unknown", status: "online" }
400
- },
401
- unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
402
- } : ch
403
- );
404
- return updated.sort((a, b) => {
405
- const timeA = a.last_message?.created_at || "";
406
- const timeB = b.last_message?.created_at || "";
407
- return timeB.localeCompare(timeA);
408
- });
409
- });
410
- break;
411
- }
412
- case "message.updated": {
413
- const updatedMessage = data.payload;
414
- setMessages((prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m));
415
- break;
416
- }
417
- case "message.deleted": {
418
- const { message_id } = data.payload;
419
- setMessages((prev) => prev.map((m) => m.id === message_id ? { ...m, deleted: true } : m));
420
- break;
421
- }
422
- case "message.delivered":
423
- case "message.read": {
424
- const { message_id, channel_id, status } = data.payload;
425
- if (channel_id === currentActiveChannelId) {
429
+ case "message.updated": {
430
+ const updatedMessage = data.payload;
426
431
  setMessages(
427
- (prev) => prev.map((m) => m.id === message_id ? { ...m, status: status || "delivered" } : m)
432
+ (prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m)
428
433
  );
434
+ break;
429
435
  }
430
- break;
431
- }
432
- case "message.delivered.batch":
433
- case "message.read.batch": {
434
- const { channel_id } = data.payload;
435
- if (channel_id === currentActiveChannelId) {
436
+ case "message.deleted": {
437
+ const { message_id } = data.payload;
436
438
  setMessages(
437
- (prev) => prev.map((m) => m.status === "sent" || m.status === "delivered" ? { ...m, status: data.type === "message.delivered.batch" ? "delivered" : "read" } : m)
439
+ (prev) => prev.map(
440
+ (m) => m.id === message_id ? { ...m, deleted: true } : m
441
+ )
438
442
  );
443
+ break;
439
444
  }
440
- break;
441
- }
442
- case "typing.start": {
443
- const { channel_id, user } = data.payload;
444
- const typingUser = {
445
- id: user.id,
446
- displayName: user.display_name,
447
- avatarUrl: user.avatar_url,
448
- startedAt: Date.now()
449
- };
450
- setTypingUsers((prev) => ({
451
- ...prev,
452
- [channel_id]: [...(prev[channel_id] || []).filter((u) => u.id !== user.id), typingUser]
453
- }));
454
- onTyping?.(channel_id, typingUser);
455
- break;
456
- }
457
- case "typing.stop": {
458
- const { channel_id, user_id } = data.payload;
459
- setTypingUsers((prev) => ({
460
- ...prev,
461
- [channel_id]: (prev[channel_id] || []).filter((u) => u.id !== user_id)
462
- }));
463
- break;
445
+ case "message.delivered":
446
+ case "message.read": {
447
+ const { message_id, channel_id, status } = data.payload;
448
+ if (channel_id === currentActiveChannelId) {
449
+ setMessages(
450
+ (prev) => prev.map(
451
+ (m) => m.id === message_id ? {
452
+ ...m,
453
+ status: status || "delivered"
454
+ } : m
455
+ )
456
+ );
457
+ }
458
+ break;
459
+ }
460
+ case "message.delivered.batch":
461
+ case "message.read.batch": {
462
+ const { channel_id } = data.payload;
463
+ if (channel_id === currentActiveChannelId) {
464
+ setMessages(
465
+ (prev) => prev.map(
466
+ (m) => m.status === "sent" || m.status === "delivered" ? {
467
+ ...m,
468
+ status: data.type === "message.delivered.batch" ? "delivered" : "read"
469
+ } : m
470
+ )
471
+ );
472
+ }
473
+ break;
474
+ }
475
+ case "typing.start": {
476
+ const { channel_id, user } = data.payload;
477
+ const typingUser = {
478
+ id: user.id,
479
+ displayName: user.display_name,
480
+ avatarUrl: user.avatar_url,
481
+ startedAt: Date.now()
482
+ };
483
+ setTypingUsers((prev) => ({
484
+ ...prev,
485
+ [channel_id]: [
486
+ ...(prev[channel_id] || []).filter((u) => u.id !== user.id),
487
+ typingUser
488
+ ]
489
+ }));
490
+ onTypingRef.current?.(channel_id, typingUser);
491
+ break;
492
+ }
493
+ case "typing.stop": {
494
+ const { channel_id, user_id } = data.payload;
495
+ setTypingUsers((prev) => ({
496
+ ...prev,
497
+ [channel_id]: (prev[channel_id] || []).filter(
498
+ (u) => u.id !== user_id
499
+ )
500
+ }));
501
+ break;
502
+ }
503
+ case "pong":
504
+ break;
505
+ default:
506
+ console.log("[AegisChat] Unhandled message type:", data.type);
464
507
  }
465
- case "pong":
466
- break;
467
- default:
468
- console.log("[AegisChat] Unhandled message type:", data.type);
469
- }
470
- }, [onMessage, onTyping]);
508
+ },
509
+ []
510
+ );
471
511
  const connectWebSocket = (0, import_react.useCallback)(() => {
472
512
  const currentSession = sessionRef.current;
473
513
  if (!currentSession?.websocket_url || !currentSession?.access_token) {
474
- console.warn("[AegisChat] Cannot connect WebSocket - missing session or token");
514
+ console.warn(
515
+ "[AegisChat] Cannot connect WebSocket - missing session or token"
516
+ );
475
517
  return;
476
518
  }
477
519
  if (wsRef.current?.readyState === WebSocket.OPEN) {
@@ -488,14 +530,19 @@ function useChat(options) {
488
530
  setIsConnected(true);
489
531
  setIsConnecting(false);
490
532
  reconnectAttempts.current = 0;
491
- onConnectionChange?.(true);
533
+ onConnectionChangeRef.current?.(true);
492
534
  pingInterval.current = setInterval(() => {
493
535
  if (ws.readyState === WebSocket.OPEN) {
494
536
  ws.send(JSON.stringify({ type: "ping" }));
495
537
  }
496
538
  }, PING_INTERVAL);
497
539
  if (activeChannelIdRef.current) {
498
- ws.send(JSON.stringify({ type: "channel.join", payload: { channel_id: activeChannelIdRef.current } }));
540
+ ws.send(
541
+ JSON.stringify({
542
+ type: "channel.join",
543
+ payload: { channel_id: activeChannelIdRef.current }
544
+ })
545
+ );
499
546
  }
500
547
  };
501
548
  ws.onmessage = (event) => {
@@ -511,9 +558,12 @@ function useChat(options) {
511
558
  setIsConnected(false);
512
559
  setIsConnecting(false);
513
560
  clearTimers();
514
- onConnectionChange?.(false);
561
+ onConnectionChangeRef.current?.(false);
515
562
  if (!isManualDisconnect.current && reconnectAttempts.current < MAX_RECONNECT_ATTEMPTS) {
516
- const delay = Math.min(RECONNECT_INTERVAL * Math.pow(2, reconnectAttempts.current), MAX_RECONNECT_DELAY);
563
+ const delay = Math.min(
564
+ RECONNECT_INTERVAL * Math.pow(2, reconnectAttempts.current),
565
+ MAX_RECONNECT_DELAY
566
+ );
517
567
  console.log(`[AegisChat] Reconnecting in ${delay}ms...`);
518
568
  reconnectTimeout.current = setTimeout(() => {
519
569
  reconnectAttempts.current++;
@@ -525,14 +575,18 @@ function useChat(options) {
525
575
  console.error("[AegisChat] WebSocket error:", error);
526
576
  };
527
577
  wsRef.current = ws;
528
- }, [clearTimers, handleWebSocketMessage, onConnectionChange]);
578
+ }, [clearTimers, handleWebSocketMessage]);
529
579
  const connect = (0, import_react.useCallback)(async () => {
530
580
  console.log("[AegisChat] connect() called");
531
- const targetSession = sessionRef.current ?? initialSession;
581
+ const targetSession = sessionRef.current;
532
582
  if (!targetSession) {
533
583
  console.log("[AegisChat] No session available, skipping connect");
534
584
  return;
535
585
  }
586
+ if (!autoConnectRef.current) {
587
+ console.log("[AegisChat] autoConnect is false, skipping connect");
588
+ return;
589
+ }
536
590
  connectWebSocket();
537
591
  }, [connectWebSocket]);
538
592
  const disconnect = (0, import_react.useCallback)(() => {
@@ -560,48 +614,72 @@ function useChat(options) {
560
614
  setIsLoadingChannels(false);
561
615
  }
562
616
  }, []);
563
- const selectChannel = (0, import_react.useCallback)(async (channelId) => {
564
- const currentActiveChannelId = activeChannelIdRef.current;
565
- setActiveChannelId(channelId);
566
- setMessages([]);
567
- setHasMoreMessages(true);
568
- oldestMessageId.current = null;
569
- if (wsRef.current?.readyState === WebSocket.OPEN) {
570
- if (currentActiveChannelId) {
571
- wsRef.current.send(JSON.stringify({ type: "channel.leave", payload: { channel_id: currentActiveChannelId } }));
617
+ const selectChannel = (0, import_react.useCallback)(
618
+ async (channelId) => {
619
+ const currentActiveChannelId = activeChannelIdRef.current;
620
+ setActiveChannelId(channelId);
621
+ setMessages([]);
622
+ setHasMoreMessages(true);
623
+ oldestMessageId.current = null;
624
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
625
+ if (currentActiveChannelId) {
626
+ wsRef.current.send(
627
+ JSON.stringify({
628
+ type: "channel.leave",
629
+ payload: { channel_id: currentActiveChannelId }
630
+ })
631
+ );
632
+ }
633
+ wsRef.current.send(
634
+ JSON.stringify({
635
+ type: "channel.join",
636
+ payload: { channel_id: channelId }
637
+ })
638
+ );
572
639
  }
573
- wsRef.current.send(JSON.stringify({ type: "channel.join", payload: { channel_id: channelId } }));
574
- }
575
- setIsLoadingMessages(true);
576
- try {
577
- const response = await fetchFromComms(`/channels/${channelId}/messages?limit=50`);
578
- setMessages(response.messages || []);
579
- setHasMoreMessages(response.has_more);
580
- if (response.oldest_id) {
581
- oldestMessageId.current = response.oldest_id;
640
+ setIsLoadingMessages(true);
641
+ try {
642
+ const response = await fetchFromComms(
643
+ `/channels/${channelId}/messages?limit=50`
644
+ );
645
+ setMessages(response.messages || []);
646
+ setHasMoreMessages(response.has_more);
647
+ if (response.oldest_id) {
648
+ oldestMessageId.current = response.oldest_id;
649
+ }
650
+ await markAsRead(channelId);
651
+ setChannels(
652
+ (prev) => prev.map(
653
+ (ch) => ch.id === channelId ? { ...ch, unread_count: 0 } : ch
654
+ )
655
+ );
656
+ } catch (error) {
657
+ console.error("[AegisChat] Failed to load messages:", error);
658
+ setMessages([]);
659
+ } finally {
660
+ setIsLoadingMessages(false);
582
661
  }
583
- await markAsRead(channelId);
584
- setChannels((prev) => prev.map((ch) => ch.id === channelId ? { ...ch, unread_count: 0 } : ch));
585
- } catch (error) {
586
- console.error("[AegisChat] Failed to load messages:", error);
587
- setMessages([]);
588
- } finally {
589
- setIsLoadingMessages(false);
590
- }
591
- }, [setActiveChannelId, fetchFromComms]);
592
- const markAsRead = (0, import_react.useCallback)(async (channelId) => {
593
- try {
594
- await fetchFromComms(`/channels/${channelId}/read`, { method: "POST" });
595
- } catch (error) {
596
- console.error("[AegisChat] Failed to mark as read:", error);
597
- }
598
- }, [fetchFromComms]);
662
+ },
663
+ [setActiveChannelId, fetchFromComms]
664
+ );
665
+ const markAsRead = (0, import_react.useCallback)(
666
+ async (channelId) => {
667
+ try {
668
+ await fetchFromComms(`/channels/${channelId}/read`, { method: "POST" });
669
+ } catch (error) {
670
+ console.error("[AegisChat] Failed to mark as read:", error);
671
+ }
672
+ },
673
+ [fetchFromComms]
674
+ );
599
675
  const loadMoreMessages = (0, import_react.useCallback)(async () => {
600
676
  if (!activeChannelId || !hasMoreMessages || isLoadingMessages) return;
601
677
  setIsLoadingMessages(true);
602
678
  try {
603
679
  const params = oldestMessageId.current ? `?before=${oldestMessageId.current}&limit=50` : "?limit=50";
604
- const response = await fetchFromComms(`/channels/${activeChannelId}/messages${params}`);
680
+ const response = await fetchFromComms(
681
+ `/channels/${activeChannelId}/messages${params}`
682
+ );
605
683
  setMessages((prev) => [...response.messages || [], ...prev]);
606
684
  setHasMoreMessages(response.has_more);
607
685
  if (response.oldest_id) {
@@ -613,138 +691,234 @@ function useChat(options) {
613
691
  setIsLoadingMessages(false);
614
692
  }
615
693
  }, [activeChannelId, hasMoreMessages, isLoadingMessages, fetchFromComms]);
616
- const sendMessage = (0, import_react.useCallback)(async (content, msgOptions = {}) => {
617
- const currentActiveChannelId = activeChannelIdRef.current;
618
- const currentSession = sessionRef.current;
619
- if (!currentActiveChannelId || !content.trim() || !currentSession) return;
620
- const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
621
- const trimmedContent = content.trim();
622
- const optimisticMessage = {
623
- id: tempId,
624
- tempId,
625
- channel_id: currentActiveChannelId,
626
- sender_id: currentSession.comms_user_id,
627
- content: trimmedContent,
628
- type: msgOptions.type || "text",
629
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
630
- updated_at: (/* @__PURE__ */ new Date()).toISOString(),
631
- status: "sending",
632
- metadata: msgOptions.metadata || {}
633
- };
634
- setMessages((prev) => [...prev, optimisticMessage]);
635
- const now = (/* @__PURE__ */ new Date()).toISOString();
636
- setChannels((prev) => {
637
- const updated = prev.map(
638
- (ch) => ch.id === currentActiveChannelId ? {
639
- ...ch,
640
- last_message: {
641
- id: tempId,
642
- content: trimmedContent,
643
- created_at: now,
644
- sender: { id: currentSession.comms_user_id, display_name: "You", status: "online" }
645
- }
646
- } : ch
647
- );
648
- return updated.sort((a, b) => {
649
- const timeA = a.last_message?.created_at || "";
650
- const timeB = b.last_message?.created_at || "";
651
- return timeB.localeCompare(timeA);
652
- });
653
- });
654
- try {
655
- await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
656
- method: "POST",
657
- body: JSON.stringify({ content: trimmedContent, type: msgOptions.type || "text", parent_id: msgOptions.parent_id, metadata: msgOptions.metadata })
658
- });
659
- } catch (error) {
660
- console.error("[AegisChat] Failed to send message:", error);
661
- setMessages(
662
- (prev) => prev.map(
663
- (m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m
664
- )
665
- );
666
- throw error;
667
- }
668
- }, [fetchFromComms]);
669
- const uploadFile = (0, import_react.useCallback)(async (file) => {
670
- const currentSession = sessionRef.current;
671
- if (!currentSession) return null;
672
- const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
673
- setUploadProgress((prev) => [...prev, { fileId, fileName: file.name, progress: 0, status: "pending" }]);
674
- try {
675
- setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p));
676
- const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
677
- method: "POST",
678
- body: JSON.stringify({ file_name: file.name, file_type: file.type || "application/octet-stream", file_size: file.size })
679
- });
680
- setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p));
681
- const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
682
- method: "PUT",
683
- body: file,
684
- headers: { "Content-Type": file.type || "application/octet-stream" }
685
- });
686
- if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.statusText}`);
687
- setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p));
688
- const confirmResponse = await fetchFromComms("/files", {
689
- method: "POST",
690
- body: JSON.stringify({ file_id: uploadUrlResponse.file_id })
694
+ const sendMessage = (0, import_react.useCallback)(
695
+ async (content, msgOptions = {}) => {
696
+ const currentActiveChannelId = activeChannelIdRef.current;
697
+ const currentSession = sessionRef.current;
698
+ if (!currentActiveChannelId || !content.trim() || !currentSession) return;
699
+ const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
700
+ const trimmedContent = content.trim();
701
+ const optimisticMessage = {
702
+ id: tempId,
703
+ tempId,
704
+ channel_id: currentActiveChannelId,
705
+ sender_id: currentSession.comms_user_id,
706
+ content: trimmedContent,
707
+ type: msgOptions.type || "text",
708
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
709
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
710
+ status: "sending",
711
+ metadata: msgOptions.metadata || {}
712
+ };
713
+ setMessages((prev) => [...prev, optimisticMessage]);
714
+ const now = (/* @__PURE__ */ new Date()).toISOString();
715
+ setChannels((prev) => {
716
+ const updated = prev.map(
717
+ (ch) => ch.id === currentActiveChannelId ? {
718
+ ...ch,
719
+ last_message: {
720
+ id: tempId,
721
+ content: trimmedContent,
722
+ created_at: now,
723
+ sender: {
724
+ id: currentSession.comms_user_id,
725
+ display_name: "You",
726
+ status: "online"
727
+ }
728
+ }
729
+ } : ch
730
+ );
731
+ return updated.sort((a, b) => {
732
+ const timeA = a.last_message?.created_at || "";
733
+ const timeB = b.last_message?.created_at || "";
734
+ return timeB.localeCompare(timeA);
735
+ });
691
736
  });
692
- setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "complete", progress: 100 } : p));
693
- setTimeout(() => setUploadProgress((prev) => prev.filter((p) => p.fileId !== uploadUrlResponse.file_id)), 2e3);
694
- return confirmResponse.file;
695
- } catch (error) {
696
- console.error("[AegisChat] Failed to upload file:", error);
697
- setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "error", error: error instanceof Error ? error.message : "Upload failed" } : p));
698
- setTimeout(() => setUploadProgress((prev) => prev.filter((p) => p.fileId !== fileId)), 5e3);
699
- return null;
700
- }
701
- }, [fetchFromComms]);
702
- const sendMessageWithFiles = (0, import_react.useCallback)(async (content, files, msgOptions = {}) => {
703
- const currentActiveChannelId = activeChannelIdRef.current;
704
- const currentSession = sessionRef.current;
705
- if (!currentActiveChannelId || !content.trim() && files.length === 0 || !currentSession) return;
706
- const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
707
- const trimmedContent = content.trim();
708
- const optimisticMessage = {
709
- id: tempId,
710
- tempId,
711
- channel_id: currentActiveChannelId,
712
- sender_id: currentSession.comms_user_id,
713
- content: trimmedContent || `Uploading ${files.length} file(s)...`,
714
- type: "file",
715
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
716
- updated_at: (/* @__PURE__ */ new Date()).toISOString(),
717
- status: "sending",
718
- metadata: { ...msgOptions.metadata, files: files.map((f) => ({ id: `temp-${f.name}`, filename: f.name, mime_type: f.type, size: f.size, url: "" })) }
719
- };
720
- setMessages((prev) => [...prev, optimisticMessage]);
721
- try {
722
- const uploadedFiles = [];
723
- for (const file of files) {
724
- const attachment = await uploadFile(file);
725
- if (attachment) uploadedFiles.push(attachment);
737
+ try {
738
+ await fetchFromComms(
739
+ `/channels/${currentActiveChannelId}/messages`,
740
+ {
741
+ method: "POST",
742
+ body: JSON.stringify({
743
+ content: trimmedContent,
744
+ type: msgOptions.type || "text",
745
+ parent_id: msgOptions.parent_id,
746
+ metadata: msgOptions.metadata
747
+ })
748
+ }
749
+ );
750
+ } catch (error) {
751
+ console.error("[AegisChat] Failed to send message:", error);
752
+ setMessages(
753
+ (prev) => prev.map(
754
+ (m) => m.tempId === tempId ? {
755
+ ...m,
756
+ status: "failed",
757
+ errorMessage: error instanceof Error ? error.message : "Failed to send"
758
+ } : m
759
+ )
760
+ );
761
+ throw error;
726
762
  }
727
- const messageType = uploadedFiles.length > 0 && !trimmedContent ? "file" : "text";
728
- await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
729
- method: "POST",
730
- body: JSON.stringify({
731
- content: trimmedContent || (uploadedFiles.length > 0 ? `Shared ${uploadedFiles.length} file(s)` : ""),
732
- type: msgOptions.type || messageType,
733
- parent_id: msgOptions.parent_id,
734
- metadata: { ...msgOptions.metadata, files: uploadedFiles },
735
- file_ids: uploadedFiles.map((f) => f.id)
736
- })
737
- });
738
- } catch (error) {
739
- console.error("[AegisChat] Failed to send message with files:", error);
740
- setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m));
741
- throw error;
742
- }
743
- }, [fetchFromComms, uploadFile]);
763
+ },
764
+ [fetchFromComms]
765
+ );
766
+ const uploadFile = (0, import_react.useCallback)(
767
+ async (file) => {
768
+ const currentSession = sessionRef.current;
769
+ if (!currentSession) return null;
770
+ const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
771
+ setUploadProgress((prev) => [
772
+ ...prev,
773
+ { fileId, fileName: file.name, progress: 0, status: "pending" }
774
+ ]);
775
+ try {
776
+ setUploadProgress(
777
+ (prev) => prev.map(
778
+ (p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p
779
+ )
780
+ );
781
+ const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
782
+ method: "POST",
783
+ body: JSON.stringify({
784
+ file_name: file.name,
785
+ file_type: file.type || "application/octet-stream",
786
+ file_size: file.size
787
+ })
788
+ });
789
+ setUploadProgress(
790
+ (prev) => prev.map(
791
+ (p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p
792
+ )
793
+ );
794
+ const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
795
+ method: "PUT",
796
+ body: file,
797
+ headers: { "Content-Type": file.type || "application/octet-stream" }
798
+ });
799
+ if (!uploadResponse.ok)
800
+ throw new Error(`Upload failed: ${uploadResponse.statusText}`);
801
+ setUploadProgress(
802
+ (prev) => prev.map(
803
+ (p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p
804
+ )
805
+ );
806
+ const confirmResponse = await fetchFromComms(
807
+ "/files",
808
+ {
809
+ method: "POST",
810
+ body: JSON.stringify({ file_id: uploadUrlResponse.file_id })
811
+ }
812
+ );
813
+ setUploadProgress(
814
+ (prev) => prev.map(
815
+ (p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "complete", progress: 100 } : p
816
+ )
817
+ );
818
+ setTimeout(
819
+ () => setUploadProgress(
820
+ (prev) => prev.filter((p) => p.fileId !== uploadUrlResponse.file_id)
821
+ ),
822
+ 2e3
823
+ );
824
+ return confirmResponse.file;
825
+ } catch (error) {
826
+ console.error("[AegisChat] Failed to upload file:", error);
827
+ setUploadProgress(
828
+ (prev) => prev.map(
829
+ (p) => p.fileId === fileId ? {
830
+ ...p,
831
+ status: "error",
832
+ error: error instanceof Error ? error.message : "Upload failed"
833
+ } : p
834
+ )
835
+ );
836
+ setTimeout(
837
+ () => setUploadProgress(
838
+ (prev) => prev.filter((p) => p.fileId !== fileId)
839
+ ),
840
+ 5e3
841
+ );
842
+ return null;
843
+ }
844
+ },
845
+ [fetchFromComms]
846
+ );
847
+ const sendMessageWithFiles = (0, import_react.useCallback)(
848
+ async (content, files, msgOptions = {}) => {
849
+ const currentActiveChannelId = activeChannelIdRef.current;
850
+ const currentSession = sessionRef.current;
851
+ if (!currentActiveChannelId || !content.trim() && files.length === 0 || !currentSession)
852
+ return;
853
+ const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
854
+ const trimmedContent = content.trim();
855
+ const optimisticMessage = {
856
+ id: tempId,
857
+ tempId,
858
+ channel_id: currentActiveChannelId,
859
+ sender_id: currentSession.comms_user_id,
860
+ content: trimmedContent || `Uploading ${files.length} file(s)...`,
861
+ type: "file",
862
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
863
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
864
+ status: "sending",
865
+ metadata: {
866
+ ...msgOptions.metadata,
867
+ files: files.map((f) => ({
868
+ id: `temp-${f.name}`,
869
+ filename: f.name,
870
+ mime_type: f.type,
871
+ size: f.size,
872
+ url: ""
873
+ }))
874
+ }
875
+ };
876
+ setMessages((prev) => [...prev, optimisticMessage]);
877
+ try {
878
+ const uploadedFiles = [];
879
+ for (const file of files) {
880
+ const attachment = await uploadFile(file);
881
+ if (attachment) uploadedFiles.push(attachment);
882
+ }
883
+ const messageType = uploadedFiles.length > 0 && !trimmedContent ? "file" : "text";
884
+ await fetchFromComms(
885
+ `/channels/${currentActiveChannelId}/messages`,
886
+ {
887
+ method: "POST",
888
+ body: JSON.stringify({
889
+ content: trimmedContent || (uploadedFiles.length > 0 ? `Shared ${uploadedFiles.length} file(s)` : ""),
890
+ type: msgOptions.type || messageType,
891
+ parent_id: msgOptions.parent_id,
892
+ metadata: { ...msgOptions.metadata, files: uploadedFiles },
893
+ file_ids: uploadedFiles.map((f) => f.id)
894
+ })
895
+ }
896
+ );
897
+ } catch (error) {
898
+ console.error("[AegisChat] Failed to send message with files:", error);
899
+ setMessages(
900
+ (prev) => prev.map(
901
+ (m) => m.tempId === tempId ? {
902
+ ...m,
903
+ status: "failed",
904
+ errorMessage: error instanceof Error ? error.message : "Failed to send"
905
+ } : m
906
+ )
907
+ );
908
+ throw error;
909
+ }
910
+ },
911
+ [fetchFromComms, uploadFile]
912
+ );
744
913
  const stopTyping = (0, import_react.useCallback)(() => {
745
914
  const currentActiveChannelId = activeChannelIdRef.current;
746
915
  if (!currentActiveChannelId || !wsRef.current) return;
747
- wsRef.current.send(JSON.stringify({ type: "typing.stop", payload: { channel_id: currentActiveChannelId } }));
916
+ wsRef.current.send(
917
+ JSON.stringify({
918
+ type: "typing.stop",
919
+ payload: { channel_id: currentActiveChannelId }
920
+ })
921
+ );
748
922
  if (typingTimeout.current) {
749
923
  clearTimeout(typingTimeout.current);
750
924
  typingTimeout.current = null;
@@ -753,46 +927,106 @@ function useChat(options) {
753
927
  const startTyping = (0, import_react.useCallback)(() => {
754
928
  const currentActiveChannelId = activeChannelIdRef.current;
755
929
  if (!currentActiveChannelId || !wsRef.current) return;
756
- wsRef.current.send(JSON.stringify({ type: "typing.start", payload: { channel_id: currentActiveChannelId } }));
930
+ wsRef.current.send(
931
+ JSON.stringify({
932
+ type: "typing.start",
933
+ payload: { channel_id: currentActiveChannelId }
934
+ })
935
+ );
757
936
  if (typingTimeout.current) clearTimeout(typingTimeout.current);
758
937
  typingTimeout.current = setTimeout(stopTyping, TYPING_TIMEOUT);
759
938
  }, [stopTyping]);
760
- const createDMWithUser = (0, import_react.useCallback)(async (userId) => {
761
- try {
762
- const channel = await fetchFromComms("/channels/dm", {
763
- method: "POST",
764
- body: JSON.stringify({ user_id: userId })
765
- });
766
- await refreshChannels();
767
- return channel.id;
768
- } catch (error) {
769
- console.error("[AegisChat] Failed to create DM:", error);
770
- return null;
771
- }
772
- }, [fetchFromComms, refreshChannels]);
773
- const retryMessage = (0, import_react.useCallback)(async (tempId) => {
774
- const failedMessage = messages.find((m) => m.tempId === tempId && m.status === "failed");
775
- const currentActiveChannelId = activeChannelIdRef.current;
776
- if (!failedMessage || !currentActiveChannelId) return;
777
- setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "sending", errorMessage: void 0 } : m));
778
- try {
779
- await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
780
- method: "POST",
781
- body: JSON.stringify({ content: failedMessage.content, type: failedMessage.type, metadata: failedMessage.metadata })
782
- });
783
- } catch (error) {
784
- console.error("[AegisChat] Failed to retry message:", error);
785
- setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m));
786
- }
787
- }, [messages, fetchFromComms]);
939
+ const createDMWithUser = (0, import_react.useCallback)(
940
+ async (userId) => {
941
+ try {
942
+ const channel = await fetchFromComms("/channels/dm", {
943
+ method: "POST",
944
+ body: JSON.stringify({ user_id: userId })
945
+ });
946
+ await refreshChannels();
947
+ return channel.id;
948
+ } catch (error) {
949
+ console.error("[AegisChat] Failed to create DM:", error);
950
+ return null;
951
+ }
952
+ },
953
+ [fetchFromComms, refreshChannels]
954
+ );
955
+ const retryMessage = (0, import_react.useCallback)(
956
+ async (tempId) => {
957
+ const failedMessage = messages.find(
958
+ (m) => m.tempId === tempId && m.status === "failed"
959
+ );
960
+ const currentActiveChannelId = activeChannelIdRef.current;
961
+ if (!failedMessage || !currentActiveChannelId) return;
962
+ setMessages(
963
+ (prev) => prev.map(
964
+ (m) => m.tempId === tempId ? { ...m, status: "sending", errorMessage: void 0 } : m
965
+ )
966
+ );
967
+ try {
968
+ await fetchFromComms(
969
+ `/channels/${currentActiveChannelId}/messages`,
970
+ {
971
+ method: "POST",
972
+ body: JSON.stringify({
973
+ content: failedMessage.content,
974
+ type: failedMessage.type,
975
+ metadata: failedMessage.metadata
976
+ })
977
+ }
978
+ );
979
+ } catch (error) {
980
+ console.error("[AegisChat] Failed to retry message:", error);
981
+ setMessages(
982
+ (prev) => prev.map(
983
+ (m) => m.tempId === tempId ? {
984
+ ...m,
985
+ status: "failed",
986
+ errorMessage: error instanceof Error ? error.message : "Failed to send"
987
+ } : m
988
+ )
989
+ );
990
+ }
991
+ },
992
+ [messages, fetchFromComms]
993
+ );
788
994
  const deleteFailedMessage = (0, import_react.useCallback)((tempId) => {
789
995
  setMessages((prev) => prev.filter((m) => m.tempId !== tempId));
790
996
  }, []);
997
+ const setup = (0, import_react.useCallback)((options2) => {
998
+ const {
999
+ config: config2,
1000
+ role: role2,
1001
+ clientId: clientId2,
1002
+ initialSession: initialSession2,
1003
+ autoConnect: autoConnect2 = true,
1004
+ onMessage: onMessage2,
1005
+ onTyping: onTyping2,
1006
+ onConnectionChange: onConnectionChange2
1007
+ } = options2;
1008
+ roleRef.current = role2;
1009
+ clientIdRef.current = clientId2;
1010
+ autoConnectRef.current = autoConnect2;
1011
+ onMessageRef.current = onMessage2;
1012
+ onTypingRef.current = onTyping2;
1013
+ onConnectionChangeRef.current = onConnectionChange2;
1014
+ if (initialSession2) {
1015
+ sessionRef.current = initialSession2;
1016
+ if (!config2) {
1017
+ configureApiClient({
1018
+ baseUrl: initialSession2.api_url,
1019
+ getAccessToken: async () => sessionRef.current?.access_token || ""
1020
+ });
1021
+ }
1022
+ setSession(initialSession2);
1023
+ }
1024
+ }, []);
791
1025
  (0, import_react.useEffect)(() => {
792
- if (initialSession && !isConnected && !isConnecting && autoConnect) {
1026
+ if (session && !isConnected && !isConnecting && autoConnectRef.current) {
793
1027
  connectWebSocket();
794
1028
  }
795
- }, [initialSession, session, isConnected, isConnecting, autoConnect, connectWebSocket]);
1029
+ }, [session, isConnected, isConnecting, connectWebSocket]);
796
1030
  (0, import_react.useEffect)(() => {
797
1031
  if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
798
1032
  wsRef.current.onmessage = (event) => {
@@ -800,7 +1034,10 @@ function useChat(options) {
800
1034
  const data = JSON.parse(event.data);
801
1035
  handleWebSocketMessage(data);
802
1036
  } catch (error) {
803
- console.error("[AegisChat] Failed to parse WebSocket message:", error);
1037
+ console.error(
1038
+ "[AegisChat] Failed to parse WebSocket message:",
1039
+ error
1040
+ );
804
1041
  }
805
1042
  };
806
1043
  }
@@ -852,7 +1089,8 @@ function useChat(options) {
852
1089
  createDMWithUser,
853
1090
  retryMessage,
854
1091
  deleteFailedMessage,
855
- markAsRead
1092
+ markAsRead,
1093
+ setup
856
1094
  };
857
1095
  }
858
1096