@memberjunction/ng-conversations 2.105.0 → 2.107.0
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/lib/components/conversation/conversation-chat-area.component.d.ts +19 -13
- package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
- package/dist/lib/components/conversation/conversation-chat-area.component.js +120 -122
- package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
- package/dist/lib/components/message/message-input.component.d.ts +64 -0
- package/dist/lib/components/message/message-input.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-input.component.js +251 -107
- package/dist/lib/components/message/message-input.component.js.map +1 -1
- package/dist/lib/components/message/message-list.component.d.ts +3 -5
- package/dist/lib/components/message/message-list.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-list.component.js +38 -9
- package/dist/lib/components/message/message-list.component.js.map +1 -1
- package/dist/lib/models/lazy-artifact-info.d.ts +68 -0
- package/dist/lib/models/lazy-artifact-info.d.ts.map +1 -0
- package/dist/lib/models/lazy-artifact-info.js +150 -0
- package/dist/lib/models/lazy-artifact-info.js.map +1 -0
- package/dist/lib/services/conversation-agent.service.d.ts +11 -0
- package/dist/lib/services/conversation-agent.service.d.ts.map +1 -1
- package/dist/lib/services/conversation-agent.service.js +138 -5
- package/dist/lib/services/conversation-agent.service.js.map +1 -1
- package/dist/public-api.d.ts +1 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +1 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +12 -12
|
@@ -19,8 +19,12 @@ function MessageInputComponent_div_6_Template(rf, ctx) { if (rf & 1) {
|
|
|
19
19
|
i0.ɵɵelementStart(0, "div", 10);
|
|
20
20
|
i0.ɵɵelement(1, "i", 11);
|
|
21
21
|
i0.ɵɵelementStart(2, "span");
|
|
22
|
-
i0.ɵɵtext(3
|
|
22
|
+
i0.ɵɵtext(3);
|
|
23
23
|
i0.ɵɵelementEnd()();
|
|
24
|
+
} if (rf & 2) {
|
|
25
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
26
|
+
i0.ɵɵadvance(3);
|
|
27
|
+
i0.ɵɵtextInterpolate(ctx_r1.processingMessage);
|
|
24
28
|
} }
|
|
25
29
|
export class MessageInputComponent {
|
|
26
30
|
dialogService;
|
|
@@ -48,6 +52,7 @@ export class MessageInputComponent {
|
|
|
48
52
|
messageText = '';
|
|
49
53
|
isSending = false;
|
|
50
54
|
isProcessing = false; // True when waiting for agent/naming response
|
|
55
|
+
processingMessage = 'AI is responding...'; // Message shown during processing
|
|
51
56
|
converationManagerAgent = null;
|
|
52
57
|
// Mention autocomplete state
|
|
53
58
|
showMentionDropdown = false;
|
|
@@ -60,6 +65,8 @@ export class MessageInputComponent {
|
|
|
60
65
|
pushStatusSubscription;
|
|
61
66
|
// Track active task execution message IDs for real-time updates
|
|
62
67
|
activeTaskExecutionMessageIds = new Set();
|
|
68
|
+
// Track completion timestamps to prevent race conditions with late progress updates
|
|
69
|
+
completionTimestamps = new Map();
|
|
63
70
|
constructor(dialogService, toastService, agentService, conversationState, dataCache, activeTasks, mentionAutocomplete, mentionParser) {
|
|
64
71
|
this.dialogService = dialogService;
|
|
65
72
|
this.toastService = toastService;
|
|
@@ -298,94 +305,174 @@ export class MessageInputComponent {
|
|
|
298
305
|
return;
|
|
299
306
|
this.isSending = true;
|
|
300
307
|
try {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
detail.Message = this.messageText.trim();
|
|
304
|
-
detail.Role = 'User';
|
|
305
|
-
// Parse mentions from message (not stored, used for routing only)
|
|
306
|
-
const mentionResult = this.mentionParser.parseMentions(detail.Message, this.mentionAutocomplete.getAvailableAgents(), this.mentionAutocomplete.getAvailableUsers());
|
|
307
|
-
console.log('[MentionInput] Parsing message for routing:', detail.Message);
|
|
308
|
-
console.log('[MentionInput] Found mentions:', mentionResult);
|
|
309
|
-
console.log('[MentionInput] Agent mention:', mentionResult.agentMention);
|
|
310
|
-
// Set ParentID if this is a thread reply
|
|
311
|
-
if (this.parentMessageId) {
|
|
312
|
-
detail.ParentID = this.parentMessageId;
|
|
313
|
-
}
|
|
314
|
-
const saved = await detail.Save();
|
|
308
|
+
const messageDetail = await this.createMessageDetail();
|
|
309
|
+
const saved = await messageDetail.Save();
|
|
315
310
|
if (saved) {
|
|
316
|
-
this.
|
|
317
|
-
this.messageText = '';
|
|
318
|
-
// Check if this is the first message in the conversation
|
|
319
|
-
const isFirstMessage = this.conversationHistory.length === 0;
|
|
320
|
-
// Determine routing: @mention > last agent context > Sage
|
|
321
|
-
if (mentionResult.agentMention) {
|
|
322
|
-
// Direct @mention - skip Sage, invoke agent directly
|
|
323
|
-
console.log('🎯 Direct @mention detected, bypassing Sage');
|
|
324
|
-
if (isFirstMessage) {
|
|
325
|
-
Promise.all([
|
|
326
|
-
this.invokeAgentDirectly(detail, mentionResult.agentMention, this.conversationId),
|
|
327
|
-
this.nameConversation(detail.Message)
|
|
328
|
-
]);
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
this.invokeAgentDirectly(detail, mentionResult.agentMention, this.conversationId);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
// Check if user is replying to an agent (implicit continuation)
|
|
336
|
-
const lastAIMessage = this.conversationHistory
|
|
337
|
-
.slice()
|
|
338
|
-
.reverse()
|
|
339
|
-
.find(msg => msg.Role === 'AI' &&
|
|
340
|
-
msg.AgentID &&
|
|
341
|
-
msg.AgentID !== this.converationManagerAgent?.ID);
|
|
342
|
-
if (lastAIMessage && lastAIMessage.AgentID) {
|
|
343
|
-
// Continue with same agent - skip Sage
|
|
344
|
-
console.log('🔄 Implicit continuation detected, continuing with last agent');
|
|
345
|
-
if (isFirstMessage) {
|
|
346
|
-
Promise.all([
|
|
347
|
-
this.continueWithAgent(detail, lastAIMessage.AgentID, this.conversationId),
|
|
348
|
-
this.nameConversation(detail.Message)
|
|
349
|
-
]);
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
this.continueWithAgent(detail, lastAIMessage.AgentID, this.conversationId);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
// No context - use Sage
|
|
357
|
-
console.log('🤖 No agent context, using Sage');
|
|
358
|
-
if (isFirstMessage) {
|
|
359
|
-
Promise.all([
|
|
360
|
-
this.processMessageThroughAgent(detail, mentionResult),
|
|
361
|
-
this.nameConversation(detail.Message)
|
|
362
|
-
]);
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
this.processMessageThroughAgent(detail, mentionResult);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
// Focus back on textarea
|
|
370
|
-
setTimeout(() => {
|
|
371
|
-
if (this.messageTextarea && this.messageTextarea.nativeElement) {
|
|
372
|
-
this.messageTextarea.nativeElement.focus();
|
|
373
|
-
}
|
|
374
|
-
}, 100);
|
|
311
|
+
await this.handleSuccessfulSend(messageDetail);
|
|
375
312
|
}
|
|
376
313
|
else {
|
|
377
|
-
|
|
378
|
-
this.toastService.error('Failed to send message. Please try again.');
|
|
314
|
+
this.handleSendFailure(messageDetail);
|
|
379
315
|
}
|
|
380
316
|
}
|
|
381
317
|
catch (error) {
|
|
382
|
-
|
|
383
|
-
this.toastService.error('Error sending message. Please try again.');
|
|
318
|
+
this.handleSendError(error);
|
|
384
319
|
}
|
|
385
320
|
finally {
|
|
386
321
|
this.isSending = false;
|
|
387
322
|
}
|
|
388
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Creates and configures a new conversation detail message
|
|
326
|
+
*/
|
|
327
|
+
async createMessageDetail() {
|
|
328
|
+
const detail = await this.dataCache.createConversationDetail(this.currentUser);
|
|
329
|
+
detail.ConversationID = this.conversationId;
|
|
330
|
+
detail.Message = this.messageText.trim();
|
|
331
|
+
detail.Role = 'User';
|
|
332
|
+
if (this.parentMessageId) {
|
|
333
|
+
detail.ParentID = this.parentMessageId;
|
|
334
|
+
}
|
|
335
|
+
return detail;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Handles successful message send - routes to appropriate agent
|
|
339
|
+
*/
|
|
340
|
+
async handleSuccessfulSend(messageDetail) {
|
|
341
|
+
this.messageSent.emit(messageDetail);
|
|
342
|
+
this.messageText = '';
|
|
343
|
+
const mentionResult = this.parseMentionsFromMessage(messageDetail.Message);
|
|
344
|
+
const isFirstMessage = this.conversationHistory.length === 0;
|
|
345
|
+
await this.routeMessage(messageDetail, mentionResult, isFirstMessage);
|
|
346
|
+
this.refocusTextarea();
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Parses mentions from the message for routing decisions
|
|
350
|
+
*/
|
|
351
|
+
parseMentionsFromMessage(message) {
|
|
352
|
+
const mentionResult = this.mentionParser.parseMentions(message, this.mentionAutocomplete.getAvailableAgents(), this.mentionAutocomplete.getAvailableUsers());
|
|
353
|
+
console.log('[MentionInput] Parsing message for routing:', message);
|
|
354
|
+
console.log('[MentionInput] Found mentions:', mentionResult);
|
|
355
|
+
console.log('[MentionInput] Agent mention:', mentionResult.agentMention);
|
|
356
|
+
return mentionResult;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Routes the message to the appropriate agent or Sage based on context
|
|
360
|
+
* Priority: @mention > intent check > Sage
|
|
361
|
+
*/
|
|
362
|
+
async routeMessage(messageDetail, mentionResult, isFirstMessage) {
|
|
363
|
+
// Priority 1: Direct @mention
|
|
364
|
+
if (mentionResult.agentMention) {
|
|
365
|
+
await this.handleDirectMention(messageDetail, mentionResult.agentMention, isFirstMessage);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// Priority 2: Check for previous agent with intent check
|
|
369
|
+
const lastAgentId = this.findLastNonSageAgentId();
|
|
370
|
+
if (lastAgentId) {
|
|
371
|
+
await this.handleAgentContinuity(messageDetail, lastAgentId, mentionResult, isFirstMessage);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
// Priority 3: No context - use Sage
|
|
375
|
+
await this.handleNoAgentContext(messageDetail, mentionResult, isFirstMessage);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Handles routing when user directly mentions an agent with @
|
|
379
|
+
*/
|
|
380
|
+
async handleDirectMention(messageDetail, agentMention, isFirstMessage) {
|
|
381
|
+
console.log('🎯 Direct @mention detected, bypassing Sage');
|
|
382
|
+
await this.executeRouteWithNaming(() => this.invokeAgentDirectly(messageDetail, agentMention, this.conversationId), messageDetail.Message, isFirstMessage);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Handles routing when there's a previous agent - checks intent first
|
|
386
|
+
*/
|
|
387
|
+
async handleAgentContinuity(messageDetail, lastAgentId, mentionResult, isFirstMessage) {
|
|
388
|
+
console.log('🔍 Previous agent found, checking continuity intent...');
|
|
389
|
+
const intent = await this.checkContinuityIntent(lastAgentId, messageDetail.Message);
|
|
390
|
+
if (intent === 'YES') {
|
|
391
|
+
console.log('✅ Intent check: YES - continuing with previous agent');
|
|
392
|
+
await this.executeRouteWithNaming(() => this.continueWithAgent(messageDetail, lastAgentId, this.conversationId), messageDetail.Message, isFirstMessage);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
console.log(`🤖 Intent check: ${intent} - routing through Sage for evaluation`);
|
|
396
|
+
await this.executeRouteWithNaming(() => this.processMessageThroughAgent(messageDetail, mentionResult), messageDetail.Message, isFirstMessage);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Handles routing when there's no previous agent context
|
|
401
|
+
*/
|
|
402
|
+
async handleNoAgentContext(messageDetail, mentionResult, isFirstMessage) {
|
|
403
|
+
console.log('🤖 No agent context, using Sage');
|
|
404
|
+
await this.executeRouteWithNaming(() => this.processMessageThroughAgent(messageDetail, mentionResult), messageDetail.Message, isFirstMessage);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Finds the last agent ID that isn't Sage
|
|
408
|
+
*/
|
|
409
|
+
findLastNonSageAgentId() {
|
|
410
|
+
const lastAIMessage = this.conversationHistory
|
|
411
|
+
.slice()
|
|
412
|
+
.reverse()
|
|
413
|
+
.find(msg => msg.Role === 'AI' &&
|
|
414
|
+
msg.AgentID &&
|
|
415
|
+
msg.AgentID !== this.converationManagerAgent?.ID);
|
|
416
|
+
return lastAIMessage?.AgentID || null;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Checks if message should continue with the previous agent
|
|
420
|
+
* Shows UI indicator during check
|
|
421
|
+
*/
|
|
422
|
+
async checkContinuityIntent(agentId, message) {
|
|
423
|
+
this.processingMessage = 'Analyzing intent...';
|
|
424
|
+
this.isProcessing = true;
|
|
425
|
+
try {
|
|
426
|
+
const intent = await this.agentService.checkAgentContinuityIntent(agentId, message, this.conversationHistory);
|
|
427
|
+
return intent;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
console.error('❌ Intent check failed, defaulting to UNSURE:', error);
|
|
431
|
+
return 'UNSURE';
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
this.processingMessage = 'AI is responding...';
|
|
435
|
+
this.isProcessing = false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Executes a routing function, optionally with conversation naming for first message
|
|
440
|
+
*/
|
|
441
|
+
async executeRouteWithNaming(routeFunction, userMessage, isFirstMessage) {
|
|
442
|
+
if (isFirstMessage) {
|
|
443
|
+
await Promise.all([
|
|
444
|
+
routeFunction(),
|
|
445
|
+
this.nameConversation(userMessage)
|
|
446
|
+
]);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
await routeFunction();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Returns focus to the message textarea
|
|
454
|
+
*/
|
|
455
|
+
refocusTextarea() {
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
if (this.messageTextarea?.nativeElement) {
|
|
458
|
+
this.messageTextarea.nativeElement.focus();
|
|
459
|
+
}
|
|
460
|
+
}, 100);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Handles message send failure
|
|
464
|
+
*/
|
|
465
|
+
handleSendFailure(messageDetail) {
|
|
466
|
+
console.error('Failed to send message:', messageDetail.LatestResult?.Message);
|
|
467
|
+
this.toastService.error('Failed to send message. Please try again.');
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Handles message send error
|
|
471
|
+
*/
|
|
472
|
+
handleSendError(error) {
|
|
473
|
+
console.error('Error sending message:', error);
|
|
474
|
+
this.toastService.error('Error sending message. Please try again.');
|
|
475
|
+
}
|
|
389
476
|
/**
|
|
390
477
|
* Safe save for ConversationDetail - prevents overwrites of completed/errored messages
|
|
391
478
|
* Use this ONLY in progress update paths to prevent race conditions
|
|
@@ -408,9 +495,23 @@ export class MessageInputComponent {
|
|
|
408
495
|
* IMPORTANT: Filters by agentRunId to prevent cross-contamination when multiple agents run in parallel
|
|
409
496
|
*/
|
|
410
497
|
createProgressCallback(conversationDetail, agentName) {
|
|
498
|
+
// Use closure to capture the agent run ID from the first progress message
|
|
499
|
+
// This allows us to filter out progress messages from other concurrent agents
|
|
500
|
+
let capturedAgentRunId = null;
|
|
411
501
|
return async (progress) => {
|
|
412
502
|
// Extract agentRunId from progress metadata
|
|
413
503
|
const progressAgentRunId = progress.metadata?.agentRunId;
|
|
504
|
+
// Capture the agent run ID from the first progress message
|
|
505
|
+
if (!capturedAgentRunId && progressAgentRunId) {
|
|
506
|
+
capturedAgentRunId = progressAgentRunId;
|
|
507
|
+
console.log(`[${agentName}] 📌 Captured agent run ID: ${capturedAgentRunId} for conversation detail: ${conversationDetail.ID}`);
|
|
508
|
+
}
|
|
509
|
+
// Filter out progress messages from other concurrent agents
|
|
510
|
+
// This prevents cross-contamination when multiple agents run in parallel
|
|
511
|
+
if (capturedAgentRunId && progressAgentRunId && progressAgentRunId !== capturedAgentRunId) {
|
|
512
|
+
console.log(`[${agentName}] 🚫 Ignoring progress from different agent run (expected: ${capturedAgentRunId}, got: ${progressAgentRunId})`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
414
515
|
// Format progress message with visual indicator
|
|
415
516
|
const progressText = progress.message;
|
|
416
517
|
// Update the active task with progress details (if it exists)
|
|
@@ -419,10 +520,16 @@ export class MessageInputComponent {
|
|
|
419
520
|
try {
|
|
420
521
|
if (conversationDetail) {
|
|
421
522
|
console.log(`[${agentName}] Got conversation detail from cache - Status: ${conversationDetail.Status}, ID: ${conversationDetail.ID}`);
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
523
|
+
// Check 1: Skip if message is already complete or errored
|
|
524
|
+
if (conversationDetail.Status === 'Complete' || conversationDetail.Status === 'Error') {
|
|
525
|
+
console.log(`[${agentName}] ⛔ Skipping progress update - message status is ${conversationDetail.Status}`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
// Check 2: Skip if message was marked as completed (prevents race condition)
|
|
529
|
+
// Once a message is marked complete, we reject ALL further progress updates
|
|
530
|
+
const completionTime = this.completionTimestamps.get(conversationDetail.ID);
|
|
531
|
+
if (completionTime) {
|
|
532
|
+
console.log(`[${agentName}] ⛔ Skipping progress update - message was marked complete at ${completionTime}`);
|
|
426
533
|
return;
|
|
427
534
|
}
|
|
428
535
|
// Emit agentRunId if we have it (for parent to track)
|
|
@@ -492,7 +599,8 @@ export class MessageInputComponent {
|
|
|
492
599
|
taskId = null;
|
|
493
600
|
}
|
|
494
601
|
if (!result || !result.success) {
|
|
495
|
-
// Evaluation failed
|
|
602
|
+
// Evaluation failed - mark as complete to stop progress updates
|
|
603
|
+
this.markMessageComplete(conversationManagerMessage);
|
|
496
604
|
conversationManagerMessage.Status = 'Error';
|
|
497
605
|
conversationManagerMessage.Message = `❌ Evaluation failed`;
|
|
498
606
|
conversationManagerMessage.Error = result?.agentRun?.ErrorMessage || 'Agent evaluation failed';
|
|
@@ -502,6 +610,8 @@ export class MessageInputComponent {
|
|
|
502
610
|
await userMessage.Save();
|
|
503
611
|
this.messageSent.emit(userMessage);
|
|
504
612
|
console.warn('⚠️ Sage failed:', result?.agentRun?.ErrorMessage);
|
|
613
|
+
// Clean up completion timestamp
|
|
614
|
+
this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
|
|
505
615
|
return;
|
|
506
616
|
}
|
|
507
617
|
console.log('🤖 Sage Response:', {
|
|
@@ -531,6 +641,8 @@ export class MessageInputComponent {
|
|
|
531
641
|
}
|
|
532
642
|
// Stage 4: Direct chat response from Sage
|
|
533
643
|
else if (result.agentRun.FinalStep === 'Chat' && result.agentRun.Message) {
|
|
644
|
+
// Mark message as completing BEFORE setting final content (prevents race condition)
|
|
645
|
+
this.markMessageComplete(conversationManagerMessage);
|
|
534
646
|
// Normal chat response
|
|
535
647
|
conversationManagerMessage.Message = result.agentRun.Message;
|
|
536
648
|
conversationManagerMessage.Status = 'Complete';
|
|
@@ -549,26 +661,35 @@ export class MessageInputComponent {
|
|
|
549
661
|
if (taskId) {
|
|
550
662
|
this.activeTasks.remove(taskId);
|
|
551
663
|
}
|
|
664
|
+
// Clean up completion timestamp after delay
|
|
665
|
+
this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
|
|
552
666
|
}
|
|
553
667
|
// Stage 5: Silent observation - but check for message content first
|
|
554
668
|
else {
|
|
555
669
|
// Check if there's a message to display even without payload/taskGraph
|
|
556
670
|
if (result.agentRun.Message) {
|
|
557
671
|
console.log('💬 Sage provided a message without payload');
|
|
558
|
-
|
|
559
|
-
conversationManagerMessage
|
|
672
|
+
// Mark message as completing BEFORE setting final content
|
|
673
|
+
this.markMessageComplete(conversationManagerMessage);
|
|
560
674
|
conversationManagerMessage.HiddenToUser = false;
|
|
561
|
-
|
|
675
|
+
// use update helper to ensure that if there is a race condition with more streaming updates we don't allow that to override this final message
|
|
676
|
+
await this.updateConversationDetail(conversationManagerMessage, result.agentRun.Message, 'Complete');
|
|
562
677
|
this.messageSent.emit(conversationManagerMessage);
|
|
678
|
+
// Clean up completion timestamp after delay
|
|
679
|
+
this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
|
|
563
680
|
}
|
|
564
681
|
else {
|
|
565
682
|
console.log('🔇 Sage chose to observe silently');
|
|
683
|
+
// Mark message as completing
|
|
684
|
+
this.markMessageComplete(conversationManagerMessage);
|
|
566
685
|
// Hide the Sage message
|
|
567
686
|
conversationManagerMessage.HiddenToUser = true;
|
|
568
|
-
|
|
569
|
-
await conversationManagerMessage.
|
|
687
|
+
// use update helper to ensure that if there is a race condition with more streaming updates we don't allow that to override this final message
|
|
688
|
+
await this.updateConversationDetail(conversationManagerMessage, conversationManagerMessage.Message, 'Complete');
|
|
570
689
|
this.messageSent.emit(conversationManagerMessage);
|
|
571
690
|
await this.handleSilentObservation(userMessage, this.conversationId);
|
|
691
|
+
// Clean up completion timestamp after delay
|
|
692
|
+
this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
|
|
572
693
|
}
|
|
573
694
|
// Remove CM from active tasks
|
|
574
695
|
if (taskId) {
|
|
@@ -580,11 +701,15 @@ export class MessageInputComponent {
|
|
|
580
701
|
console.error('❌ Error processing message through agents:', error);
|
|
581
702
|
// Update conversationManagerMessage status to Error
|
|
582
703
|
if (conversationManagerMessage && conversationManagerMessage.ID) {
|
|
704
|
+
// Mark as complete to stop progress updates
|
|
705
|
+
this.markMessageComplete(conversationManagerMessage);
|
|
583
706
|
conversationManagerMessage.Status = 'Error';
|
|
584
707
|
conversationManagerMessage.Message = `❌ Error: ${String(error)}`;
|
|
585
708
|
conversationManagerMessage.Error = String(error);
|
|
586
709
|
await conversationManagerMessage.Save();
|
|
587
710
|
this.messageSent.emit(conversationManagerMessage);
|
|
711
|
+
// Clean up completion timestamp
|
|
712
|
+
this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
|
|
588
713
|
}
|
|
589
714
|
// Mark user message as complete
|
|
590
715
|
userMessage.Status = 'Complete';
|
|
@@ -685,27 +810,20 @@ export class MessageInputComponent {
|
|
|
685
810
|
};
|
|
686
811
|
const result = await GraphQLDataProvider.Instance.ExecuteGQL(mutation, variables);
|
|
687
812
|
console.log('📊 ExecuteTaskGraph result:', {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
813
|
+
hasExecuteTaskGraph: !!result?.ExecuteTaskGraph,
|
|
814
|
+
success: result?.ExecuteTaskGraph?.success,
|
|
815
|
+
resultsCount: result?.ExecuteTaskGraph?.results?.length,
|
|
816
|
+
result: result
|
|
692
817
|
});
|
|
693
818
|
// Step 4: Update task execution message with results
|
|
694
|
-
//
|
|
695
|
-
if (result?.
|
|
696
|
-
const errorMsg = result.errors.map((e) => e.message).join(', ');
|
|
697
|
-
console.error('❌ GraphQL errors:', result.errors);
|
|
698
|
-
taskExecutionMessage.Message = `❌ **${workflowName}** failed: ${errorMsg}`;
|
|
699
|
-
taskExecutionMessage.Status = 'Error';
|
|
700
|
-
taskExecutionMessage.Error = errorMsg;
|
|
701
|
-
}
|
|
702
|
-
else if (result?.data?.ExecuteTaskGraph?.success) {
|
|
819
|
+
// ExecuteGQL returns data directly (not wrapped in {data, errors})
|
|
820
|
+
if (result?.ExecuteTaskGraph?.success) {
|
|
703
821
|
console.log('✅ Task graph execution completed successfully');
|
|
704
822
|
taskExecutionMessage.Message = `✅ **${workflowName}** completed successfully`;
|
|
705
823
|
taskExecutionMessage.Status = 'Complete';
|
|
706
824
|
}
|
|
707
825
|
else {
|
|
708
|
-
const errorMsg = result?.
|
|
826
|
+
const errorMsg = result?.ExecuteTaskGraph?.errorMessage || 'Unknown error';
|
|
709
827
|
console.error('❌ Task graph execution failed:', errorMsg);
|
|
710
828
|
taskExecutionMessage.Message = `❌ **${workflowName}** failed: ${errorMsg}`;
|
|
711
829
|
taskExecutionMessage.Status = 'Error';
|
|
@@ -738,6 +856,10 @@ export class MessageInputComponent {
|
|
|
738
856
|
if (convoDetail.Status === 'Complete' || convoDetail.Status === 'Error') {
|
|
739
857
|
return; // Do not update completed or errored messages
|
|
740
858
|
}
|
|
859
|
+
// Mark as completing BEFORE updating if status is Complete or Error
|
|
860
|
+
if (status === 'Complete' || status === 'Error') {
|
|
861
|
+
this.markMessageComplete(convoDetail);
|
|
862
|
+
}
|
|
741
863
|
const maxAttempts = 2;
|
|
742
864
|
let attempts = 0, done = false;
|
|
743
865
|
while (attempts < maxAttempts && !done) {
|
|
@@ -753,6 +875,10 @@ export class MessageInputComponent {
|
|
|
753
875
|
}
|
|
754
876
|
attempts++;
|
|
755
877
|
}
|
|
878
|
+
// Clean up completion timestamp after delay
|
|
879
|
+
if (status === 'Complete' || status === 'Error') {
|
|
880
|
+
this.cleanupCompletionTimestamp(convoDetail.ID);
|
|
881
|
+
}
|
|
756
882
|
}
|
|
757
883
|
/**
|
|
758
884
|
* Handle single task execution from task graph using direct agent execution
|
|
@@ -1445,13 +1571,30 @@ export class MessageInputComponent {
|
|
|
1445
1571
|
// Don't show error to user - naming failures should be silent
|
|
1446
1572
|
}
|
|
1447
1573
|
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Marks a conversation detail as complete and records timestamp to prevent race conditions
|
|
1576
|
+
*/
|
|
1577
|
+
markMessageComplete(conversationDetail) {
|
|
1578
|
+
const now = Date.now();
|
|
1579
|
+
this.completionTimestamps.set(conversationDetail.ID, now);
|
|
1580
|
+
console.log(`🏁 Marked message ${conversationDetail.ID} as complete at ${now}`);
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Cleans up completion timestamps for completed messages (prevents memory leak)
|
|
1584
|
+
*/
|
|
1585
|
+
cleanupCompletionTimestamp(conversationDetailId) {
|
|
1586
|
+
// Keep timestamp for a short period to catch any late progress updates
|
|
1587
|
+
setTimeout(() => {
|
|
1588
|
+
this.completionTimestamps.delete(conversationDetailId);
|
|
1589
|
+
}, 5000); // 5 seconds should be more than enough
|
|
1590
|
+
}
|
|
1448
1591
|
static ɵfac = function MessageInputComponent_Factory(t) { return new (t || MessageInputComponent)(i0.ɵɵdirectiveInject(i1.DialogService), i0.ɵɵdirectiveInject(i2.ToastService), i0.ɵɵdirectiveInject(i3.ConversationAgentService), i0.ɵɵdirectiveInject(i4.ConversationStateService), i0.ɵɵdirectiveInject(i5.DataCacheService), i0.ɵɵdirectiveInject(i6.ActiveTasksService), i0.ɵɵdirectiveInject(i7.MentionAutocompleteService), i0.ɵɵdirectiveInject(i8.MentionParserService)); };
|
|
1449
1592
|
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MessageInputComponent, selectors: [["mj-message-input"]], viewQuery: function MessageInputComponent_Query(rf, ctx) { if (rf & 1) {
|
|
1450
1593
|
i0.ɵɵviewQuery(_c0, 5);
|
|
1451
1594
|
} if (rf & 2) {
|
|
1452
1595
|
let _t;
|
|
1453
1596
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.messageTextarea = _t.first);
|
|
1454
|
-
} }, inputs: { conversationId: "conversationId", currentUser: "currentUser", disabled: "disabled", placeholder: "placeholder", parentMessageId: "parentMessageId", conversationHistory: "conversationHistory" }, outputs: { messageSent: "messageSent", agentResponse: "agentResponse", agentRunDetected: "agentRunDetected", artifactCreated: "artifactCreated", conversationRenamed: "conversationRenamed" }, decls: 11, vars:
|
|
1597
|
+
} }, inputs: { conversationId: "conversationId", currentUser: "currentUser", disabled: "disabled", placeholder: "placeholder", parentMessageId: "parentMessageId", conversationHistory: "conversationHistory" }, outputs: { messageSent: "messageSent", agentResponse: "agentResponse", agentRunDetected: "agentRunDetected", artifactCreated: "artifactCreated", conversationRenamed: "conversationRenamed" }, decls: 11, vars: 13, consts: [["messageTextarea", ""], [1, "message-input-container"], ["rows", "3", 1, "message-input", 3, "ngModelChange", "keydown", "input", "ngModel", "placeholder", "disabled"], [3, "suggestionSelected", "closed", "suggestions", "position", "visible", "showAbove"], [1, "input-actions"], ["class", "processing-indicator", 4, "ngIf"], ["title", "Attach file (coming soon)", 1, "btn-attach", 3, "disabled"], [1, "fas", "fa-paperclip"], [1, "btn-send", 3, "click", "disabled", "title"], [1, "fas", "fa-paper-plane"], [1, "processing-indicator"], [1, "fas", "fa-circle-notch", "fa-spin"]], template: function MessageInputComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1455
1598
|
const _r1 = i0.ɵɵgetCurrentView();
|
|
1456
1599
|
i0.ɵɵelementStart(0, "div", 1)(1, "textarea", 2, 0);
|
|
1457
1600
|
i0.ɵɵtwoWayListener("ngModelChange", function MessageInputComponent_Template_textarea_ngModelChange_1_listener($event) { i0.ɵɵrestoreView(_r1); i0.ɵɵtwoWayBindingSet(ctx.messageText, $event) || (ctx.messageText = $event); return i0.ɵɵresetView($event); });
|
|
@@ -1462,7 +1605,7 @@ export class MessageInputComponent {
|
|
|
1462
1605
|
i0.ɵɵlistener("suggestionSelected", function MessageInputComponent_Template_mj_mention_dropdown_suggestionSelected_4_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onMentionSelected($event)); })("closed", function MessageInputComponent_Template_mj_mention_dropdown_closed_4_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.closeMentionDropdown()); });
|
|
1463
1606
|
i0.ɵɵelementEnd();
|
|
1464
1607
|
i0.ɵɵelementStart(5, "div", 4);
|
|
1465
|
-
i0.ɵɵtemplate(6, MessageInputComponent_div_6_Template, 4,
|
|
1608
|
+
i0.ɵɵtemplate(6, MessageInputComponent_div_6_Template, 4, 1, "div", 5);
|
|
1466
1609
|
i0.ɵɵelementStart(7, "button", 6);
|
|
1467
1610
|
i0.ɵɵelement(8, "i", 7);
|
|
1468
1611
|
i0.ɵɵelementEnd();
|
|
@@ -1472,8 +1615,9 @@ export class MessageInputComponent {
|
|
|
1472
1615
|
i0.ɵɵelementEnd()()();
|
|
1473
1616
|
} if (rf & 2) {
|
|
1474
1617
|
i0.ɵɵadvance();
|
|
1618
|
+
i0.ɵɵclassProp("intent-checking", ctx.isProcessing);
|
|
1475
1619
|
i0.ɵɵtwoWayProperty("ngModel", ctx.messageText);
|
|
1476
|
-
i0.ɵɵproperty("placeholder", ctx.placeholder)("disabled", ctx.disabled || ctx.
|
|
1620
|
+
i0.ɵɵproperty("placeholder", ctx.placeholder)("disabled", ctx.disabled || ctx.isProcessing);
|
|
1477
1621
|
i0.ɵɵadvance(3);
|
|
1478
1622
|
i0.ɵɵproperty("suggestions", ctx.mentionSuggestions)("position", ctx.mentionDropdownPosition)("visible", ctx.showMentionDropdown)("showAbove", ctx.mentionDropdownShowAbove);
|
|
1479
1623
|
i0.ɵɵadvance(2);
|
|
@@ -1482,11 +1626,11 @@ export class MessageInputComponent {
|
|
|
1482
1626
|
i0.ɵɵproperty("disabled", ctx.disabled);
|
|
1483
1627
|
i0.ɵɵadvance(2);
|
|
1484
1628
|
i0.ɵɵproperty("disabled", !ctx.canSend)("title", ctx.isSending ? "Sending..." : "Send message");
|
|
1485
|
-
} }, dependencies: [i9.NgIf, i10.DefaultValueAccessor, i10.NgControlStatus, i10.NgModel, i11.MentionDropdownComponent], styles: [".message-input-container[_ngcontent-%COMP%] {\n position: relative;\n padding: 16px 24px;\n border-top: 1px solid #D9D9D9;\n background: #FFF;\n}\n\n.message-input-wrapper[_ngcontent-%COMP%] {\n border: 2px solid #D9D9D9;\n border-radius: 8px;\n padding: 12px;\n transition: border-color 0.2s, box-shadow 0.2s;\n background: #FFF;\n}\n\n.message-input-wrapper[_ngcontent-%COMP%]:focus-within {\n border-color: #0076B6;\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.1);\n}\n\n.message-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 0;\n border: none;\n resize: none;\n font-family: inherit;\n font-size: 14px;\n min-height: 40px;\n max-height: 200px;\n line-height: 1.5;\n}\n\n.message-input[_ngcontent-%COMP%]:focus {\n outline: none;\n}\n\n.message-input[_ngcontent-%COMP%]:disabled {\n background:
|
|
1629
|
+
} }, dependencies: [i9.NgIf, i10.DefaultValueAccessor, i10.NgControlStatus, i10.NgModel, i11.MentionDropdownComponent], styles: [".message-input-container[_ngcontent-%COMP%] {\n position: relative;\n padding: 16px 24px;\n border-top: 1px solid #D9D9D9;\n background: #FFF;\n}\n\n.message-input-wrapper[_ngcontent-%COMP%] {\n border: 2px solid #D9D9D9;\n border-radius: 8px;\n padding: 12px;\n transition: border-color 0.2s, box-shadow 0.2s;\n background: #FFF;\n}\n\n.message-input-wrapper[_ngcontent-%COMP%]:focus-within {\n border-color: #0076B6;\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.1);\n}\n\n.message-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 0;\n border: none;\n resize: none;\n font-family: inherit;\n font-size: 14px;\n min-height: 40px;\n max-height: 200px;\n line-height: 1.5;\n}\n\n.message-input[_ngcontent-%COMP%]:focus {\n outline: none;\n}\n\n.message-input[_ngcontent-%COMP%]:disabled {\n background: transparent;\n cursor: wait;\n}\n\n//[_ngcontent-%COMP%] Subtle[_ngcontent-%COMP%] visual[_ngcontent-%COMP%] feedback[_ngcontent-%COMP%] when[_ngcontent-%COMP%] checking[_ngcontent-%COMP%] intent[_ngcontent-%COMP%] (no[_ngcontent-%COMP%] ugly[_ngcontent-%COMP%] gray[_ngcontent-%COMP%] background)\n.message-input.intent-checking[_ngcontent-%COMP%] {\n opacity: 0.6;\n transition: opacity 0.2s;\n}\n.input-actions[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 12px;\n}\n.btn-attach[_ngcontent-%COMP%] {\n padding: 8px 16px;\n background: transparent;\n border: 1px solid #D9D9D9;\n border-radius: 6px;\n cursor: pointer;\n color: #333;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.btn-attach[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #F4F4F4;\n border-color: #AAA;\n}\n.btn-attach[_ngcontent-%COMP%]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.btn-send[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n background: #3B82F6;\n color: white;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.2s;\n flex-shrink: 0;\n}\n.btn-send[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #2563EB;\n}\n.btn-send[_ngcontent-%COMP%]:disabled {\n background: #D9D9D9;\n color: #AAA;\n cursor: not-allowed;\n}\n.btn-send[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 16px;\n}\n.processing-indicator[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #6B7280;\n margin-right: auto;\n}\n.processing-indicator[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #0076B6;\n}"] });
|
|
1486
1630
|
}
|
|
1487
1631
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MessageInputComponent, [{
|
|
1488
1632
|
type: Component,
|
|
1489
|
-
args: [{ selector: 'mj-message-input', template: "<div class=\"message-input-container\">\n <textarea\n #messageTextarea\n class=\"message-input\"\n [(ngModel)]=\"messageText\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled ||
|
|
1633
|
+
args: [{ selector: 'mj-message-input', template: "<div class=\"message-input-container\">\n <textarea\n #messageTextarea\n class=\"message-input\"\n [class.intent-checking]=\"isProcessing\"\n [(ngModel)]=\"messageText\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled || isProcessing\"\n (keydown)=\"onKeyDown($event)\"\n (input)=\"onInput($event)\"\n rows=\"3\">\n </textarea>\n\n <!-- Mention Autocomplete Dropdown -->\n <mj-mention-dropdown\n [suggestions]=\"mentionSuggestions\"\n [position]=\"mentionDropdownPosition\"\n [visible]=\"showMentionDropdown\"\n [showAbove]=\"mentionDropdownShowAbove\"\n (suggestionSelected)=\"onMentionSelected($event)\"\n (closed)=\"closeMentionDropdown()\">\n </mj-mention-dropdown>\n\n <div class=\"input-actions\">\n <div class=\"processing-indicator\" *ngIf=\"isProcessing\">\n <i class=\"fas fa-circle-notch fa-spin\"></i>\n <span>{{ processingMessage }}</span>\n </div>\n <button\n class=\"btn-attach\"\n [disabled]=\"disabled\"\n title=\"Attach file (coming soon)\">\n <i class=\"fas fa-paperclip\"></i>\n </button>\n <button\n class=\"btn-send\"\n [disabled]=\"!canSend\"\n (click)=\"onSend()\"\n [title]=\"isSending ? 'Sending...' : 'Send message'\">\n <i class=\"fas fa-paper-plane\"></i>\n </button>\n </div>\n</div>", styles: [".message-input-container {\n position: relative;\n padding: 16px 24px;\n border-top: 1px solid #D9D9D9;\n background: #FFF;\n}\n\n.message-input-wrapper {\n border: 2px solid #D9D9D9;\n border-radius: 8px;\n padding: 12px;\n transition: border-color 0.2s, box-shadow 0.2s;\n background: #FFF;\n}\n\n.message-input-wrapper:focus-within {\n border-color: #0076B6;\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.1);\n}\n\n.message-input {\n width: 100%;\n padding: 0;\n border: none;\n resize: none;\n font-family: inherit;\n font-size: 14px;\n min-height: 40px;\n max-height: 200px;\n line-height: 1.5;\n}\n\n.message-input:focus {\n outline: none;\n}\n\n.message-input:disabled {\n background: transparent;\n cursor: wait;\n}\n\n// Subtle visual feedback when checking intent (no ugly gray background)\n.message-input.intent-checking {\n opacity: 0.6;\n transition: opacity 0.2s;\n}\n.input-actions {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 12px;\n}\n.btn-attach {\n padding: 8px 16px;\n background: transparent;\n border: 1px solid #D9D9D9;\n border-radius: 6px;\n cursor: pointer;\n color: #333;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.btn-attach:hover:not(:disabled) {\n background: #F4F4F4;\n border-color: #AAA;\n}\n.btn-attach:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.btn-send {\n width: 40px;\n height: 40px;\n background: #3B82F6;\n color: white;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.2s;\n flex-shrink: 0;\n}\n.btn-send:hover:not(:disabled) {\n background: #2563EB;\n}\n.btn-send:disabled {\n background: #D9D9D9;\n color: #AAA;\n cursor: not-allowed;\n}\n.btn-send i {\n font-size: 16px;\n}\n.processing-indicator {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #6B7280;\n margin-right: auto;\n}\n.processing-indicator i {\n color: #0076B6;\n}"] }]
|
|
1490
1634
|
}], () => [{ type: i1.DialogService }, { type: i2.ToastService }, { type: i3.ConversationAgentService }, { type: i4.ConversationStateService }, { type: i5.DataCacheService }, { type: i6.ActiveTasksService }, { type: i7.MentionAutocompleteService }, { type: i8.MentionParserService }], { conversationId: [{
|
|
1491
1635
|
type: Input
|
|
1492
1636
|
}], currentUser: [{
|