@vezlo/assistant-server 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +9 -5
  2. package/database-schema.sql +9 -0
  3. package/dist/knexfile.js +5 -6
  4. package/dist/knexfile.js.map +1 -1
  5. package/dist/src/bootstrap/initializeServices.d.ts.map +1 -1
  6. package/dist/src/bootstrap/initializeServices.js +12 -1
  7. package/dist/src/bootstrap/initializeServices.js.map +1 -1
  8. package/dist/src/config/global.d.ts.map +1 -1
  9. package/dist/src/config/global.js +10 -1
  10. package/dist/src/config/global.js.map +1 -1
  11. package/dist/src/controllers/AuthController.d.ts +4 -1
  12. package/dist/src/controllers/AuthController.d.ts.map +1 -1
  13. package/dist/src/controllers/AuthController.js +8 -3
  14. package/dist/src/controllers/AuthController.js.map +1 -1
  15. package/dist/src/controllers/ChatController.d.ts +8 -1
  16. package/dist/src/controllers/ChatController.d.ts.map +1 -1
  17. package/dist/src/controllers/ChatController.js +476 -15
  18. package/dist/src/controllers/ChatController.js.map +1 -1
  19. package/dist/src/migrations/005_add_conversation_handoff_columns.d.ts +4 -0
  20. package/dist/src/migrations/005_add_conversation_handoff_columns.d.ts.map +1 -0
  21. package/dist/src/migrations/005_add_conversation_handoff_columns.js +42 -0
  22. package/dist/src/migrations/005_add_conversation_handoff_columns.js.map +1 -0
  23. package/dist/src/schemas/ConversationSchemas.d.ts +125 -33
  24. package/dist/src/schemas/ConversationSchemas.d.ts.map +1 -1
  25. package/dist/src/schemas/ConversationSchemas.js +47 -18
  26. package/dist/src/schemas/ConversationSchemas.js.map +1 -1
  27. package/dist/src/schemas/MessageSchemas.d.ts +9 -0
  28. package/dist/src/schemas/MessageSchemas.d.ts.map +1 -1
  29. package/dist/src/schemas/MessageSchemas.js +3 -1
  30. package/dist/src/schemas/MessageSchemas.js.map +1 -1
  31. package/dist/src/schemas/index.d.ts +134 -33
  32. package/dist/src/schemas/index.d.ts.map +1 -1
  33. package/dist/src/server.js +222 -27
  34. package/dist/src/server.js.map +1 -1
  35. package/dist/src/services/ChatManager.d.ts.map +1 -1
  36. package/dist/src/services/ChatManager.js +26 -29
  37. package/dist/src/services/ChatManager.js.map +1 -1
  38. package/dist/src/services/IntentService.d.ts.map +1 -1
  39. package/dist/src/services/IntentService.js +6 -3
  40. package/dist/src/services/IntentService.js.map +1 -1
  41. package/dist/src/services/RealtimePublisher.d.ts +6 -0
  42. package/dist/src/services/RealtimePublisher.d.ts.map +1 -0
  43. package/dist/src/services/RealtimePublisher.js +43 -0
  44. package/dist/src/services/RealtimePublisher.js.map +1 -0
  45. package/dist/src/storage/ConversationRepository.d.ts +2 -2
  46. package/dist/src/storage/ConversationRepository.d.ts.map +1 -1
  47. package/dist/src/storage/ConversationRepository.js +36 -19
  48. package/dist/src/storage/ConversationRepository.js.map +1 -1
  49. package/dist/src/storage/MessageRepository.d.ts +4 -1
  50. package/dist/src/storage/MessageRepository.d.ts.map +1 -1
  51. package/dist/src/storage/MessageRepository.js +10 -4
  52. package/dist/src/storage/MessageRepository.js.map +1 -1
  53. package/dist/src/storage/SupabaseStorage.d.ts +5 -3
  54. package/dist/src/storage/SupabaseStorage.d.ts.map +1 -1
  55. package/dist/src/storage/SupabaseStorage.js +42 -11
  56. package/dist/src/storage/SupabaseStorage.js.map +1 -1
  57. package/dist/src/storage/UnifiedStorage.d.ts +5 -3
  58. package/dist/src/storage/UnifiedStorage.d.ts.map +1 -1
  59. package/dist/src/storage/UnifiedStorage.js +4 -4
  60. package/dist/src/storage/UnifiedStorage.js.map +1 -1
  61. package/dist/src/types/index.d.ts +24 -4
  62. package/dist/src/types/index.d.ts.map +1 -1
  63. package/knexfile.ts +4 -4
  64. package/package.json +2 -2
