@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.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,23 +279,19 @@ 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
- }
277
- useEffect(() => {
278
- configRef.current = config;
279
- }, [config]);
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);
280
289
  useEffect(() => {
281
290
  activeChannelIdRef.current = activeChannelId;
282
291
  }, [activeChannelId]);
283
292
  useEffect(() => {
284
- sessionRef.current = initialSession ?? null;
285
- }, [initialSession]);
293
+ activeChannelIdRef.current = activeChannelId;
294
+ }, [activeChannelId]);
286
295
  const getActiveChannelId = useCallback(() => {
287
296
  if (typeof window === "undefined") return null;
288
297
  return sessionStorage.getItem(SESSION_STORAGE_KEY);
@@ -297,26 +306,29 @@ function useChat(options) {
297
306
  }
298
307
  }
299
308
  }, []);
300
- const fetchFromComms = useCallback(async (path, fetchOptions = {}) => {
301
- const currentSession = sessionRef.current;
302
- if (!currentSession) {
303
- throw new Error("Chat session not initialized");
304
- }
305
- const response = await fetch(`${currentSession.api_url}${path}`, {
306
- ...fetchOptions,
307
- headers: {
308
- "Content-Type": "application/json",
309
- Authorization: `Bearer ${currentSession.access_token}`,
310
- ...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");
311
314
  }
312
- });
313
- if (!response.ok) {
314
- const error = await response.json().catch(() => ({}));
315
- throw new Error(error.message || `HTTP ${response.status}`);
316
- }
317
- const data = await response.json();
318
- return data.data || data;
319
- }, []);
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
+ );
320
332
  const clearTimers = useCallback(() => {
321
333
  if (reconnectTimeout.current) {
322
334
  clearTimeout(reconnectTimeout.current);
@@ -327,111 +339,141 @@ function useChat(options) {
327
339
  pingInterval.current = null;
328
340
  }
329
341
  }, []);
330
- const handleWebSocketMessage = useCallback((data) => {
331
- const currentActiveChannelId = activeChannelIdRef.current;
332
- console.log("[AegisChat] WebSocket message received:", data.type, data);
333
- switch (data.type) {
334
- case "message.new": {
335
- const newMessage = data.payload;
336
- if (newMessage.channel_id === currentActiveChannelId) {
337
- setMessages((prev) => {
338
- const existingIndex = prev.findIndex(
339
- (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
340
380
  );
341
- if (existingIndex !== -1) {
342
- const updated = [...prev];
343
- updated[existingIndex] = { ...newMessage, status: "sent" };
344
- return updated;
345
- }
346
- if (prev.some((m) => m.id === newMessage.id)) return prev;
347
- 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
+ });
348
386
  });
349
- onMessage?.(newMessage);
387
+ break;
350
388
  }
351
- setChannels((prev) => {
352
- const updated = prev.map(
353
- (ch) => ch.id === newMessage.channel_id ? {
354
- ...ch,
355
- last_message: {
356
- id: newMessage.id,
357
- content: newMessage.content,
358
- created_at: newMessage.created_at,
359
- sender: { id: newMessage.sender_id, display_name: "Unknown", status: "online" }
360
- },
361
- unread_count: ch.id === currentActiveChannelId ? 0 : ch.unread_count + 1
362
- } : ch
363
- );
364
- return updated.sort((a, b) => {
365
- const timeA = a.last_message?.created_at || "";
366
- const timeB = b.last_message?.created_at || "";
367
- return timeB.localeCompare(timeA);
368
- });
369
- });
370
- break;
371
- }
372
- case "message.updated": {
373
- const updatedMessage = data.payload;
374
- setMessages((prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m));
375
- break;
376
- }
377
- case "message.deleted": {
378
- const { message_id } = data.payload;
379
- setMessages((prev) => prev.map((m) => m.id === message_id ? { ...m, deleted: true } : m));
380
- break;
381
- }
382
- case "message.delivered":
383
- case "message.read": {
384
- const { message_id, channel_id, status } = data.payload;
385
- if (channel_id === currentActiveChannelId) {
389
+ case "message.updated": {
390
+ const updatedMessage = data.payload;
386
391
  setMessages(
387
- (prev) => prev.map((m) => m.id === message_id ? { ...m, status: status || "delivered" } : m)
392
+ (prev) => prev.map((m) => m.id === updatedMessage.id ? updatedMessage : m)
388
393
  );
394
+ break;
389
395
  }
390
- break;
391
- }
392
- case "message.delivered.batch":
393
- case "message.read.batch": {
394
- const { channel_id } = data.payload;
395
- if (channel_id === currentActiveChannelId) {
396
+ case "message.deleted": {
397
+ const { message_id } = data.payload;
396
398
  setMessages(
397
- (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
+ )
398
402
  );
403
+ break;
399
404
  }
400
- break;
401
- }
402
- case "typing.start": {
403
- const { channel_id, user } = data.payload;
404
- const typingUser = {
405
- id: user.id,
406
- displayName: user.display_name,
407
- avatarUrl: user.avatar_url,
408
- startedAt: Date.now()
409
- };
410
- setTypingUsers((prev) => ({
411
- ...prev,
412
- [channel_id]: [...(prev[channel_id] || []).filter((u) => u.id !== user.id), typingUser]
413
- }));
414
- onTyping?.(channel_id, typingUser);
415
- break;
416
- }
417
- case "typing.stop": {
418
- const { channel_id, user_id } = data.payload;
419
- setTypingUsers((prev) => ({
420
- ...prev,
421
- [channel_id]: (prev[channel_id] || []).filter((u) => u.id !== user_id)
422
- }));
423
- 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);
424
467
  }
425
- case "pong":
426
- break;
427
- default:
428
- console.log("[AegisChat] Unhandled message type:", data.type);
429
- }
430
- }, [onMessage, onTyping]);
468
+ },
469
+ []
470
+ );
431
471
  const connectWebSocket = useCallback(() => {
432
472
  const currentSession = sessionRef.current;
433
473
  if (!currentSession?.websocket_url || !currentSession?.access_token) {
434
- console.warn("[AegisChat] Cannot connect WebSocket - missing session or token");
474
+ console.warn(
475
+ "[AegisChat] Cannot connect WebSocket - missing session or token"
476
+ );
435
477
  return;
436
478
  }
437
479
  if (wsRef.current?.readyState === WebSocket.OPEN) {
@@ -448,14 +490,19 @@ function useChat(options) {
448
490
  setIsConnected(true);
449
491
  setIsConnecting(false);
450
492
  reconnectAttempts.current = 0;
451
- onConnectionChange?.(true);
493
+ onConnectionChangeRef.current?.(true);
452
494
  pingInterval.current = setInterval(() => {
453
495
  if (ws.readyState === WebSocket.OPEN) {
454
496
  ws.send(JSON.stringify({ type: "ping" }));
455
497
  }
456
498
  }, PING_INTERVAL);
457
499
  if (activeChannelIdRef.current) {
458
- 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
+ );
459
506
  }
460
507
  };
461
508
  ws.onmessage = (event) => {
@@ -471,9 +518,12 @@ function useChat(options) {
471
518
  setIsConnected(false);
472
519
  setIsConnecting(false);
473
520
  clearTimers();
474
- onConnectionChange?.(false);
521
+ onConnectionChangeRef.current?.(false);
475
522
  if (!isManualDisconnect.current && reconnectAttempts.current < MAX_RECONNECT_ATTEMPTS) {
476
- 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
+ );
477
527
  console.log(`[AegisChat] Reconnecting in ${delay}ms...`);
478
528
  reconnectTimeout.current = setTimeout(() => {
479
529
  reconnectAttempts.current++;
@@ -485,14 +535,18 @@ function useChat(options) {
485
535
  console.error("[AegisChat] WebSocket error:", error);
486
536
  };
487
537
  wsRef.current = ws;
488
- }, [clearTimers, handleWebSocketMessage, onConnectionChange]);
538
+ }, [clearTimers, handleWebSocketMessage]);
489
539
  const connect = useCallback(async () => {
490
540
  console.log("[AegisChat] connect() called");
491
- const targetSession = sessionRef.current ?? initialSession;
541
+ const targetSession = sessionRef.current;
492
542
  if (!targetSession) {
493
543
  console.log("[AegisChat] No session available, skipping connect");
494
544
  return;
495
545
  }
546
+ if (!autoConnectRef.current) {
547
+ console.log("[AegisChat] autoConnect is false, skipping connect");
548
+ return;
549
+ }
496
550
  connectWebSocket();
497
551
  }, [connectWebSocket]);
498
552
  const disconnect = useCallback(() => {
@@ -520,48 +574,72 @@ function useChat(options) {
520
574
  setIsLoadingChannels(false);
521
575
  }
522
576
  }, []);
523
- const selectChannel = useCallback(async (channelId) => {
524
- const currentActiveChannelId = activeChannelIdRef.current;
525
- setActiveChannelId(channelId);
526
- setMessages([]);
527
- setHasMoreMessages(true);
528
- oldestMessageId.current = null;
529
- if (wsRef.current?.readyState === WebSocket.OPEN) {
530
- if (currentActiveChannelId) {
531
- 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
+ );
532
599
  }
533
- wsRef.current.send(JSON.stringify({ type: "channel.join", payload: { channel_id: channelId } }));
534
- }
535
- setIsLoadingMessages(true);
536
- try {
537
- const response = await fetchFromComms(`/channels/${channelId}/messages?limit=50`);
538
- setMessages(response.messages || []);
539
- setHasMoreMessages(response.has_more);
540
- if (response.oldest_id) {
541
- 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);
542
621
  }
543
- await markAsRead(channelId);
544
- setChannels((prev) => prev.map((ch) => ch.id === channelId ? { ...ch, unread_count: 0 } : ch));
545
- } catch (error) {
546
- console.error("[AegisChat] Failed to load messages:", error);
547
- setMessages([]);
548
- } finally {
549
- setIsLoadingMessages(false);
550
- }
551
- }, [setActiveChannelId, fetchFromComms]);
552
- const markAsRead = useCallback(async (channelId) => {
553
- try {
554
- await fetchFromComms(`/channels/${channelId}/read`, { method: "POST" });
555
- } catch (error) {
556
- console.error("[AegisChat] Failed to mark as read:", error);
557
- }
558
- }, [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
+ );
559
635
  const loadMoreMessages = useCallback(async () => {
560
636
  if (!activeChannelId || !hasMoreMessages || isLoadingMessages) return;
561
637
  setIsLoadingMessages(true);
562
638
  try {
563
639
  const params = oldestMessageId.current ? `?before=${oldestMessageId.current}&limit=50` : "?limit=50";
564
- const response = await fetchFromComms(`/channels/${activeChannelId}/messages${params}`);
640
+ const response = await fetchFromComms(
641
+ `/channels/${activeChannelId}/messages${params}`
642
+ );
565
643
  setMessages((prev) => [...response.messages || [], ...prev]);
566
644
  setHasMoreMessages(response.has_more);
567
645
  if (response.oldest_id) {
@@ -573,138 +651,234 @@ function useChat(options) {
573
651
  setIsLoadingMessages(false);
574
652
  }
575
653
  }, [activeChannelId, hasMoreMessages, isLoadingMessages, fetchFromComms]);
576
- const sendMessage = useCallback(async (content, msgOptions = {}) => {
577
- const currentActiveChannelId = activeChannelIdRef.current;
578
- const currentSession = sessionRef.current;
579
- if (!currentActiveChannelId || !content.trim() || !currentSession) return;
580
- const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
581
- const trimmedContent = content.trim();
582
- const optimisticMessage = {
583
- id: tempId,
584
- tempId,
585
- channel_id: currentActiveChannelId,
586
- sender_id: currentSession.comms_user_id,
587
- content: trimmedContent,
588
- type: msgOptions.type || "text",
589
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
590
- updated_at: (/* @__PURE__ */ new Date()).toISOString(),
591
- status: "sending",
592
- metadata: msgOptions.metadata || {}
593
- };
594
- setMessages((prev) => [...prev, optimisticMessage]);
595
- const now = (/* @__PURE__ */ new Date()).toISOString();
596
- setChannels((prev) => {
597
- const updated = prev.map(
598
- (ch) => ch.id === currentActiveChannelId ? {
599
- ...ch,
600
- last_message: {
601
- id: tempId,
602
- content: trimmedContent,
603
- created_at: now,
604
- sender: { id: currentSession.comms_user_id, display_name: "You", status: "online" }
605
- }
606
- } : ch
607
- );
608
- return updated.sort((a, b) => {
609
- const timeA = a.last_message?.created_at || "";
610
- const timeB = b.last_message?.created_at || "";
611
- return timeB.localeCompare(timeA);
612
- });
613
- });
614
- try {
615
- await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
616
- method: "POST",
617
- body: JSON.stringify({ content: trimmedContent, type: msgOptions.type || "text", parent_id: msgOptions.parent_id, metadata: msgOptions.metadata })
618
- });
619
- } catch (error) {
620
- console.error("[AegisChat] Failed to send message:", error);
621
- setMessages(
622
- (prev) => prev.map(
623
- (m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m
624
- )
625
- );
626
- throw error;
627
- }
628
- }, [fetchFromComms]);
629
- const uploadFile = useCallback(async (file) => {
630
- const currentSession = sessionRef.current;
631
- if (!currentSession) return null;
632
- const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
633
- setUploadProgress((prev) => [...prev, { fileId, fileName: file.name, progress: 0, status: "pending" }]);
634
- try {
635
- setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "uploading", progress: 10 } : p));
636
- const uploadUrlResponse = await fetchFromComms("/files/upload-url", {
637
- method: "POST",
638
- body: JSON.stringify({ file_name: file.name, file_type: file.type || "application/octet-stream", file_size: file.size })
639
- });
640
- setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, fileId: uploadUrlResponse.file_id, progress: 30 } : p));
641
- const uploadResponse = await fetch(uploadUrlResponse.upload_url, {
642
- method: "PUT",
643
- body: file,
644
- headers: { "Content-Type": file.type || "application/octet-stream" }
645
- });
646
- if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.statusText}`);
647
- setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "confirming", progress: 70 } : p));
648
- const confirmResponse = await fetchFromComms("/files", {
649
- method: "POST",
650
- 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
+ });
651
696
  });
652
- setUploadProgress((prev) => prev.map((p) => p.fileId === uploadUrlResponse.file_id ? { ...p, status: "complete", progress: 100 } : p));
653
- setTimeout(() => setUploadProgress((prev) => prev.filter((p) => p.fileId !== uploadUrlResponse.file_id)), 2e3);
654
- return confirmResponse.file;
655
- } catch (error) {
656
- console.error("[AegisChat] Failed to upload file:", error);
657
- setUploadProgress((prev) => prev.map((p) => p.fileId === fileId ? { ...p, status: "error", error: error instanceof Error ? error.message : "Upload failed" } : p));
658
- setTimeout(() => setUploadProgress((prev) => prev.filter((p) => p.fileId !== fileId)), 5e3);
659
- return null;
660
- }
661
- }, [fetchFromComms]);
662
- const sendMessageWithFiles = useCallback(async (content, files, msgOptions = {}) => {
663
- const currentActiveChannelId = activeChannelIdRef.current;
664
- const currentSession = sessionRef.current;
665
- if (!currentActiveChannelId || !content.trim() && files.length === 0 || !currentSession) return;
666
- const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
667
- const trimmedContent = content.trim();
668
- const optimisticMessage = {
669
- id: tempId,
670
- tempId,
671
- channel_id: currentActiveChannelId,
672
- sender_id: currentSession.comms_user_id,
673
- content: trimmedContent || `Uploading ${files.length} file(s)...`,
674
- type: "file",
675
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
676
- updated_at: (/* @__PURE__ */ new Date()).toISOString(),
677
- status: "sending",
678
- metadata: { ...msgOptions.metadata, files: files.map((f) => ({ id: `temp-${f.name}`, filename: f.name, mime_type: f.type, size: f.size, url: "" })) }
679
- };
680
- setMessages((prev) => [...prev, optimisticMessage]);
681
- try {
682
- const uploadedFiles = [];
683
- for (const file of files) {
684
- const attachment = await uploadFile(file);
685
- 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;
686
722
  }
687
- const messageType = uploadedFiles.length > 0 && !trimmedContent ? "file" : "text";
688
- await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
689
- method: "POST",
690
- body: JSON.stringify({
691
- content: trimmedContent || (uploadedFiles.length > 0 ? `Shared ${uploadedFiles.length} file(s)` : ""),
692
- type: msgOptions.type || messageType,
693
- parent_id: msgOptions.parent_id,
694
- metadata: { ...msgOptions.metadata, files: uploadedFiles },
695
- file_ids: uploadedFiles.map((f) => f.id)
696
- })
697
- });
698
- } catch (error) {
699
- console.error("[AegisChat] Failed to send message with files:", error);
700
- setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m));
701
- throw error;
702
- }
703
- }, [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
+ );
704
873
  const stopTyping = useCallback(() => {
705
874
  const currentActiveChannelId = activeChannelIdRef.current;
706
875
  if (!currentActiveChannelId || !wsRef.current) return;
707
- 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
+ );
708
882
  if (typingTimeout.current) {
709
883
  clearTimeout(typingTimeout.current);
710
884
  typingTimeout.current = null;
@@ -713,46 +887,106 @@ function useChat(options) {
713
887
  const startTyping = useCallback(() => {
714
888
  const currentActiveChannelId = activeChannelIdRef.current;
715
889
  if (!currentActiveChannelId || !wsRef.current) return;
716
- 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
+ );
717
896
  if (typingTimeout.current) clearTimeout(typingTimeout.current);
718
897
  typingTimeout.current = setTimeout(stopTyping, TYPING_TIMEOUT);
719
898
  }, [stopTyping]);
720
- const createDMWithUser = useCallback(async (userId) => {
721
- try {
722
- const channel = await fetchFromComms("/channels/dm", {
723
- method: "POST",
724
- body: JSON.stringify({ user_id: userId })
725
- });
726
- await refreshChannels();
727
- return channel.id;
728
- } catch (error) {
729
- console.error("[AegisChat] Failed to create DM:", error);
730
- return null;
731
- }
732
- }, [fetchFromComms, refreshChannels]);
733
- const retryMessage = useCallback(async (tempId) => {
734
- const failedMessage = messages.find((m) => m.tempId === tempId && m.status === "failed");
735
- const currentActiveChannelId = activeChannelIdRef.current;
736
- if (!failedMessage || !currentActiveChannelId) return;
737
- setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "sending", errorMessage: void 0 } : m));
738
- try {
739
- await fetchFromComms(`/channels/${currentActiveChannelId}/messages`, {
740
- method: "POST",
741
- body: JSON.stringify({ content: failedMessage.content, type: failedMessage.type, metadata: failedMessage.metadata })
742
- });
743
- } catch (error) {
744
- console.error("[AegisChat] Failed to retry message:", error);
745
- setMessages((prev) => prev.map((m) => m.tempId === tempId ? { ...m, status: "failed", errorMessage: error instanceof Error ? error.message : "Failed to send" } : m));
746
- }
747
- }, [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
+ );
748
954
  const deleteFailedMessage = useCallback((tempId) => {
749
955
  setMessages((prev) => prev.filter((m) => m.tempId !== tempId));
750
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
+ }, []);
751
985
  useEffect(() => {
752
- if (initialSession && !isConnected && !isConnecting && autoConnect) {
986
+ if (session && !isConnected && !isConnecting && autoConnectRef.current) {
753
987
  connectWebSocket();
754
988
  }
755
- }, [initialSession, session, isConnected, isConnecting, autoConnect, connectWebSocket]);
989
+ }, [session, isConnected, isConnecting, connectWebSocket]);
756
990
  useEffect(() => {
757
991
  if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
758
992
  wsRef.current.onmessage = (event) => {
@@ -760,7 +994,10 @@ function useChat(options) {
760
994
  const data = JSON.parse(event.data);
761
995
  handleWebSocketMessage(data);
762
996
  } catch (error) {
763
- console.error("[AegisChat] Failed to parse WebSocket message:", error);
997
+ console.error(
998
+ "[AegisChat] Failed to parse WebSocket message:",
999
+ error
1000
+ );
764
1001
  }
765
1002
  };
766
1003
  }
@@ -812,7 +1049,8 @@ function useChat(options) {
812
1049
  createDMWithUser,
813
1050
  retryMessage,
814
1051
  deleteFailedMessage,
815
- markAsRead
1052
+ markAsRead,
1053
+ setup
816
1054
  };
817
1055
  }
818
1056