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