@@ -13,6 +13,7 @@ class ChatController {
13
13
  const { historyLength } = options;
14
14
  this.chatHistoryLength = typeof historyLength === 'number' && historyLength > 0 ? historyLength : 2;
15
15
  this.intentService = options.intentService;
16
+ this.realtimePublisher = options.realtimePublisher;
16
17
  }
17
18
  // Create a new conversation
18
19
  async createConversation(req, res) {
@@ -96,15 +97,34 @@ class ChatController {
96
97
  }
97
98
  // Generate a unique thread ID for the conversation
98
99
  const threadId = `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
100
+ const now = new Date();
99
101
  const conversation = await this.storage.saveConversation({
100
102
  threadId,
101
103
  userId,
102
104
  organizationId: companyId,
103
105
  title: title || 'New Conversation',
104
106
  messageCount: 0,
105
- createdAt: new Date(),
106
- updatedAt: new Date()
107
+ createdAt: now,
108
+ updatedAt: now,
109
+ lastMessageAt: now
107
110
  });
111
+ // Publish realtime update for new conversation
112
+ if (this.realtimePublisher && companyUuid) {
113
+ try {
114
+ await this.realtimePublisher.publish(`company:${companyUuid}:conversations`, 'conversation:created', {
115
+ conversation: {
116
+ uuid: conversation.id,
117
+ status: 'open',
118
+ message_count: conversation.messageCount,
119
+ last_message_at: conversation.lastMessageAt?.toISOString() || now.toISOString(),
120
+ created_at: conversation.createdAt.toISOString()
121
+ }
122
+ });
123
+ }
124
+ catch (error) {
125
+ logger_1.default.error('[ChatController] Failed to publish conversation:created event:', error);
126
+ }
127
+ }
108
128
  res.json({
109
129
  uuid: conversation.id,
110
130
  title: conversation.title,
@@ -146,9 +166,44 @@ class ChatController {
146
166
  createdAt: new Date()
147
167
  });
148
168
  // Update conversation message count
169
+ const timestamp = new Date();
170
+ const newMessageCount = conversation.messageCount + 1;
149
171
  await this.storage.updateConversation(uuid, {
150
- messageCount: conversation.messageCount + 1
172
+ messageCount: newMessageCount,
173
+ lastMessageAt: timestamp
151
174
  });
175
+ // Publish realtime update
176
+ if (this.realtimePublisher && conversation.organizationId) {
177
+ try {
178
+ logger_1.default.info(`[ChatController] Fetching company UUID for organizationId: ${conversation.organizationId}`);
179
+ const { data: company } = await this.supabase
180
+ .from('vezlo_companies')
181
+ .select('uuid')
182
+ .eq('id', conversation.organizationId)
183
+ .single();
184
+ if (company?.uuid) {
185
+ logger_1.default.info(`[ChatController] Publishing user message update for company: ${company.uuid}`);
186
+ await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
187
+ conversation_uuid: uuid,
188
+ message: {
189
+ uuid: userMessage.id,
190
+ content: userMessage.content,
191
+ type: userMessage.role,
192
+ author_id: null,
193
+ created_at: userMessage.createdAt.toISOString()
194
+ },
195
+ conversation_update: {
196
+ message_count: newMessageCount,
197
+ last_message_at: timestamp.toISOString(),
198
+ status: conversation.joinedAt ? 'in_progress' : 'open'
199
+ }
200
+ });
201
+ }
202
+ }
203
+ catch (error) {
204
+ logger_1.default.error('[ChatController] Failed to publish realtime update:', error);
205
+ }
206
+ }
152
207
  res.json({
153
208
  uuid: userMessage.id,
154
209
  conversation_uuid: uuid,
@@ -178,9 +233,19 @@ class ChatController {
178
233
  const conversationId = userMessage.conversationId;
179
234
  const userMessageContent = userMessage.content;
180
235
  // Get conversation context (recent messages)
181
- const messages = await this.chatManager.getRecentMessages(conversationId, this.chatHistoryLength);
236
+ // Exclude the current user message to avoid duplication (it's added separately as the query)
237
+ const allMessages = await this.chatManager.getRecentMessages(conversationId, this.chatHistoryLength + 1);
238
+ const messages = allMessages.filter(msg => msg.id !== uuid).slice(-this.chatHistoryLength);
182
239
  logger_1.default.info(`📜 Retrieved ${messages.length} message(s) from conversation history (limit: ${this.chatHistoryLength})`);
183
240
  const conversation = await this.storage.getConversation(conversationId);
241
+ // Check if conversation has been joined by an agent
242
+ if (conversation?.joinedAt) {
243
+ res.status(400).json({
244
+ error: 'Conversation is being handled by an agent',
245
+ message: 'AI responses are disabled when an agent has joined the conversation'
246
+ });
247
+ return;
248
+ }
184
249
  // Run intent classification to decide handling strategy
185
250
  const intentResult = await this.classifyIntent(userMessageContent, messages);
186
251
  const handled = await this.handleIntentResult(intentResult, userMessage, conversationId, conversation, res);
@@ -261,37 +326,360 @@ class ChatController {
261
326
  });
262
327
  }
263
328
  }
264
- // Get conversation details with messages
329
+ // Get conversation details
265
330
  async getConversation(req, res) {
266
331
  try {
332
+ if (!req.profile) {
333
+ res.status(401).json({ error: 'Authentication required' });
334
+ return;
335
+ }
267
336
  const { uuid } = req.params;
268
337
  const conversation = await this.storage.getConversation(uuid);
269
338
  if (!conversation) {
270
339
  res.status(404).json({ error: 'Conversation not found' });
271
340
  return;
272
341
  }
273
- const messages = await this.storage.getMessages(uuid, 50);
342
+ if (conversation.organizationId && conversation.organizationId !== req.profile.companyId) {
343
+ res.status(404).json({ error: 'Conversation not found' });
344
+ return;
345
+ }
346
+ const toIso = (date) => (date ? date.toISOString() : null);
347
+ const status = conversation.closedAt
348
+ ? 'closed'
349
+ : conversation.joinedAt
350
+ ? 'in_progress'
351
+ : 'open';
274
352
  res.json({
275
353
  uuid: conversation.id,
276
354
  title: conversation.title,
277
355
  user_uuid: conversation.userId,
278
356
  company_uuid: conversation.organizationId,
279
357
  message_count: conversation.messageCount,
280
- created_at: conversation.createdAt,
358
+ created_at: toIso(conversation.createdAt),
359
+ updated_at: toIso(conversation.updatedAt),
360
+ joined_at: toIso(conversation.joinedAt),
361
+ responded_at: toIso(conversation.respondedAt),
362
+ closed_at: toIso(conversation.closedAt),
363
+ last_message_at: toIso(conversation.lastMessageAt),
364
+ status
365
+ });
366
+ }
367
+ catch (error) {
368
+ logger_1.default.error('Get conversation error:', error);
369
+ res.status(500).json({
370
+ error: 'Failed to get conversation',
371
+ message: error instanceof Error ? error.message : 'Unknown error'
372
+ });
373
+ }
374
+ }
375
+ // Get conversation messages
376
+ async getConversationMessages(req, res) {
377
+ try {
378
+ if (!req.profile) {
379
+ res.status(401).json({ error: 'Authentication required' });
380
+ return;
381
+ }
382
+ const { uuid } = req.params;
383
+ const page = Math.max(1, parseInt(req.query.page || '1', 10) || 1);
384
+ const pageSizeRaw = parseInt(req.query.page_size || '50', 10);
385
+ const pageSize = Math.min(200, Math.max(1, isNaN(pageSizeRaw) ? 50 : pageSizeRaw));
386
+ const offset = (page - 1) * pageSize;
387
+ const orderParam = (req.query.order || 'desc').toLowerCase();
388
+ const order = orderParam === 'asc' ? 'asc' : 'desc';
389
+ const conversation = await this.storage.getConversation(uuid);
390
+ if (!conversation) {
391
+ res.status(404).json({ error: 'Conversation not found' });
392
+ return;
393
+ }
394
+ if (conversation.organizationId && conversation.organizationId !== req.profile.companyId) {
395
+ res.status(404).json({ error: 'Conversation not found' });
396
+ return;
397
+ }
398
+ const messages = await this.storage.getMessages(uuid, pageSize, offset, { order });
399
+ const hasMore = messages.length === pageSize;
400
+ const toIso = (date) => (date ? date.toISOString() : null);
401
+ res.json({
402
+ conversation_uuid: conversation.id,
403
+ order,
281
404
  messages: messages.map(msg => ({
282
405
  uuid: msg.id,
283
406
  parent_message_uuid: msg.parentMessageId,
284
407
  type: msg.role,
285
408
  content: msg.content,
286
409
  status: 'completed',
287
- created_at: msg.createdAt
288
- }))
410
+ created_at: toIso(msg.createdAt),
411
+ author_id: msg.authorId ?? null
412
+ })),
413
+ pagination: {
414
+ page,
415
+ page_size: pageSize,
416
+ has_more: hasMore,
417
+ next_offset: hasMore ? offset + pageSize : null
418
+ }
289
419
  });
290
420
  }
291
421
  catch (error) {
292
- logger_1.default.error('Get conversation error:', error);
422
+ logger_1.default.error('Get conversation messages error:', error);
293
423
  res.status(500).json({
294
- error: 'Failed to get conversation',
424
+ error: 'Failed to get conversation messages',
425
+ message: error instanceof Error ? error.message : 'Unknown error'
426
+ });
427
+ }
428
+ }
429
+ // Join conversation
430
+ async joinConversation(req, res) {
431
+ try {
432
+ if (!req.user || !req.profile) {
433
+ res.status(401).json({ error: 'Authentication required' });
434
+ return;
435
+ }
436
+ const { uuid } = req.params;
437
+ const conversation = await this.storage.getConversation(uuid);
438
+ if (!conversation) {
439
+ res.status(404).json({ error: 'Conversation not found' });
440
+ return;
441
+ }
442
+ if (conversation.organizationId !== req.profile.companyId) {
443
+ res.status(404).json({ error: 'Conversation not found' });
444
+ return;
445
+ }
446
+ if (conversation.closedAt) {
447
+ res.status(400).json({ error: 'Conversation is closed' });
448
+ return;
449
+ }
450
+ const joinedAt = new Date();
451
+ await this.storage.updateConversation(uuid, {
452
+ joinedAt,
453
+ status: 'in_progress'
454
+ });
455
+ const systemMessage = await this.storage.saveMessage({
456
+ conversationId: uuid,
457
+ threadId: conversation.threadId,
458
+ role: 'system',
459
+ content: `${req.user.name} has joined the conversation.`,
460
+ createdAt: joinedAt,
461
+ authorId: parseInt(req.user.id)
462
+ });
463
+ const newMessageCount = conversation.messageCount + 1;
464
+ await this.storage.updateConversation(uuid, {
465
+ messageCount: newMessageCount,
466
+ lastMessageAt: joinedAt
467
+ });
468
+ if (this.realtimePublisher) {
469
+ try {
470
+ const { data: company } = await this.supabase
471
+ .from('vezlo_companies')
472
+ .select('uuid')
473
+ .eq('id', conversation.organizationId)
474
+ .single();
475
+ if (company?.uuid) {
476
+ await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
477
+ conversation_uuid: uuid,
478
+ message: {
479
+ uuid: systemMessage.id,
480
+ content: systemMessage.content,
481
+ type: systemMessage.role,
482
+ author_id: systemMessage.authorId,
483
+ created_at: systemMessage.createdAt.toISOString()
484
+ },
485
+ conversation_update: {
486
+ message_count: newMessageCount,
487
+ last_message_at: joinedAt.toISOString(),
488
+ joined_at: joinedAt.toISOString(),
489
+ status: 'in_progress'
490
+ }
491
+ });
492
+ }
493
+ }
494
+ catch (error) {
495
+ logger_1.default.error('[ChatController] Failed to publish join conversation update:', error);
496
+ }
497
+ }
498
+ res.json({
499
+ success: true,
500
+ message: {
501
+ uuid: systemMessage.id,
502
+ content: systemMessage.content,
503
+ type: systemMessage.role,
504
+ author_id: systemMessage.authorId,
505
+ created_at: systemMessage.createdAt.toISOString()
506
+ }
507
+ });
508
+ }
509
+ catch (error) {
510
+ logger_1.default.error('Join conversation error:', error);
511
+ res.status(500).json({
512
+ error: 'Failed to join conversation',
513
+ message: error instanceof Error ? error.message : 'Unknown error'
514
+ });
515
+ }
516
+ }
517
+ // Close conversation
518
+ async closeConversation(req, res) {
519
+ try {
520
+ if (!req.user || !req.profile) {
521
+ res.status(401).json({ error: 'Authentication required' });
522
+ return;
523
+ }
524
+ const { uuid } = req.params;
525
+ const conversation = await this.storage.getConversation(uuid);
526
+ if (!conversation) {
527
+ res.status(404).json({ error: 'Conversation not found' });
528
+ return;
529
+ }
530
+ if (conversation.organizationId !== req.profile.companyId) {
531
+ res.status(404).json({ error: 'Conversation not found' });
532
+ return;
533
+ }
534
+ if (conversation.closedAt) {
535
+ res.status(400).json({ error: 'Conversation is already closed' });
536
+ return;
537
+ }
538
+ const closedAt = new Date();
539
+ const systemMessage = await this.storage.saveMessage({
540
+ conversationId: uuid,
541
+ threadId: conversation.threadId,
542
+ role: 'system',
543
+ content: `${req.user.name} has closed the conversation.`,
544
+ createdAt: closedAt,
545
+ authorId: parseInt(req.user.id, 10)
546
+ });
547
+ const newMessageCount = conversation.messageCount + 1;
548
+ await this.storage.updateConversation(uuid, {
549
+ messageCount: newMessageCount,
550
+ lastMessageAt: closedAt,
551
+ closedAt
552
+ });
553
+ if (this.realtimePublisher) {
554
+ try {
555
+ const { data: company } = await this.supabase
556
+ .from('vezlo_companies')
557
+ .select('uuid')
558
+ .eq('id', conversation.organizationId)
559
+ .single();
560
+ if (company?.uuid) {
561
+ await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
562
+ conversation_uuid: uuid,
563
+ message: {
564
+ uuid: systemMessage.id,
565
+ content: systemMessage.content,
566
+ type: systemMessage.role,
567
+ author_id: systemMessage.authorId,
568
+ created_at: systemMessage.createdAt.toISOString()
569
+ },
570
+ conversation_update: {
571
+ message_count: newMessageCount,
572
+ last_message_at: closedAt.toISOString(),
573
+ joined_at: conversation.joinedAt?.toISOString() || null,
574
+ closed_at: closedAt.toISOString(),
575
+ status: 'closed'
576
+ }
577
+ });
578
+ }
579
+ }
580
+ catch (error) {
581
+ logger_1.default.error('[ChatController] Failed to publish close conversation update:', error);
582
+ }
583
+ }
584
+ res.json({
585
+ success: true,
586
+ message: {
587
+ uuid: systemMessage.id,
588
+ content: systemMessage.content,
589
+ type: systemMessage.role,
590
+ author_id: systemMessage.authorId,
591
+ created_at: systemMessage.createdAt.toISOString()
592
+ }
593
+ });
594
+ }
595
+ catch (error) {
596
+ logger_1.default.error('Close conversation error:', error);
597
+ res.status(500).json({
598
+ error: 'Failed to close conversation',
599
+ message: error instanceof Error ? error.message : 'Unknown error'
600
+ });
601
+ }
602
+ }
603
+ // Send agent message
604
+ async sendAgentMessage(req, res) {
605
+ try {
606
+ if (!req.user || !req.profile) {
607
+ res.status(401).json({ error: 'Authentication required' });
608
+ return;
609
+ }
610
+ const { uuid } = req.params;
611
+ const { content } = req.body;
612
+ if (!content) {
613
+ res.status(400).json({ error: 'content is required' });
614
+ return;
615
+ }
616
+ const conversation = await this.storage.getConversation(uuid);
617
+ if (!conversation) {
618
+ res.status(404).json({ error: 'Conversation not found' });
619
+ return;
620
+ }
621
+ if (conversation.organizationId !== req.profile.companyId) {
622
+ res.status(404).json({ error: 'Conversation not found' });
623
+ return;
624
+ }
625
+ if (conversation.closedAt) {
626
+ res.status(400).json({ error: 'Conversation is closed' });
627
+ return;
628
+ }
629
+ const agentMessage = await this.storage.saveMessage({
630
+ conversationId: uuid,
631
+ threadId: conversation.threadId,
632
+ role: 'agent',
633
+ content,
634
+ createdAt: new Date(),
635
+ authorId: parseInt(req.user.id)
636
+ });
637
+ const newMessageCount = conversation.messageCount + 1;
638
+ const timestamp = new Date();
639
+ await this.storage.updateConversation(uuid, {
640
+ messageCount: newMessageCount,
641
+ lastMessageAt: timestamp
642
+ });
643
+ if (this.realtimePublisher) {
644
+ try {
645
+ const { data: company } = await this.supabase
646
+ .from('vezlo_companies')
647
+ .select('uuid')
648
+ .eq('id', conversation.organizationId)
649
+ .single();
650
+ if (company?.uuid) {
651
+ await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
652
+ conversation_uuid: uuid,
653
+ message: {
654
+ uuid: agentMessage.id,
655
+ content: agentMessage.content,
656
+ type: agentMessage.role,
657
+ author_id: agentMessage.authorId,
658
+ created_at: agentMessage.createdAt.toISOString()
659
+ },
660
+ conversation_update: {
661
+ message_count: newMessageCount,
662
+ last_message_at: timestamp.toISOString()
663
+ }
664
+ });
665
+ }
666
+ }
667
+ catch (error) {
668
+ logger_1.default.error('[ChatController] Failed to publish agent message update:', error);
669
+ }
670
+ }
671
+ res.json({
672
+ uuid: agentMessage.id,
673
+ content: agentMessage.content,
674
+ type: agentMessage.role,
675
+ author_id: agentMessage.authorId,
676
+ created_at: agentMessage.createdAt.toISOString()
677
+ });
678
+ }
679
+ catch (error) {
680
+ logger_1.default.error('Send agent message error:', error);
681
+ res.status(500).json({
682
+ error: 'Failed to send agent message',
295
683
  message: error instanceof Error ? error.message : 'Unknown error'
296
684
  });
297
685
  }
@@ -303,14 +691,42 @@ class ChatController {
303
691
  res.status(401).json({ error: 'Authentication required' });
304
692
  return;
305
693
  }
306
- const conversations = await this.storage.getUserConversations(req.user.id, req.profile?.companyId || undefined);
694
+ const page = Math.max(1, parseInt(req.query.page || '1', 10) || 1);
695
+ const pageSizeRaw = parseInt(req.query.page_size || '20', 10);
696
+ const pageSize = Math.min(100, Math.max(1, isNaN(pageSizeRaw) ? 20 : pageSizeRaw));
697
+ const offset = (page - 1) * pageSize;
698
+ const orderParam = req.query.order_by || 'last_message_at';
699
+ const orderBy = orderParam === 'created_at' ? 'updated_at' : 'last_message_at';
700
+ const { conversations, total } = await this.storage.getUserConversations(req.user.id, req.profile.companyId, {
701
+ limit: pageSize,
702
+ offset,
703
+ orderBy: orderBy
704
+ });
705
+ const toIso = (date) => (date ? date.toISOString() : null);
307
706
  res.json({
308
707
  conversations: conversations.map(conversation => ({
309
708
  uuid: conversation.id,
310
709
  title: conversation.title,
311
710
  message_count: conversation.messageCount,
312
- created_at: conversation.createdAt
313
- }))
711
+ created_at: toIso(conversation.createdAt),
712
+ updated_at: toIso(conversation.updatedAt),
713
+ joined_at: toIso(conversation.joinedAt),
714
+ responded_at: toIso(conversation.respondedAt),
715
+ closed_at: toIso(conversation.closedAt),
716
+ last_message_at: toIso(conversation.lastMessageAt),
717
+ status: conversation.closedAt
718
+ ? 'closed'
719
+ : conversation.joinedAt
720
+ ? 'in_progress'
721
+ : 'open'
722
+ })),
723
+ pagination: {
724
+ page,
725
+ page_size: pageSize,
726
+ total,
727
+ total_pages: Math.max(1, Math.ceil(total / pageSize)),
728
+ has_more: offset + conversations.length < total
729
+ }
314
730
  });
315
731
  }
316
732
  catch (error) {
@@ -514,10 +930,55 @@ class ChatController {
514
930
  });
515
931
  if (options.conversation) {
516
932
  const nextCount = (options.conversation.messageCount || 0) + 1;
933
+ const timestamp = new Date();
517
934
  await this.storage.updateConversation(options.conversationId, {
518
- messageCount: nextCount
935
+ messageCount: nextCount,
936
+ lastMessageAt: timestamp
519
937
  });
520
938
  options.conversation.messageCount = nextCount;
939
+ options.conversation.lastMessageAt = timestamp;
940
+ // Publish realtime update
941
+ if (this.realtimePublisher && options.conversation.organizationId) {
942
+ try {
943
+ logger_1.default.info(`[ChatController] Fetching company UUID for assistant message, organizationId: ${options.conversation.organizationId}`);
944
+ const { data: company } = await this.supabase
945
+ .from('vezlo_companies')
946
+ .select('uuid')
947
+ .eq('id', options.conversation.organizationId)
948
+ .single();
949
+ if (company?.uuid) {
950
+ logger_1.default.info(`[ChatController] Publishing assistant message update for company: ${company.uuid}`);
951
+ await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
952
+ conversation_uuid: options.conversationId,
953
+ message: {
954
+ uuid: assistantMessage.id,
955
+ content: assistantMessage.content,
956
+ type: assistantMessage.role,
957
+ author_id: null,
958
+ created_at: assistantMessage.createdAt.toISOString()
959
+ },
960
+ conversation_update: {
961
+ message_count: nextCount,
962
+ last_message_at: timestamp.toISOString()
963
+ }
964
+ });
965
+ }
966
+ else {
967
+ logger_1.default.warn(`[ChatController] No company UUID found for organizationId: ${options.conversation.organizationId}`);
968
+ }
969
+ }
970
+ catch (error) {
971
+ logger_1.default.error('[ChatController] Failed to publish realtime update:', error);
972
+ }
973
+ }
974
+ else {
975
+ if (!this.realtimePublisher) {
976
+ logger_1.default.warn('[ChatController] Realtime publisher not available for assistant message');
977
+ }
978
+ if (!options.conversation.organizationId) {
979
+ logger_1.default.warn('[ChatController] No organizationId in conversation for assistant message');
980
+ }
981
+ }
521
982
  }
522
983
  return assistantMessage;
523
984
  }