@thrillee/aegischat 0.1.4 → 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,17 +319,16 @@ 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
- }
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);
317
329
  (0, import_react.useEffect)(() => {
318
- configRef.current = config;
319
- }, [config]);
330
+ activeChannelIdRef.current = activeChannelId;
331
+ }, [activeChannelId]);
320
332
  (0, import_react.useEffect)(() => {
321
333
  activeChannelIdRef.current = activeChannelId;
322
334
  }, [activeChannelId]);
@@ -334,26 +346,29 @@ function useChat(options) {
334
346
  }
335
347
  }
336
348
  }, []);
337
- const fetchFromComms = (0, import_react.useCallback)(async (path, fetchOptions = {}) => {
338
- const currentSession = sessionRef.current;
339
- if (!currentSession) {
340
- throw new Error("Chat session not initialized");
341
- }
342
- const response = await fetch(`${currentSession.api_url}${path}`, {
343
- ...fetchOptions,
344
- headers: {
345
- "Content-Type": "application/json",
346
- Authorization: `Bearer ${currentSession.access_token}`,
347
- ...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");
348
354
  }
349
- });
350
- if (!response.ok) {
351
- const error = await response.json().catch(() => ({}));
352
- throw new Error(error.message || `HTTP ${response.status}`);
353
- }
354
- const data = await response.json();
355
- return data.data || data;
356
- }, []);
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
+ );
357
372
  const clearTimers = (0, import_react.useCallback)(() => {
358
373
  if (reconnectTimeout.current) {
359
374
  clearTimeout(reconnectTimeout.current);
@@ -364,111 +379,141 @@ function useChat(options) {
364
379
  pingInterval.current = null;
365
380
  }
366
381
  }, []);
367
- const handleWebSocketMessage = (0, import_react.useCallback)((data) => {
368
- const currentActiveChannelId = activeChannelIdRef.current;
369
- console.log("[AegisChat] WebSocket message received:", data.type, data);
370
- switch (data.type) {
371
- case "message.new": {
372
- const newMessage = data.payload;
373
- if (newMessage.channel_id === currentActiveChannelId) {
374
- setMessages((prev) => {
375
- const existingIndex = prev.findIndex(
376
- (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
377
420
  );
378
- if (existingIndex !== -1) {
379
- const updated = [...prev];
380
- updated[existingIndex] = { ...newMessage, status: "sent" };
381
- return updated;
382
- }
383
- if (prev.some((m) => m.id === newMessage.id)) return prev;
384
- 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
+ });
385
426
  });
386
- onMessage?.(newMessage);
427
+ break;
387
428
  }
388
- setChannels((prev) => {
389
- const updated = prev.map(
390
- (ch) => ch.id === newMessage.channel_id ? {
391
- ...ch,
392
- last_message: {
393
- id: newMessage.id,
394
- content: newMessage.content,
395
- created_at: newMessage.created_at,
396
- sender: { id: newMessage.sender_id, display_name: "Unknown", status: "online" }
397
- },
398
- unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
399
- } : ch
400
- );
401
- return updated.sort((a, b) => {
402
- const timeA = a.last_message?.created_at || "";
403
- const timeB = b.last_message?.created_at || "";
404
- return timeB.localeCompare(timeA);
405
- });
406
- });
407
- break;
408
- }
409
- case "message.updated": {
410
- const updatedMessage = data.payload;
411
- setMessages((prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m));
412
- break;
413
- }
414
- case "message.deleted": {
415
- const { message_id } = data.payload;
416
- setMessages((prev) => prev.map((m) => m.id === message_id ? { ...m, deleted: true } : m));
417
- break;
418
- }
419
- case "message.delivered":
420
- case "message.read": {
421
- const { message_id, channel_id, status } = data.payload;
422
- if (channel_id === currentActiveChannelId) {
429
+ case "message.updated": {
430
+ const updatedMessage = data.payload;
423
431
  setMessages(
424
- (prev) => prev.map((m) => m.id === message_id ? { ...m, status: status || "delivered" } : m)
432
+ (prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m)
425
433
  );
434
+ break;
426
435
  }
427
- break;
428
- }
429
- case "message.delivered.batch":
430
- case "message.read.batch": {
431
- const { channel_id } = data.payload;
432
- if (channel_id === currentActiveChannelId) {
436
+ case "message.deleted": {
437
+ const { message_id } = data.payload;
433
438
  setMessages(
434
- (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
+ )
435
442
  );
443
+ break;
436
444
  }
437
- break;
438
- }
439
- case "typing.start": {
440
- const { channel_id, user } = data.payload;
441
- const typingUser = {
442
- id: user.id,
443
- displayName: user.display_name,
444
- avatarUrl: user.avatar_url,
445
- startedAt: Date.now()
446
- };
447
- setTypingUsers((prev) => ({
448
- ...prev,
449
- [channel_id]: [...(prev[channel_id] || []).filter((u) => u.id !== user.id), typingUser]
450
- }));
451
- onTyping?.(channel_id, typingUser);
452
- break;
453
- }
454
- case "typing.stop": {
455
- const { channel_id, user_id } = data.payload;
456
- setTypingUsers((prev) => ({
457
- ...prev,
458
- [channel_id]: (prev[channel_id] || []).filter((u) => u.id !== user_id)
459
- }));
460
- 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);
461
507
  }
462
- case "pong":
463
- break;
464
- default:
465
- console.log("[AegisChat] Unhandled message type:", data.type);
466
- }
467
- }, [onMessage, onTyping]);
508
+ },
509
+ []
510
+ );
468
511
  const connectWebSocket = (0, import_react.useCallback)(() => {
469
512
  const currentSession = sessionRef.current;
470
513
  if (!currentSession?.websocket_url || !currentSession?.access_token) {
471
- console.warn("[AegisChat] Cannot connect WebSocket - missing session or token");
514
+ console.warn(
515
+ "[AegisChat] Cannot connect WebSocket - missing session or token"
516
+ );
472
517
  return;
473
518
  }
474
519
  if (wsRef.current?.readyState === WebSocket.OPEN) {
@@ -485,14 +530,19 @@ function useChat(options) {
485
530
  setIsConnected(true);
486
531
  setIsConnecting(false);
487
532
  reconnectAttempts.current = 0;
488
- onConnectionChange?.(true);
533
+ onConnectionChangeRef.current?.(true);
489
534
  pingInterval.current = setInterval(() => {
490
535
  if (ws.readyState === WebSocket.OPEN) {
491
536
  ws.send(JSON.stringify({ type: "ping" }));
492
537
  }
493
538
  }, PING_INTERVAL);
494
539
  if (activeChannelIdRef.current) {
495
- 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
+ );
496
546
  }
497
547
  };
498
548
  ws.onmessage = (event) => {
@@ -508,9 +558,12 @@ function useChat(options) {
508
558
  setIsConnected(false);
509
559
  setIsConnecting(false);
510
560
  clearTimers();
511
- onConnectionChange?.(false);
561
+ onConnectionChangeRef.current?.(false);
512
562
  if (!isManualDisconnect.current && reconnectAttempts.current < MAX_RECONNECT_ATTEMPTS) {
513
- 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
+ );
514
567
  console.log(`[AegisChat] Reconnecting in ${delay}ms...`);
515
568
  reconnectTimeout.current = setTimeout(() => {
516
569
  reconnectAttempts.current++;
@@ -522,14 +575,18 @@ function useChat(options) {
522
575
  console.error("[AegisChat] WebSocket error:", error);
523
576
  };
524
577
  wsRef.current = ws;
525
- }, [clearTimers, handleWebSocketMessage, onConnectionChange]);
578
+ }, [clearTimers, handleWebSocketMessage]);
526
579
  const connect = (0, import_react.useCallback)(async () => {
527
580
  console.log("[AegisChat] connect() called");
528
- const targetSession = sessionRef.current ?? initialSession;
581
+ const targetSession = sessionRef.current;
529
582
  if (!targetSession) {
530
583
  console.log("[AegisChat] No session available, skipping connect");
531
584
  return;
532
585
  }
586
+ if (!autoConnectRef.current) {
587
+ console.log("[AegisChat] autoConnect is false, skipping connect");
588
+ return;
589
+ }
533
590
  connectWebSocket();
534
591
  }, [connectWebSocket]);
535
592
  const disconnect = (0, import_react.useCallback)(() => {
@@ -557,48 +614,72 @@ function useChat(options) {
557
614
  setIsLoadingChannels(false);
558
615
  }
559
616
  }, []);
560
- const selectChannel = (0, import_react.useCallback)(async (channelId) => {
561
- const currentActiveChannelId = activeChannelIdRef.current;
562
- setActiveChannelId(channelId);
563
- setMessages([]);
564
- setHasMoreMessages(true);
565
- oldestMessageId.current = null;
566
- if (wsRef.current?.readyState === WebSocket.OPEN) {
567
- if (currentActiveChannelId) {
568
- 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
+ );
569
639
  }
570
- wsRef.current.send(JSON.stringify({ type: "channel.join", payload: { channel_id: channelId } }));
571
- }
572
- setIsLoadingMessages(true);
573
- try {
574
- const response = await fetchFromComms(`/channels/${channelId}/messages?limit=50`);
575
- setMessages(response.messages || []);
576
- setHasMoreMessages(response.has_more);
577
- if (response.oldest_id) {
578
- 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);
579
661
  }
580
- await markAsRead(channelId);
581
- setChannels((prev) => prev.map((ch) => ch.id === channelId ? { ...ch, unread_count: 0 } : ch));
582
- } catch (error) {
583
- console.error("[AegisChat] Failed to load messages:", error);
584
- setMessages([]);
585
- } finally {
586
- setIsLoadingMessages(false);
587
- }
588
- }, [setActiveChannelId, fetchFromComms]);
589
- const markAsRead = (0, import_react.useCallback)(async (channelId) => {
590
- try {
591
- await fetchFromComms(`/channels/${channelId}/read`, { method: "POST" });
592
- } catch (error) {
593
- console.error("[AegisChat] Failed to mark as read:", error);
594
- }
595
- }, [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
+ );
596
675
  const loadMoreMessages = (0, import_react.useCallback)(async () => {
597
676
  if (!activeChannelId || !hasMoreMessages || isLoadingMessages) return;
598
677
  setIsLoadingMessages(true);
599
678
  try {
600
679
  const params = oldestMessageId.current ? `?before=${oldestMessageId.current}&limit=50` : "?limit=50";
601
- const response = await fetchFromComms(`/channels/${activeChannelId}/messages${params}`);
680
+ const response = await fetchFromComms(
681
+ `/channels/${activeChannelId}/messages${params}`
682
+ );
602
683
  setMessages((prev) => [...response.messages || [], ...prev]);
603
684
  setHasMoreMessages(response.has_more);
604
685
  if (response.oldest_id) {
@@ -610,138 +691,234 @@ function useChat(options) {
610
691
  setIsLoadingMessages(false);
611
692
  }
612
693
  }, [activeChannelId, hasMoreMessages, isLoadingMessages, fetchFromComms]);
613
- const sendMessage = (0, import_react.useCallback)(async (content, msgOptions = {}) => {
614
- const currentActiveChannelId = activeChannelIdRef.current;
615
- const currentSession = sessionRef.current;
616
- if (!currentActiveChannelId || !content.trim() || !currentSession) return;
617
- const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
618
- const trimmedContent = content.trim();
619
- const optimisticMessage = {
620
- id: tempId,
621
- tempId,
622
- channel_id: currentActiveChannelId,
623
- sender_id: currentSession.comms_user_id,
624
- content: trimmedContent,
625
- type: msgOptions.type || "text",
626
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
627
- updated_at: (/* @__PURE__ */ new Date()).toISOString(),
628
- status: "sending",
629
- metadata: msgOptions.metadata || {}
630
- };
631
- setMessages((prev) => [...prev, optimisticMessage]);
632
- const now = (/* @__PURE__ */ new Date()).toISOString();
633
- setChannels((prev) => {
634
- const updated = prev.map(
635
- (ch) => ch.id === currentActiveChannelId ? {
636
- ...ch,
637
- last_message: {
638
- id: tempId,
639
- content: trimmedContent,
640
- created_at: now,
641
- sender: { id: currentSession.comms_user_id, display_name: "You", status: "online" }
642
- }
643
- } : ch
644
- );
645
- return updated.sort((a, b) => {
646
- const timeA = a.last_message?.created_at || "";
647
- const timeB = b.last_message?.created_at || "";
648
- return timeB.localeCompare(timeA);
649
- });
650
- });
651
- try {
652
- await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
653
- method: "POST",
654
- body: JSON.stringify({ content: trimmedContent, type: msgOptions.type || "text", parent_id: msgOptions.parent_id, metadata: msgOptions.metadata })
655
- });
656
- } catch (error) {
657
- console.error("[AegisChat] Failed to send message:", error);
658
- setMessages(
659
- (prev) => prev.map(
660
- (m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m
661
- )
662
- );
663
- throw error;
664
- }
665
- }, [fetchFromComms]);
666
- const uploadFile = (0, import_react.useCallback)(async (file) => {
667
- const currentSession = sessionRef.current;
668
- if (!currentSession) return null;
669
- const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
670
- setUploadProgress((prev) => [...prev, { fileId, fileName: file.name, progress: 0, status: "pending" }]);
671
- try {
672
- setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p));
673
- const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
674
- method: "POST",
675
- body: JSON.stringify({ file_name: file.name, file_type: file.type || "application/octet-stream", file_size: file.size })
676
- });
677
- setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p));
678
- const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
679
- method: "PUT",
680
- body: file,
681
- headers: { "Content-Type": file.type || "application/octet-stream" }
682
- });
683
- if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.statusText}`);
684
- setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p));
685
- const confirmResponse = await fetchFromComms("/files", {
686
- method: "POST",
687
- 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
+ });
688
736
  });
689
- setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "complete", progress: 100 } : p));
690
- setTimeout(() => setUploadProgress((prev) => prev.filter((p) => p.fileId !== uploadUrlResponse.file_id)), 2e3);
691
- return confirmResponse.file;
692
- } catch (error) {
693
- console.error("[AegisChat] Failed to upload file:", error);
694
- setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "error", error: error instanceof Error ? error.message : "Upload failed" } : p));
695
- setTimeout(() => setUploadProgress((prev) => prev.filter((p) => p.fileId !== fileId)), 5e3);
696
- return null;
697
- }
698
- }, [fetchFromComms]);
699
- const sendMessageWithFiles = (0, import_react.useCallback)(async (content, files, msgOptions = {}) => {
700
- const currentActiveChannelId = activeChannelIdRef.current;
701
- const currentSession = sessionRef.current;
702
- if (!currentActiveChannelId || !content.trim() && files.length === 0 || !currentSession) return;
703
- const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
704
- const trimmedContent = content.trim();
705
- const optimisticMessage = {
706
- id: tempId,
707
- tempId,
708
- channel_id: currentActiveChannelId,
709
- sender_id: currentSession.comms_user_id,
710
- content: trimmedContent || `Uploading ${files.length} file(s)...`,
711
- type: "file",
712
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
713
- updated_at: (/* @__PURE__ */ new Date()).toISOString(),
714
- status: "sending",
715
- metadata: { ...msgOptions.metadata, files: files.map((f) => ({ id: `temp-${f.name}`, filename: f.name, mime_type: f.type, size: f.size, url: "" })) }
716
- };
717
- setMessages((prev) => [...prev, optimisticMessage]);
718
- try {
719
- const uploadedFiles = [];
720
- for (const file of files) {
721
- const attachment = await uploadFile(file);
722
- 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;
723
762
  }
724
- const messageType = uploadedFiles.length > 0 && !trimmedContent ? "file" : "text";
725
- await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
726
- method: "POST",
727
- body: JSON.stringify({
728
- content: trimmedContent || (uploadedFiles.length > 0 ? `Shared ${uploadedFiles.length} file(s)` : ""),
729
- type: msgOptions.type || messageType,
730
- parent_id: msgOptions.parent_id,
731
- metadata: { ...msgOptions.metadata, files: uploadedFiles },
732
- file_ids: uploadedFiles.map((f) => f.id)
733
- })
734
- });
735
- } catch (error) {
736
- console.error("[AegisChat] Failed to send message with files:", error);
737
- setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m));
738
- throw error;
739
- }
740
- }, [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
+ );
741
913
  const stopTyping = (0, import_react.useCallback)(() => {
742
914
  const currentActiveChannelId = activeChannelIdRef.current;
743
915
  if (!currentActiveChannelId || !wsRef.current) return;
744
- 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
+ );
745
922
  if (typingTimeout.current) {
746
923
  clearTimeout(typingTimeout.current);
747
924
  typingTimeout.current = null;
@@ -750,46 +927,106 @@ function useChat(options) {
750
927
  const startTyping = (0, import_react.useCallback)(() => {
751
928
  const currentActiveChannelId = activeChannelIdRef.current;
752
929
  if (!currentActiveChannelId || !wsRef.current) return;
753
- 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
+ );
754
936
  if (typingTimeout.current) clearTimeout(typingTimeout.current);
755
937
  typingTimeout.current = setTimeout(stopTyping, TYPING_TIMEOUT);
756
938
  }, [stopTyping]);
757
- const createDMWithUser = (0, import_react.useCallback)(async (userId) => {
758
- try {
759
- const channel = await fetchFromComms("/channels/dm", {
760
- method: "POST",
761
- body: JSON.stringify({ user_id: userId })
762
- });
763
- await refreshChannels();
764
- return channel.id;
765
- } catch (error) {
766
- console.error("[AegisChat] Failed to create DM:", error);
767
- return null;
768
- }
769
- }, [fetchFromComms, refreshChannels]);
770
- const retryMessage = (0, import_react.useCallback)(async (tempId) => {
771
- const failedMessage = messages.find((m) => m.tempId === tempId && m.status === "failed");
772
- const currentActiveChannelId = activeChannelIdRef.current;
773
- if (!failedMessage || !currentActiveChannelId) return;
774
- setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "sending", errorMessage: void 0 } : m));
775
- try {
776
- await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
777
- method: "POST",
778
- body: JSON.stringify({ content: failedMessage.content, type: failedMessage.type, metadata: failedMessage.metadata })
779
- });
780
- } catch (error) {
781
- console.error("[AegisChat] Failed to retry message:", error);
782
- setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m));
783
- }
784
- }, [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
+ );
785
994
  const deleteFailedMessage = (0, import_react.useCallback)((tempId) => {
786
995
  setMessages((prev) => prev.filter((m) => m.tempId !== tempId));
787
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
+ }, []);
788
1025
  (0, import_react.useEffect)(() => {
789
- if (session && !isConnected && !isConnecting && autoConnect) {
1026
+ if (session && !isConnected && !isConnecting && autoConnectRef.current) {
790
1027
  connectWebSocket();
791
1028
  }
792
- }, [session, isConnected, isConnecting, autoConnect, connectWebSocket]);
1029
+ }, [session, isConnected, isConnecting, connectWebSocket]);
793
1030
  (0, import_react.useEffect)(() => {
794
1031
  if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
795
1032
  wsRef.current.onmessage = (event) => {
@@ -797,7 +1034,10 @@ function useChat(options) {
797
1034
  const data = JSON.parse(event.data);
798
1035
  handleWebSocketMessage(data);
799
1036
  } catch (error) {
800
- console.error("[AegisChat] Failed to parse WebSocket message:", error);
1037
+ console.error(
1038
+ "[AegisChat] Failed to parse WebSocket message:",
1039
+ error
1040
+ );
801
1041
  }
802
1042
  };
803
1043
  }
@@ -849,7 +1089,8 @@ function useChat(options) {
849
1089
  createDMWithUser,
850
1090
  retryMessage,
851
1091
  deleteFailedMessage,
852
- markAsRead
1092
+ markAsRead,
1093
+ setup
853
1094
  };
854
1095
  }
855
1096