UNPKG

@memberjunction/ng-ai-test-harness

Version:

MemberJunction AI Test Harness - A reusable component for testing AI agents and prompts with beautiful UX

1,060 lines 61 kB
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 { UUIDsEqual } from '@memberjunction/global'; 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 && !UUIDsEqual(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(__ngFactoryType__) { return new (__ngFactoryType__ || 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)(_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" }, standalone: false, 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.ɵɵconditionalCreate(6, AgentExecutionMonitorComponent_Conditional_6_Template, 3, 0, "span", 6); i0.ɵɵelementEnd(); i0.ɵɵconditionalCreate(7, AgentExecutionMonitorComponent_Conditional_7_Template, 5, 1, "div", 7); i0.ɵɵconditionalCreate(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.ɵɵconditionalCreate(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.ɵɵconditionalCreate(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.ɵɵconditionalCreate(37, AgentExecutionMonitorComponent_Conditional_37_Template, 5, 1, "div", 14); i0.ɵɵelementEnd(); i0.ɵɵconditionalCreate(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: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n overflow: hidden;\n }\n\n \n\n .monitor-header[_ngcontent-%COMP%] {\n padding: 16px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\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: var(--mj-text-primary);\n margin-bottom: 8px;\n }\n\n .header-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n }\n\n .live-indicator[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n background: var(--mj-status-error);\n color: var(--mj-text-inverse);\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: var(--mj-text-secondary);\n }\n\n .status-label[_ngcontent-%COMP%] {\n font-weight: 500;\n }\n\n .agent-path[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n }\n\n .step-name[_ngcontent-%COMP%] {\n color: var(--mj-text-primary);\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: var(--mj-text-muted);\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: var(--mj-bg-surface-card);\n border-top: 1px solid var(--mj-border-default);\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: var(--mj-text-secondary);\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: var(--mj-text-primary);\n }\n\n .failed-count[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\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: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n border-radius: 4px;\n font-size: 12px;\n font-weight: 500;\n }\n\n \n\n .view-run-btn[_ngcontent-%COMP%] {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\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: var(--mj-brand-primary-hover);\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: [{ standalone: false, 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