@memberjunction/ng-ai-test-harness
Version:
MemberJunction AI Test Harness - A reusable component for testing AI agents and prompts with beautiful UX
1,073 lines (1,072 loc) • 60 kB
JavaScript
import { Component, Input, ViewChild, ChangeDetectionStrategy, ViewContainerRef, Output, EventEmitter } from '@angular/core';
import { Subject, interval } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ExecutionNodeComponent } from './agent-execution-node.component';
import * as i0 from "@angular/core";
const _c0 = ["executionTreeContainer"];
const _c1 = ["executionNodesContainer"];
function AgentExecutionMonitorComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "span", 6);
i0.ɵɵelement(1, "span", 19);
i0.ɵɵtext(2, " LIVE ");
i0.ɵɵelementEnd();
} }
function AgentExecutionMonitorComponent_Conditional_7_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div", 7)(1, "span", 20);
i0.ɵɵtext(2, "Current:");
i0.ɵɵelementEnd();
i0.ɵɵelementStart(3, "span", 21);
i0.ɵɵtext(4);
i0.ɵɵelementEnd()();
} if (rf & 2) {
const ctx_r1 = i0.ɵɵnextContext();
i0.ɵɵadvance(4);
i0.ɵɵtextInterpolate(ctx_r1.currentStep.StepName);
} }
function AgentExecutionMonitorComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
const _r3 = i0.ɵɵgetCurrentView();
i0.ɵɵelementStart(0, "button", 22);
i0.ɵɵlistener("click", function AgentExecutionMonitorComponent_Conditional_8_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onViewRunClick()); });
i0.ɵɵelement(1, "i", 23);
i0.ɵɵtext(2);
i0.ɵɵelementEnd();
} if (rf & 2) {
const ctx_r1 = i0.ɵɵnextContext();
i0.ɵɵadvance(2);
i0.ɵɵtextInterpolate1(" View ", ctx_r1.runType === "agent" ? "Agent" : "Prompt", " Run ");
} }
function AgentExecutionMonitorComponent_Conditional_13_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div", 11);
i0.ɵɵelement(1, "i", 24);
i0.ɵɵelementStart(2, "p");
i0.ɵɵtext(3, "Waiting for execution to begin...");
i0.ɵɵelementEnd()();
} }
function AgentExecutionMonitorComponent_Conditional_21_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "span", 17);
i0.ɵɵtext(1);
i0.ɵɵelementEnd();
} if (rf & 2) {
const ctx_r1 = i0.ɵɵnextContext();
i0.ɵɵadvance();
i0.ɵɵtextInterpolate1("(", ctx_r1.stats.failedSteps, " failed)");
} }
function AgentExecutionMonitorComponent_Conditional_37_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div", 14)(1, "span", 15);
i0.ɵɵtext(2, "Duration");
i0.ɵɵelementEnd();
i0.ɵɵelementStart(3, "span", 16);
i0.ɵɵtext(4);
i0.ɵɵelementEnd()();
} if (rf & 2) {
const ctx_r1 = i0.ɵɵnextContext();
i0.ɵɵadvance(4);
i0.ɵɵtextInterpolate(ctx_r1.formatDuration(ctx_r1.stats.totalDuration));
} }
function AgentExecutionMonitorComponent_Conditional_38_For_2_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "span", 25);
i0.ɵɵtext(1);
i0.ɵɵelementEnd();
} if (rf & 2) {
const type_r4 = ctx.$implicit;
const ctx_r1 = i0.ɵɵnextContext(2);
i0.ɵɵadvance();
i0.ɵɵtextInterpolate2(" ", ctx_r1.stats.stepsByType[type_r4], " ", ctx_r1.pluralizeStepType(type_r4, ctx_r1.stats.stepsByType[type_r4]), " ");
} }
function AgentExecutionMonitorComponent_Conditional_38_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div", 18);
i0.ɵɵrepeaterCreate(1, AgentExecutionMonitorComponent_Conditional_38_For_2_Template, 2, 2, "span", 25, i0.ɵɵrepeaterTrackByIdentity);
i0.ɵɵelementEnd();
} if (rf & 2) {
const ctx_r1 = i0.ɵɵnextContext();
i0.ɵɵadvance();
i0.ɵɵrepeater(ctx_r1.getStepTypes());
} }
/**
* AgentExecutionMonitor Component
*
* A reusable component for visualizing AI agent execution flow in real-time or historically.
* Displays a hierarchical tree of execution steps with status, timing, and preview information.
*
* Features:
* - Real-time updates for live executions
* - Historical playback for completed executions
* - Collapsible tree structure
* - Step status indicators with animations
* - Execution statistics
* - Token usage and cost tracking
* - Error display
* - Input/output preview
*/
export class AgentExecutionMonitorComponent {
constructor(cdr) {
this.cdr = cdr;
this.mode = 'historical';
this.agentRun = null; // For historical mode
this.liveSteps = []; // For live mode streaming
this.autoExpand = true; // Auto-expand nodes in live mode
this.runId = null; // ID of the run (agent or prompt)
this.runType = 'agent'; // Type of run
this.viewRunClick = new EventEmitter();
// Store the currently rendered steps for UI state management
this.currentStep = null;
// Track component references for dynamic components
this.nodeComponentMap = new Map();
// UI state management - track expanded states separately from entities
this.expandedStates = new Map();
this.detailsExpandedStates = new Map();
this.stats = {
totalSteps: 0,
completedSteps: 0,
failedSteps: 0,
totalTokens: 0,
totalCost: 0,
stepsByType: {},
totalPrompts: 0
};
// User interaction tracking
this.userHasInteracted = false;
this.userHasScrolled = false;
this.isAutoScrolling = false;
this.lastScrollPosition = 0;
this.scrollThreshold = 50; // pixels from bottom to consider "at bottom"
// View initialization flag
this.viewInitialized = false;
// Track processed step IDs to avoid duplication
this.processedStepIds = new Set();
// Track which nodes were added in the current update
this.newlyAddedNodeIds = new Set();
// Temporary storage for parsed metadata
this.lastParsedAgentData = null;
this.lastParsedActionData = null;
this.destroy$ = new Subject();
}
ngOnChanges(changes) {
console.log('🔄 Execution Monitor ngOnChanges:', {
agentRunChanged: !!changes['agentRun'],
liveStepsChanged: !!changes['liveSteps'],
modeChanged: !!changes['mode'],
hasAgentRun: !!this.agentRun,
liveStepsCount: this.liveSteps?.length || 0
});
// Handle agent run changes (historical mode)
if (changes['agentRun'] && this.mode === 'historical') {
const oldRun = changes['agentRun'].previousValue;
const newRun = changes['agentRun'].currentValue;
console.log('📊 Agent run changed:', {
oldRunExists: !!oldRun,
newRunExists: !!newRun,
oldRunId: oldRun?.ID,
newRunId: newRun?.ID,
stepsCount: newRun?.Steps?.length
});
// Only clear if it's actually a different execution (different ID)
const isDifferentExecution = (!oldRun && newRun) ||
(oldRun && newRun && oldRun.ID !== newRun.ID) ||
(oldRun && !newRun);
if (isDifferentExecution) {
console.log('🗑️ Clearing for different agent run');
this.processedStepIds.clear();
this.newlyAddedNodeIds.clear();
this.expandedStates.clear();
this.detailsExpandedStates.clear();
this.clearNodeComponents();
this.currentStep = null;
}
// Always process the agent run data (will handle updates to existing run)
if (newRun) {
this.processAgentRun();
}
}
// Handle live steps changes (live mode)
if (changes['liveSteps'] && this.mode === 'live') {
const oldSteps = changes['liveSteps'].previousValue;
const newSteps = changes['liveSteps'].currentValue;
console.log('📊 Live steps changed:', {
oldCount: oldSteps?.length || 0,
newCount: newSteps?.length || 0
});
// Process live steps
this.processLiveSteps();
}
// Handle mode changes
if (changes['mode'] && !changes['mode'].firstChange) {
const previousMode = changes['mode'].previousValue;
const currentMode = changes['mode'].currentValue;
console.log('🔄 Mode changed:', { previousMode, currentMode });
// Clear everything when switching modes
this.processedStepIds.clear();
this.newlyAddedNodeIds.clear();
this.expandedStates.clear();
this.detailsExpandedStates.clear();
this.clearNodeComponents();
this.currentStep = null;
// Stop live updates when switching away from live mode
if (previousMode === 'live' && this.updateSubscription) {
this.updateSubscription.unsubscribe();
this.updateSubscription = undefined;
}
// Process data for the new mode
if (currentMode === 'historical' && this.agentRun) {
this.processAgentRun();
}
else if (currentMode === 'live') {
this.setupLiveUpdates();
}
}
}
ngAfterViewInit() {
this.viewInitialized = true;
console.log('🎯 View initialized, checking for pending data:', {
hasAgentRun: !!this.agentRun,
hasLiveSteps: this.liveSteps?.length > 0,
hasContainer: !!this.executionNodesContainer
});
// Initial setup for scroll behavior
if (this.mode === 'live') {
this.checkIfUserAtBottom();
}
// If we have data waiting to be rendered, render it now
if (this.mode === 'historical' && this.agentRun && this.nodeComponentMap.size === 0) {
console.log('⚡ Processing agent run after view init');
this.processAgentRun();
}
else if (this.mode === 'live' && this.liveSteps?.length > 0) {
console.log('⚡ Processing live steps after view init');
this.processLiveSteps();
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
if (this.updateSubscription) {
this.updateSubscription.unsubscribe();
this.updateSubscription = undefined;
}
this.clearNodeComponents();
}
/**
* Clear all dynamically created components
*/
clearNodeComponents() {
this.nodeComponentMap.forEach(ref => ref.destroy());
this.nodeComponentMap.clear();
if (this.executionNodesContainer) {
this.executionNodesContainer.clear();
}
}
/**
* Process agent run for historical mode
*/
processAgentRun() {
console.log('⚙️ Processing agent run:', {
hasAgentRun: !!this.agentRun,
stepsCount: this.agentRun?.Steps?.length || 0,
viewInitialized: this.viewInitialized,
hasContainer: !!this.executionNodesContainer
});
if (!this.agentRun || !this.viewInitialized || !this.executionNodesContainer) {
console.warn('⚠️ Cannot process agent run:', {
agentRun: !this.agentRun ? 'missing' : 'present',
viewInitialized: this.viewInitialized ? 'yes' : 'no',
container: !this.executionNodesContainer ? 'missing' : 'present'
});
return;
}
// Clear existing components
this.clearNodeComponents();
// Render the agent run steps
if (this.agentRun.Steps && this.agentRun.Steps.length > 0) {
console.log('🎨 Rendering', this.agentRun.Steps.length, 'steps');
this.renderSteps(this.agentRun.Steps, 0, []);
console.log('✅ Finished rendering, container now has', this.executionNodesContainer.length, 'components');
}
else {
console.warn('⚠️ No steps to render');
}
// Calculate statistics
this.calculateStats();
// Trigger change detection after rendering all components
this.cdr.detectChanges();
}
/**
* Process live steps for live mode
*/
processLiveSteps() {
console.log('⚙️ Processing live steps:', {
stepsCount: this.liveSteps?.length || 0,
viewInitialized: this.viewInitialized,
hasContainer: !!this.executionNodesContainer
});
if (!this.liveSteps || !this.viewInitialized || !this.executionNodesContainer) {
return;
}
// Append new steps without clearing existing ones
this.appendNewLiveSteps(this.liveSteps);
// Trigger change detection after appending live steps
this.cdr.detectChanges();
// Set up live update monitoring if not already set up
if (!this.updateSubscription) {
this.setupLiveUpdates();
}
}
/**
* Render steps recursively with proper hierarchy
*/
renderSteps(steps, depth, agentPath) {
console.log('🎨 Rendering steps:', {
count: steps.length,
depth,
agentPath
});
// Sort steps by StepNumber to ensure proper ordering
const sortedSteps = [...steps].sort((a, b) => (a.StepNumber || 0) - (b.StepNumber || 0));
for (const step of sortedSteps) {
// Skip if already processed in live mode
if (this.processedStepIds.has(step.ID)) {
continue;
}
// Create component for this step
this.createStepComponent(step, depth, agentPath);
// Mark as processed
this.processedStepIds.add(step.ID);
// Handle sub-agent recursion
if (step.StepType === 'Sub-Agent' && step.SubAgentRun?.Steps) {
const subAgentPath = [...agentPath, step.SubAgentRun.Agent || 'Sub-Agent'];
this.renderSteps(step.SubAgentRun.Steps, depth + 1, subAgentPath);
}
}
}
/**
* Create a component for a step
*/
createStepComponent(step, depth, agentPath) {
// Ensure container exists
if (!this.executionNodesContainer) {
console.error('❌ executionNodesContainer not available');
throw new Error('executionNodesContainer ViewContainerRef not initialized');
}
const componentRef = this.executionNodesContainer.createComponent(ExecutionNodeComponent);
const instance = componentRef.instance;
console.log('🔨 Creating component for step:', {
stepId: step.ID,
stepName: step.StepName,
depth,
containerLength: this.executionNodesContainer.length,
hostElement: componentRef.location.nativeElement
});
// Pass the step data with UI state information
instance.step = step;
instance.depth = depth;
instance.agentPath = agentPath;
instance.expanded = this.expandedStates.get(step.ID) || false;
instance.detailsExpanded = this.detailsExpandedStates.get(step.ID) || false;
// Subscribe to outputs
instance.toggleNode.subscribe(() => this.toggleStepExpansion(step));
instance.toggleDetails.subscribe(() => this.toggleStepDetails(step));
instance.userInteracted.subscribe(() => this.onUserInteraction());
// Store reference
this.nodeComponentMap.set(step.ID, componentRef);
// Log the component state after setup
console.log('✅ Component created and configured:', {
stepId: step.ID,
hasData: !!instance.step,
isAttached: componentRef.hostView.destroyed === false
});
return componentRef;
}
/**
* Toggle step expansion
*/
toggleStepExpansion(step) {
const currentState = this.expandedStates.get(step.ID) || false;
this.expandedStates.set(step.ID, !currentState);
this.userHasInteracted = true;
// Update the component
const componentRef = this.nodeComponentMap.get(step.ID);
if (componentRef) {
componentRef.instance.expanded = !currentState;
componentRef.changeDetectorRef.detectChanges();
}
this.cdr.markForCheck();
}
/**
* Toggle step details expansion
*/
toggleStepDetails(step) {
const currentState = this.detailsExpandedStates.get(step.ID) || false;
this.detailsExpandedStates.set(step.ID, !currentState);
// Update the component
const componentRef = this.nodeComponentMap.get(step.ID);
if (componentRef) {
componentRef.instance.detailsExpanded = !currentState;
componentRef.changeDetectorRef.detectChanges();
}
}
/**
* Get step type CSS class for styling
*/
getStepTypeClass(stepType) {
const typeMap = {
'Validation': 'validation',
'Prompt': 'prompt',
'Actions': 'action',
'Sub-Agent': 'sub-agent',
'Decision': 'decision',
'Chat': 'chat'
};
return typeMap[stepType] || 'prompt';
}
/**
* Get status CSS class for styling
*/
getStatusClass(status) {
const statusMap = {
'Pending': 'pending',
'Running': 'running',
'Completed': 'completed',
'Failed': 'failed',
'Cancelled': 'failed',
'Paused': 'pending'
};
return statusMap[status] || 'pending';
}
/**
* Helper function to safely convert a value to a preview string
*/
valueToPreviewString(value) {
if (value === null || value === undefined) {
return '';
}
if (typeof value === 'object') {
return JSON.stringify(value, null, 2);
}
return String(value);
}
/**
* Create a preview string from data
*/
createPreview(data) {
if (!data)
return undefined;
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
// Extract meaningful preview and store metadata
if (parsed.promptName)
return `Prompt: ${parsed.promptName}`;
if (parsed.actionName) {
// Store action metadata if available
this.lastParsedActionData = {
actionName: parsed.actionName,
actionId: parsed.actionId,
actionIconClass: parsed.actionIconClass
};
return `Action: ${parsed.actionName}`;
}
if (parsed.subAgentName) {
// Store agent metadata if available
this.lastParsedAgentData = {
agentName: parsed.subAgentName,
agentId: parsed.subAgentId || parsed.agentId,
agentIconClass: parsed.subAgentIconClass || parsed.agentIconClass,
agentLogoURL: parsed.subAgentLogoURL || parsed.agentLogoURL
};
return `Sub-agent: ${parsed.subAgentName}`;
}
if (parsed.message)
return this.valueToPreviewString(parsed.message);
if (parsed.userMessage)
return this.valueToPreviewString(parsed.userMessage);
// Show action results clearly
if (parsed.actionResult) {
const result = parsed.actionResult;
let preview = '';
if (result.success !== undefined) {
preview += `Success: ${result.success}\n`;
}
if (result.resultCode) {
preview += `Result Code: ${result.resultCode}\n`;
}
if (result.message) {
preview += `Message: ${this.valueToPreviewString(result.message)}\n`;
}
if (result.result) {
preview += `Result: ${typeof result.result === 'object' ? JSON.stringify(result.result, null, 2) : result.result}`;
}
return preview.trim();
}
// Legacy action result format
if (parsed.result) {
return this.valueToPreviewString(parsed.result);
}
// Show prompt results
if (parsed.promptResult) {
const result = parsed.promptResult;
let preview = '';
if (result.success !== undefined) {
preview += `Success: ${result.success}\n`;
}
if (result.content) {
preview += `Content: ${this.valueToPreviewString(result.content)}`;
}
return preview;
}
// Fallback to stringified preview
const str = JSON.stringify(parsed, null, 2);
return str.length > 500 ? str.substring(0, 500) + '...' : str;
}
catch {
return typeof data === 'string' ? data : JSON.stringify(data);
}
}
/**
* Extract token count from output data
*/
extractTokens(data) {
if (!data)
return undefined;
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
return parsed.promptResult?.tokensUsed || parsed.tokensUsed;
}
catch {
return undefined;
}
}
/**
* Extract cost from output data
*/
extractCost(data) {
if (!data)
return undefined;
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
return parsed.promptResult?.totalCost || parsed.totalCost;
}
catch {
return undefined;
}
}
/**
* Calculate duration from timestamps
*/
calculateDuration(start, end) {
if (!start || !end)
return undefined;
const startTime = new Date(start).getTime();
const endTime = new Date(end).getTime();
return endTime - startTime;
}
/**
* Set up live updates for real-time execution
*/
setupLiveUpdates() {
console.log('⚡ Setting up live updates');
// Process initial live steps if any
if (this.liveSteps && this.liveSteps.length > 0) {
this.appendNewLiveSteps(this.liveSteps);
}
// Set up interval to check for updates (if not already set up)
if (this.mode === 'live' && !this.updateSubscription) {
this.updateSubscription = interval(500)
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
// Update current step indicator
this.updateCurrentStep();
// Recalculate stats periodically
this.calculateStats();
});
}
}
/**
* Update current step indicator
*/
updateCurrentStep() {
// Find the first running step from all rendered components
let runningStep = null;
this.nodeComponentMap.forEach((componentRef, stepId) => {
const step = componentRef.instance.step;
if (step && step.Status === 'Running' && !runningStep) {
runningStep = step;
}
});
this.currentStep = runningStep;
this.cdr.markForCheck();
}
/**
* Mark previously new nodes as completed
*/
markPreviouslyNewNodesAsComplete() {
// Mark ALL running steps as completed (not just previously new ones)
// This ensures only the latest step shows as running
this.nodeComponentMap.forEach((componentRef, stepId) => {
const step = componentRef.instance.step;
if (step.Status === 'Running') {
// Create a copy with updated status
const updatedStep = Object.assign({}, step, { Status: 'Completed' });
componentRef.instance.step = updatedStep;
componentRef.changeDetectorRef.detectChanges();
}
});
}
/**
* Ensure only the last step in the execution order shows as running
*/
ensureOnlyLastStepIsRunning() {
// Find the last step by step number
let lastRunningStep = null;
this.nodeComponentMap.forEach((componentRef, stepId) => {
const step = componentRef.instance.step;
if (step.Status === 'Running') {
const stepNumber = step.StepNumber || 0;
if (!lastRunningStep || stepNumber > lastRunningStep.stepNumber) {
lastRunningStep = { stepId, stepNumber, componentRef };
}
}
});
// Set override display status for all running steps
this.nodeComponentMap.forEach((componentRef, stepId) => {
const step = componentRef.instance.step;
if (step.Status === 'Running') {
// Override display to show as completed, except for the last one
if (lastRunningStep && stepId === lastRunningStep.stepId) {
componentRef.instance.overrideDisplayStatus = 'Running';
}
else {
componentRef.instance.overrideDisplayStatus = 'Completed';
}
componentRef.changeDetectorRef.detectChanges();
}
});
}
/**
* Calculate execution statistics
*/
calculateStats() {
this.stats = {
totalSteps: 0,
completedSteps: 0,
failedSteps: 0,
totalTokens: 0,
totalCost: 0,
stepsByType: {},
totalDuration: 0,
totalPrompts: 0
};
// Use token data from agent run if available
if (this.agentRun) {
this.stats.totalTokens = this.agentRun.TotalTokensUsed || 0;
this.stats.totalCost = this.agentRun.TotalCost || 0;
// Calculate duration
if (this.agentRun.StartedAt && this.agentRun.CompletedAt) {
this.stats.totalDuration = new Date(this.agentRun.CompletedAt).getTime() -
new Date(this.agentRun.StartedAt).getTime();
}
// Count steps recursively
if (this.agentRun.Steps) {
this.countSteps(this.agentRun.Steps);
}
}
console.log('📈 Stats calculated:', {
totalSteps: this.stats.totalSteps,
totalTokens: this.stats.totalTokens,
totalCost: this.stats.totalCost
});
}
/**
* Count steps recursively
*/
countSteps(steps) {
for (const step of steps) {
this.stats.totalSteps++;
// Map to display types for consistency
const displayType = this.getStepTypeClass(step.StepType);
this.stats.stepsByType[displayType] = (this.stats.stepsByType[displayType] || 0) + 1;
if (step.Status === 'Completed')
this.stats.completedSteps++;
if (step.Status === 'Failed' || step.Status === 'Cancelled')
this.stats.failedSteps++;
// Count prompts
if (step.StepType === 'Prompt') {
this.stats.totalPrompts++;
}
// Recurse for sub-agents
if (step.StepType === 'Sub-Agent' && step.SubAgentRun?.Steps) {
this.countSteps(step.SubAgentRun.Steps);
}
}
}
/**
* Handle scroll events
*/
onScroll(event) {
if (this.isAutoScrolling) {
// Ignore scroll events triggered by auto-scrolling
return;
}
const element = event.target;
const isAtBottom = this.isScrolledToBottom(element);
// If user scrolled up from bottom, mark as user interaction
if (!isAtBottom && this.lastScrollPosition !== element.scrollTop) {
this.userHasScrolled = true;
this.userHasInteracted = true;
}
// If user scrolled back to bottom, reset interaction flags
if (isAtBottom) {
this.userHasScrolled = false;
this.userHasInteracted = false;
}
this.lastScrollPosition = element.scrollTop;
}
/**
* Handle user clicks (interaction detection)
*/
onUserInteraction() {
// This is called when user clicks anywhere in the execution tree
// The toggleNode method will set userHasInteracted for specific interactions
}
/**
* Check if scroll is at bottom
*/
isScrolledToBottom(element) {
const threshold = this.scrollThreshold;
return element.scrollHeight - element.scrollTop - element.clientHeight < threshold;
}
/**
* Check if user is at bottom (for initial setup)
*/
checkIfUserAtBottom() {
if (!this.executionTreeContainer)
return;
const element = this.executionTreeContainer.nativeElement;
if (this.isScrolledToBottom(element)) {
this.userHasScrolled = false;
}
}
/**
* Auto-scroll to bottom if user hasn't interacted
*/
autoScrollToBottom() {
if (!this.executionTreeContainer || this.userHasInteracted || this.userHasScrolled) {
return;
}
const element = this.executionTreeContainer.nativeElement;
this.isAutoScrolling = true;
// Use smooth scrolling for better UX
element.scrollTo({
top: element.scrollHeight,
behavior: 'smooth'
});
// Reset auto-scrolling flag after animation
setTimeout(() => {
this.isAutoScrolling = false;
}, 300);
}
/**
* Format agent path for display
*/
formatAgentPath(path) {
return path.join(' → ');
}
/**
* Format duration for display
*/
formatDuration(ms) {
if (ms < 1000)
return `${ms}ms`;
if (ms < 60000)
return `${(ms / 1000).toFixed(1)}s`;
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
return `${minutes}m ${seconds}s`;
}
/**
* Get step types for display
*/
getStepTypes() {
return Object.keys(this.stats.stepsByType).sort();
}
/**
* Pluralize step type based on count
*/
pluralizeStepType(type, count) {
if (count === 1) {
return type;
}
// Handle special cases
switch (type.toLowerCase()) {
case 'analysis':
return 'Analyses';
case 'summary':
return 'Summaries';
default:
// Default pluralization - just add 's'
return type + 's';
}
}
/**
* Format markdown content for display
*/
formatMarkdown(markdown) {
// Basic markdown formatting
// This is a simple implementation - you might want to use a proper markdown library
let html = markdown;
// Headers
html = html.replace(/^### (.*$)/gim, '<h4>$1</h4>');
html = html.replace(/^## (.*$)/gim, '<h3>$1</h3>');
html = html.replace(/^# (.*$)/gim, '<h2>$1</h2>');
// Bold
html = html.replace(/\*\*(.*)\*\*/g, '<strong>$1</strong>');
// Italic
html = html.replace(/\*(.*)\*/g, '<em>$1</em>');
// Detect and linkify URLs
// This regex matches URLs starting with http://, https://, or www.
const urlRegex = /(?:https?:\/\/|www\.)[^\s<]+/gi;
html = html.replace(urlRegex, (url) => {
// Ensure the URL has a protocol
const href = url.startsWith('http') ? url : `https://${url}`;
// Truncate long URLs for display
const displayUrl = url.length > 50 ? url.substring(0, 50) + '...' : url;
return `<a href="${href}" target="_blank" rel="noopener noreferrer" style="color: #2196f3; text-decoration: underline; font-size: inherit;">${displayUrl}</a>`;
});
// Line breaks
html = html.replace(/\n/g, '<br>');
// Lists
html = html.replace(/^\* (.+)$/gim, '<li>$1</li>');
html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
// Code blocks
html = html.replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>');
// Inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
return html;
}
/**
* Append new live steps without re-rendering entire tree
*/
appendNewLiveSteps(newSteps) {
if (!newSteps || newSteps.length === 0 || !this.viewInitialized || !this.executionNodesContainer) {
return;
}
console.log('📝 Appending', newSteps.length, 'live steps');
// Sort steps by StepNumber
const sortedSteps = [...newSteps].sort((a, b) => (a.StepNumber || 0) - (b.StepNumber || 0));
// Mark previously new steps as completed
if (this.newlyAddedNodeIds.size > 0) {
this.markPreviouslyNewNodesAsComplete();
this.newlyAddedNodeIds.clear();
}
let hasNewSteps = false;
for (const step of sortedSteps) {
// Skip if already processed
if (this.processedStepIds.has(step.ID)) {
// Update existing step if status changed
const componentRef = this.nodeComponentMap.get(step.ID);
if (componentRef && componentRef.instance.step.Status !== step.Status) {
componentRef.instance.step = step;
componentRef.changeDetectorRef.detectChanges();
}
continue;
}
// Mark as processed and newly added
this.processedStepIds.add(step.ID);
this.newlyAddedNodeIds.add(step.ID);
hasNewSteps = true;
// Determine depth based on parent hierarchy
const depth = this.calculateStepDepth(step);
const agentPath = this.buildAgentPath(step);
// Create component for new step
this.createStepComponent(step, depth, agentPath);
// Handle sub-agent steps recursively
if (step.StepType === 'Sub-Agent' && step.SubAgentRun?.Steps) {
const subAgentPath = [...agentPath, step.SubAgentRun.Agent || 'Sub-Agent'];
this.renderSteps(step.SubAgentRun.Steps, depth + 1, subAgentPath);
}
}
if (hasNewSteps) {
// After processing all new steps, ensure only the last one shows as running
this.ensureOnlyLastStepIsRunning();
this.updateCurrentStep();
this.calculateStats();
// Force change detection to ensure new components are rendered
this.cdr.detectChanges();
// Auto-scroll if user hasn't interacted
setTimeout(() => {
this.autoScrollToBottom();
}, 100);
}
}
/**
* Calculate step depth based on parent hierarchy
*/
calculateStepDepth(step) {
// For now, return 0 for top-level steps
// In live mode, depth information should come from the streaming data
return 0;
}
/**
* Build agent path for a step
*/
buildAgentPath(step) {
// In live mode, agent path should come from streaming data
// For now, return empty array
return [];
}
/**
* Check if execution is complete
*/
isExecutionComplete() {
return this.stats.completedSteps > 0 &&
this.stats.completedSteps === this.stats.totalSteps - this.stats.failedSteps;
}
/**
* Handle view run button click
*/
onViewRunClick() {
if (this.agentRun) {
this.viewRunClick.emit({ runId: this.agentRun.ID, runType: 'agent' });
}
}
/**
* Expands all nodes that have sub-agent children
*/
expandAllNodes() {
console.log('🔄 Auto-expanding nodes with sub-agents');
// Expand all steps that have sub-agent runs
this.nodeComponentMap.forEach((componentRef, stepId) => {
const step = componentRef.instance.step;
if (step.StepType === 'Sub-Agent' && step.SubAgentRun?.Steps?.length) {
this.expandedStates.set(stepId, true);
componentRef.instance.expanded = true;
componentRef.changeDetectorRef.detectChanges();
console.log(`Expanded sub-agent: ${step.StepName}`);
}
});
// Trigger change detection
this.cdr.markForCheck();
this.cdr.detectChanges();
// Scroll to top after expansion
setTimeout(() => {
this.scrollToTop();
}, 100);
}
/**
* Scroll the execution tree to the top
*/
scrollToTop() {
if (this.executionTreeContainer) {
const element = this.executionTreeContainer.nativeElement;
element.scrollTo({
top: 0,
behavior: 'smooth'
});
}
}
/**
* Check if there is content to display
*/
hasContent() {
return this.nodeComponentMap.size > 0;
}
static { this.ɵfac = function AgentExecutionMonitorComponent_Factory(t) { return new (t || AgentExecutionMonitorComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); }; }
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: AgentExecutionMonitorComponent, selectors: [["mj-agent-execution-monitor"]], viewQuery: function AgentExecutionMonitorComponent_Query(rf, ctx) { if (rf & 1) {
i0.ɵɵviewQuery(_c0, 5);
i0.ɵɵviewQuery(_c1, 5, ViewContainerRef);
} if (rf & 2) {
let _t;
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.executionTreeContainer = _t.first);
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.executionNodesContainer = _t.first);
} }, inputs: { mode: "mode", agentRun: "agentRun", liveSteps: "liveSteps", autoExpand: "autoExpand", runId: "runId", runType: "runType" }, outputs: { viewRunClick: "viewRunClick" }, features: [i0.ɵɵNgOnChangesFeature], decls: 39, vars: 16, consts: [["executionTreeContainer", ""], ["executionNodesContainer", ""], [1, "execution-monitor"], [1, "monitor-header"], [1, "header-title"], [1, "fa-solid", "fa-diagram-project"], [1, "live-indicator"], [1, "current-status"], [1, "view-run-btn"], [1, "execution-tree", 3, "scroll", "click"], [1, "nodes-container"], [1, "empty-state"], [1, "monitor-footer"], [1, "stats-grid"], [1, "stat-item"], [1, "stat-label"], [1, "stat-value"], [1, "failed-count"], [1, "step-types"], [1, "pulse"], [1, "status-label"], [1, "step-name"], [1, "view-run-btn", 3, "click"], [1, "fa-solid", "fa-external-link-alt"], [1, "fa-solid", "fa-hourglass-start"], [1, "type-badge"]], template: function AgentExecutionMonitorComponent_Template(rf, ctx) { if (rf & 1) {
const _r1 = i0.ɵɵgetCurrentView();
i0.ɵɵelementStart(0, "div", 2)(1, "div", 3)(2, "div", 4);
i0.ɵɵelement(3, "i", 5);
i0.ɵɵelementStart(4, "span");
i0.ɵɵtext(5, "Execution Monitor");
i0.ɵɵelementEnd();
i0.ɵɵtemplate(6, AgentExecutionMonitorComponent_Conditional_6_Template, 3, 0, "span", 6);
i0.ɵɵelementEnd();
i0.ɵɵtemplate(7, AgentExecutionMonitorComponent_Conditional_7_Template, 5, 1, "div", 7)(8, AgentExecutionMonitorComponent_Conditional_8_Template, 3, 1, "button", 8);
i0.ɵɵelementEnd();
i0.ɵɵelementStart(9, "div", 9, 0);
i0.ɵɵlistener("scroll", function AgentExecutionMonitorComponent_Template_div_scroll_9_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onScroll($event)); })("click", function AgentExecutionMonitorComponent_Template_div_click_9_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onUserInteraction()); });
i0.ɵɵelement(11, "div", 10, 1);
i0.ɵɵtemplate(13, AgentExecutionMonitorComponent_Conditional_13_Template, 4, 0, "div", 11);
i0.ɵɵelementEnd();
i0.ɵɵelementStart(14, "div", 12)(15, "div", 13)(16, "div", 14)(17, "span", 15);
i0.ɵɵtext(18, "Steps");
i0.ɵɵelementEnd();
i0.ɵɵelementStart(19, "span", 16);
i0.ɵɵtext(20);
i0.ɵɵtemplate(21, AgentExecutionMonitorComponent_Conditional_21_Template, 2, 1, "span", 17);
i0.ɵɵelementEnd()();
i0.ɵɵelementStart(22, "div", 14)(23, "span", 15);
i0.ɵɵtext(24, "Prompts");
i0.ɵɵelementEnd();
i0.ɵɵelementStart(25, "span", 16);
i0.ɵɵtext(26);
i0.ɵɵelementEnd()();
i0.ɵɵelementStart(27, "div", 14)(28, "span", 15);
i0.ɵɵtext(29, "Tokens");
i0.ɵɵelementEnd();
i0.ɵɵelementStart(30, "span", 16);
i0.ɵɵtext(31);
i0.ɵɵelementEnd()();
i0.ɵɵelementStart(32, "div", 14)(33, "span", 15);
i0.ɵɵtext(34, "Cost");
i0.ɵɵelementEnd();
i0.ɵɵelementStart(35, "span", 16);
i0.ɵɵtext(36);
i0.ɵɵelementEnd()();
i0.ɵɵtemplate(37, AgentExecutionMonitorComponent_Conditional_37_Template, 5, 1, "div", 14);
i0.ɵɵelementEnd();
i0.ɵɵtemplate(38, AgentExecutionMonitorComponent_Conditional_38_Template, 3, 0, "div", 18);
i0.ɵɵelementEnd()();
} if (rf & 2) {
i0.ɵɵclassProp("live-mode", ctx.mode === "live");
i0.ɵɵadvance(6);
i0.ɵɵconditional(ctx.mode === "live" ? 6 : -1);
i0.ɵɵadvance();
i0.ɵɵconditional(ctx.currentStep && ctx.mode === "live" ? 7 : -1);
i0.ɵɵadvance();
i0.ɵɵconditional(ctx.mode === "historical" && ctx.agentRun && ctx.isExecutionComplete() ? 8 : -1);
i0.ɵɵadvance();
i0.ɵɵclassProp("has-content", ctx.hasContent());
i0.ɵɵadvance(4);
i0.ɵɵconditional(!ctx.agentRun && ctx.liveSteps.length === 0 ? 13 : -1);
i0.ɵɵadvance(7);
i0.ɵɵtextInterpolate2(" ", ctx.stats.completedSteps, "/", ctx.stats.totalSteps, " ");
i0.ɵɵadvance();
i0.ɵɵconditional(ctx.stats.failedSteps > 0 ? 21 : -1);
i0.ɵɵadvance(5);
i0.ɵɵtextInterpolate(ctx.stats.totalPrompts);
i0.ɵɵadvance(5);
i0.ɵɵtextInterpolate(ctx.stats.totalTokens.toLocaleString());
i0.ɵɵadvance(5);
i0.ɵɵtextInterpolate1("$", ctx.stats.totalCost.toFixed(4), "");
i0.ɵɵadvance();
i0.ɵɵconditional(ctx.stats.totalDuration ? 37 : -1);
i0.ɵɵadvance();
i0.ɵɵconditional(ctx.getStepTypes().length > 0 ? 38 : -1);
} }, styles: ["[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n }\n \n .execution-monitor[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n flex: 1;\n background: #fff;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n overflow: hidden;\n }\n\n \n\n .monitor-header[_ngcontent-%COMP%] {\n padding: 16px;\n background: #f8f9fa;\n border-bottom: 1px solid #e0e0e0;\n }\n\n .header-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n margin-bottom: 8px;\n }\n\n .header-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #2196f3;\n }\n\n .live-indicator[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n background: #ff4444;\n color: white;\n border-radius: 4px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.5px;\n }\n\n .pulse[_ngcontent-%COMP%] {\n display: inline-block;\n width: 8px;\n height: 8px;\n background: white;\n border-radius: 50%;\n animation: _ngcontent-%COMP%_pulse 2s infinite;\n }\n\n @keyframes _ngcontent-%COMP%_pulse {\n 0% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.5; transform: scale(0.8); }\n 100% { opacity: 1; transform: scale(1); }\n }\n\n .current-status[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #666;\n }\n\n .status-label[_ngcontent-%COMP%] {\n font-weight: 500;\n }\n\n .agent-path[_ngcontent-%COMP%] {\n color: #999;\n }\n\n .step-name[_ngcontent-%COMP%] {\n color: #1a1a1a;\n font-weight: 500;\n }\n\n \n\n .execution-tree[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n overflow-x: auto;\n padding: 16px;\n min-height: 0;\n }\n \n .nodes-container[_ngcontent-%COMP%] {\n \n\n }\n\n .empty-state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: #999;\n text-align: center;\n }\n\n .empty-state[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n }\n\n .empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 14px;\n }\n\n \n\n .tree-node[_ngcontent-%COMP%] {\n margin-bottom: 4px;\n }\n\n \n\n .monitor-footer[_ngcontent-%COMP%] {\n padding: 16px;\n background: #f8f9fa;\n border-top: 1px solid #e0e0e0;\n }\n\n .stats-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));\n gap: 16px;\n margin-bottom: 12px;\n }\n\n .stat-item[_ngcontent-%COMP%] {\n text-align: center;\n }\n\n .stat-label[_ngcontent-%COMP%] {\n display: block;\n font-size: 11px;\n color: #666;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-bottom: 4px;\n }\n\n .stat-value[_ngcontent-%COMP%] {\n display: block;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n }\n\n .failed-count[_ngcontent-%COMP%] {\n color: #f44336;\n font-size: 12px;\n font-weight: normal;\n }\n\n .step-types[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n justify-content: center;\n }\n\n .type-badge[_ngcontent-%COMP%] {\n display: inline-block;\n padding: 4px 8px;\n background: #e3f2fd;\n color: #1976d2;\n border-radius: 4px;\n font-size: 12px;\n font-weight: 500;\n }\n \n \n\n .view-run-btn[_ngcontent-%COMP%] {\n background: #2196f3;\n color: white;\n border: none;\n padding: 6px 12px;\n border-radius: 4px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n transition: all 0.2s ease;\n margin-left: auto;\n }\n \n .view-run-btn[_ngcontent-%COMP%]:hover {\n background: #1976d2;\n transform: translateY(-1px);\n box-shadow: 0 2px 4px rgba(0,0,0,0.2);\n }\n \n .view-run-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 12px;\n }\n\n \n\n @media (max-width: 768px) {\n .monitor-header[_ngcontent-%COMP%] {\n padding: 12px;\n }\n\n .execution-tree[_ngcontent-%COMP%] {\n padding: 12px;\n }\n }"], changeDetection: 0 }); }
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AgentExecutionMonitorComponent, [{
type: Component,
args: [{ selector: 'mj-agent-execution-monitor', changeDetection: ChangeDetectionStrategy.OnPush, template: `
<div class="execution-monitor" [class.live-mode]="mode === 'live'">
<!-- Header -->
<div class="monitor-header">
<div class="header-title">
<i class="fa-solid fa-diagram-project"></i>
<span>Execution Monitor</span>
@if (mode === 'live') {
<span class="live-indicator">
<span class="pulse"></span>
LIVE
</span>
}
</div>
@if (currentStep && mode === 'live') {
<div class="current-status">
<span class="status-label">Current:</span>
<span class="step-name">{{ currentStep.StepName }}</span>
</div>
}