UNPKG

@memberjunction/ng-ai-test-harness

Version:

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

410 lines (408 loc) 24.5 kB
import { Component, Input, Output, EventEmitter, ViewChild, ChangeDetectorRef, inject } from '@angular/core'; import { BaseAngularComponent } from '@memberjunction/ng-base-types'; import { AITestHarnessComponent } from './ai-test-harness.component'; import * as i0 from "@angular/core"; import * as i1 from "@memberjunction/ng-ui-components"; import * as i2 from "@memberjunction/ng-shared-generic"; import * as i3 from "./ai-test-harness.component"; const _c0 = ["mjWindow"]; function TestHarnessCustomWindowComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelement(0, "img", 3); } if (rf & 2) { const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵproperty("src", ctx_r1.Agent == null ? null : ctx_r1.Agent.LogoURL, i0.ɵɵsanitizeUrl); } } function TestHarnessCustomWindowComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelement(0, "i", 4); } } function TestHarnessCustomWindowComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelement(0, "i", 5); } } function TestHarnessCustomWindowComponent_Conditional_17_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelementStart(0, "div", 14); i0.ɵɵelement(1, "mj-loading", 17); i0.ɵɵelementEnd(); } if (rf & 2) { const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵadvance(); i0.ɵɵproperty("text", "Loading " + (ctx_r1.Mode === "agent" ? "AI Agent" : "AI Prompt") + "..."); } } function TestHarnessCustomWindowComponent_Conditional_18_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelementStart(0, "div", 15); i0.ɵɵelement(1, "i", 18); i0.ɵɵelementStart(2, "p"); i0.ɵɵtext(3); i0.ɵɵelementEnd()(); } if (rf & 2) { const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵadvance(3); i0.ɵɵtextInterpolate(ctx_r1.Error); } } function TestHarnessCustomWindowComponent_Conditional_19_Template(rf, ctx) { if (rf & 1) { const _r3 = i0.ɵɵgetCurrentView(); i0.ɵɵelementStart(0, "mj-ai-test-harness", 19); i0.ɵɵlistener("runOpened", function TestHarnessCustomWindowComponent_Conditional_19_Template_mj_ai_test_harness_runOpened_0_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnRunOpened($event)); }); i0.ɵɵelementEnd(); } if (rf & 2) { const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵproperty("entity", ctx_r1.Agent || ctx_r1.Prompt || null)("mode", ctx_r1.Mode)("isVisible", true); } } export class TestHarnessCustomWindowComponent extends BaseAngularComponent { constructor() { super(...arguments); this.Data = {}; this.CloseWindow = new EventEmitter(); this.MinimizeWindow = new EventEmitter(); this.RestoreWindow = new EventEmitter(); this.ExecutionStateChange = new EventEmitter(); this.WindowTitle = 'AI Test Harness'; this.WindowVisible = false; this.Width = 1200; this.Height = 800; this.WindowTop = 100; this.WindowLeft = 100; this.Loading = true; this.Error = ''; this.WindowState = 'default'; this.IsMaximized = false; this.IsMinimized = false; // Store original dimensions for restore this.originalWidth = 1200; this.originalHeight = 800; this.originalTop = 100; this.originalLeft = 100; this.Mode = 'agent'; this.metadata = this.ProviderToUse; this.executionCheckInterval = null; this.cdr = inject(ChangeDetectorRef); } ngOnInit() { // Set window dimensions this.Width = this.convertToNumber(this.Data.width) || 1200; this.Height = this.convertToNumber(this.Data.height) || 800; // Store original dimensions this.originalWidth = this.Width; this.originalHeight = this.Height; // Calculate centered position const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; this.WindowLeft = Math.max(0, (viewportWidth - this.Width) / 2); this.WindowTop = Math.max(0, (viewportHeight - this.Height) / 2); // Store original position this.originalLeft = this.WindowLeft; this.originalTop = this.WindowTop; // Determine mode this.Mode = this.Data.mode || (this.Data.promptId || this.Data.prompt ? 'prompt' : 'agent'); // Set initial title from data if available if (this.Data.title) { this.WindowTitle = this.Data.title; } else if (this.Data.agent) { this.WindowTitle = `Test: ${this.Data.agent.Name}`; } else if (this.Data.prompt) { this.WindowTitle = `Test: ${this.Data.prompt.Name}`; } // Show window now that position is calculated this.WindowVisible = true; // Load entity this.LoadEntity(); } async LoadEntity() { try { if (this.Mode === 'agent') { await this.loadAgent(); } else { await this.loadPrompt(); } this.Loading = false; this.cdr.detectChanges(); } catch (err) { this.Error = err instanceof Error ? err.message : 'Failed to load entity'; this.Loading = false; this.cdr.detectChanges(); } } async loadAgent() { if (this.Data.agent) { this.Agent = this.Data.agent; this.WindowTitle = this.Data.title || `Test: ${this.Agent.Name}`; } else if (this.Data.agentId) { const agentEntity = await this.metadata.GetEntityObject('MJ: AI Agents'); await agentEntity.Load(this.Data.agentId); if (agentEntity.IsSaved) { this.Agent = agentEntity; this.WindowTitle = this.Data.title || `Test: ${this.Agent.Name}`; } else { throw new Error('Agent not found'); } } else { throw new Error('No agent provided'); } } async loadPrompt() { if (this.Data.prompt) { this.Prompt = this.Data.prompt; this.WindowTitle = this.Data.title || `Test: ${this.Prompt.Name}`; } else if (this.Data.promptId) { const promptEntity = await this.metadata.GetEntityObject('MJ: AI Prompts'); await promptEntity.Load(this.Data.promptId); if (promptEntity.IsSaved) { this.Prompt = promptEntity; this.WindowTitle = this.Data.title || `Test: ${this.Prompt.Name}`; } else { throw new Error('Prompt not found'); } } else { throw new Error('No prompt provided'); } } OnClose() { this.CloseWindow.emit(); } CloseButtonClick() { this.OnClose(); } OnStateChange(state) { this.WindowState = state; this.IsMaximized = state === 'maximized'; } OnWindowResize() { // Window was resized — no special handling needed as mj-window tracks dimensions internally } OnRunOpened(_event) { // Auto-minimize the test harness window when a run is opened this.Minimize(); } Minimize() { if (!this.IsMinimized) { // Store current dimensions before minimizing this.originalWidth = this.Width; this.originalHeight = this.Height; this.originalTop = this.WindowTop; this.originalLeft = this.WindowLeft; // Hide the window when minimized (dock will show icon) this.IsMinimized = true; this.WindowVisible = false; this.MinimizeWindow.emit(); this.cdr.detectChanges(); } } RestoreFromMinimized() { this.Width = this.originalWidth; this.Height = this.originalHeight; this.WindowTop = this.originalTop; this.WindowLeft = this.originalLeft; this.WindowState = 'default'; this.IsMinimized = false; this.IsMaximized = false; this.WindowVisible = true; this.cdr.detectChanges(); // Emit restore event this.RestoreWindow.emit(); } ToggleMaximize() { if (this.IsMinimized) { // First restore from minimized, then maximize this.RestoreFromMinimized(); setTimeout(() => { this.WindowState = 'maximized'; this.IsMaximized = true; this.cdr.detectChanges(); }, 100); } else { this.WindowState = this.IsMaximized ? 'default' : 'maximized'; this.IsMaximized = !this.IsMaximized; } } convertToNumber(value) { if (!value) return undefined; if (typeof value === 'number') return value; // Handle percentage values if (value.endsWith('vw') || value.endsWith('vh')) { const percentage = parseFloat(value) / 100; if (value.endsWith('vw')) { return window.innerWidth * percentage; } else { return window.innerHeight * percentage; } } // Handle pixel values if (value.endsWith('px')) { return parseFloat(value); } // Try to parse as number const parsed = parseFloat(value); return isNaN(parsed) ? undefined : parsed; } ngAfterViewInit() { // Set up execution tracking this.setupExecutionTracking(); } setupExecutionTracking() { // Use a timer to check the test harness execution state if (this.TestHarness) { let lastExecutingState = false; this.executionCheckInterval = setInterval(() => { if (this.TestHarness && this.TestHarness.isExecuting !== lastExecutingState) { lastExecutingState = this.TestHarness.isExecuting; this.ExecutionStateChange.emit({ isExecuting: lastExecutingState }); } }, 100); } } ngOnDestroy() { if (this.executionCheckInterval) { clearInterval(this.executionCheckInterval); } } static { this.ɵfac = /*@__PURE__*/ (() => { let ɵTestHarnessCustomWindowComponent_BaseFactory; return function TestHarnessCustomWindowComponent_Factory(__ngFactoryType__) { return (ɵTestHarnessCustomWindowComponent_BaseFactory || (ɵTestHarnessCustomWindowComponent_BaseFactory = i0.ɵɵgetInheritedFactory(TestHarnessCustomWindowComponent)))(__ngFactoryType__ || TestHarnessCustomWindowComponent); }; })(); } static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: TestHarnessCustomWindowComponent, selectors: [["mj-test-harness-custom-window"]], viewQuery: function TestHarnessCustomWindowComponent_Query(rf, ctx) { if (rf & 1) { i0.ɵɵviewQuery(_c0, 5)(AITestHarnessComponent, 5); } if (rf & 2) { let _t; i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.MjWindow = _t.first); i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.TestHarness = _t.first); } }, inputs: { Data: "Data" }, outputs: { CloseWindow: "CloseWindow", MinimizeWindow: "MinimizeWindow", RestoreWindow: "RestoreWindow", ExecutionStateChange: "ExecutionStateChange" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 20, vars: 19, consts: [["mjWindow", ""], [3, "Close", "StateChange", "Resize", "Visible", "Title", "Width", "Height", "Top", "Left", "MinWidth", "MinHeight", "Draggable", "Resizable", "State"], [1, "window-title"], ["alt", "Agent logo", 1, "title-logo", 3, "src"], [1, "fa-solid", "fa-robot", "title-icon"], [1, "fa-solid", "fa-comment-dots", "title-icon"], [1, "window-actions"], ["title", "Minimize", 1, "window-action-btn", 3, "click"], [1, "fa-solid", "fa-window-minimize"], [1, "window-action-btn", 3, "click", "title"], [1, "fa-solid"], ["title", "Close", 1, "window-action-btn", 3, "click"], [1, "fa-solid", "fa-xmark"], [1, "window-content"], [1, "loading-container"], [1, "error-container"], [3, "entity", "mode", "isVisible"], ["size", "large", 3, "text"], [1, "fa-solid", "fa-exclamation-triangle"], [3, "runOpened", "entity", "mode", "isVisible"]], template: function TestHarnessCustomWindowComponent_Template(rf, ctx) { if (rf & 1) { const _r1 = i0.ɵɵgetCurrentView(); i0.ɵɵelementStart(0, "mj-window", 1, 0); i0.ɵɵlistener("Close", function TestHarnessCustomWindowComponent_Template_mj_window_Close_0_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnClose()); })("StateChange", function TestHarnessCustomWindowComponent_Template_mj_window_StateChange_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnStateChange($event)); })("Resize", function TestHarnessCustomWindowComponent_Template_mj_window_Resize_0_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnWindowResize()); }); i0.ɵɵelementStart(2, "mj-window-titlebar")(3, "div", 2); i0.ɵɵconditionalCreate(4, TestHarnessCustomWindowComponent_Conditional_4_Template, 1, 1, "img", 3)(5, TestHarnessCustomWindowComponent_Conditional_5_Template, 1, 0, "i", 4)(6, TestHarnessCustomWindowComponent_Conditional_6_Template, 1, 0, "i", 5); i0.ɵɵelementStart(7, "span"); i0.ɵɵtext(8); i0.ɵɵelementEnd()(); i0.ɵɵelementStart(9, "div", 6)(10, "button", 7); i0.ɵɵlistener("click", function TestHarnessCustomWindowComponent_Template_button_click_10_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.Minimize()); }); i0.ɵɵelement(11, "i", 8); i0.ɵɵelementEnd(); i0.ɵɵelementStart(12, "button", 9); i0.ɵɵlistener("click", function TestHarnessCustomWindowComponent_Template_button_click_12_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.ToggleMaximize()); }); i0.ɵɵelement(13, "i", 10); i0.ɵɵelementEnd(); i0.ɵɵelementStart(14, "button", 11); i0.ɵɵlistener("click", function TestHarnessCustomWindowComponent_Template_button_click_14_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.CloseButtonClick()); }); i0.ɵɵelement(15, "i", 12); i0.ɵɵelementEnd()()(); i0.ɵɵelementStart(16, "div", 13); i0.ɵɵconditionalCreate(17, TestHarnessCustomWindowComponent_Conditional_17_Template, 2, 1, "div", 14)(18, TestHarnessCustomWindowComponent_Conditional_18_Template, 4, 1, "div", 15)(19, TestHarnessCustomWindowComponent_Conditional_19_Template, 1, 3, "mj-ai-test-harness", 16); i0.ɵɵelementEnd()(); } if (rf & 2) { i0.ɵɵproperty("Visible", ctx.WindowVisible)("Title", "")("Width", ctx.Width)("Height", ctx.Height)("Top", ctx.WindowTop)("Left", ctx.WindowLeft)("MinWidth", ctx.IsMinimized ? 400 : 800)("MinHeight", ctx.IsMinimized ? 60 : 600)("Draggable", true)("Resizable", !ctx.IsMinimized)("State", ctx.WindowState); i0.ɵɵadvance(4); i0.ɵɵconditional(ctx.Mode === "agent" && (ctx.Agent == null ? null : ctx.Agent.LogoURL) ? 4 : ctx.Mode === "agent" ? 5 : 6); i0.ɵɵadvance(4); i0.ɵɵtextInterpolate(ctx.WindowTitle); i0.ɵɵadvance(4); i0.ɵɵproperty("title", ctx.IsMaximized ? "Restore" : "Maximize"); i0.ɵɵadvance(); i0.ɵɵclassProp("fa-window-maximize", !ctx.IsMaximized)("fa-window-restore", ctx.IsMaximized); i0.ɵɵadvance(4); i0.ɵɵconditional(ctx.Loading ? 17 : ctx.Error ? 18 : 19); } }, dependencies: [i1.MJWindowComponent, i1.MJWindowTitlebarComponent, i2.LoadingComponent, i3.AITestHarnessComponent], styles: ["[_nghost-%COMP%] {\n display: contents;\n }\n\n .window-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n\n .title-icon {\n color: var(--mj-text-muted);\n font-size: 16px;\n }\n\n .title-logo {\n width: 20px;\n height: 20px;\n object-fit: contain;\n }\n }\n\n .window-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 4px;\n align-items: center;\n }\n\n .window-action-btn[_ngcontent-%COMP%] {\n background: transparent;\n border: none;\n padding: 8px;\n cursor: pointer;\n border-radius: var(--mj-radius-sm);\n transition: var(--mj-transition-colors);\n display: flex;\n align-items: center;\n justify-content: center;\n\n &:hover {\n background: var(--mj-bg-surface-hover);\n }\n\n i {\n font-size: 14px;\n color: var(--mj-text-muted);\n }\n }\n\n .window-content[_ngcontent-%COMP%] {\n height: 100%;\n display: flex;\n flex-direction: column;\n }\n\n .loading-container[_ngcontent-%COMP%], \n .error-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n gap: 1rem;\n }\n\n .error-container[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n\n i {\n font-size: 3rem;\n }\n }\n\n mj-ai-test-harness[_ngcontent-%COMP%] {\n flex: 1;\n overflow: hidden;\n }\n\n \n\n [_nghost-%COMP%] .mj-window-body {\n display: flex;\n flex-direction: column;\n padding: 0;\n }\n\n \n\n [_nghost-%COMP%] .mj-window-close {\n display: none;\n }"] }); } } (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(TestHarnessCustomWindowComponent, [{ type: Component, args: [{ standalone: false, selector: 'mj-test-harness-custom-window', template: ` <mj-window #mjWindow [Visible]="WindowVisible" [Title]="''" [Width]="Width" [Height]="Height" [Top]="WindowTop" [Left]="WindowLeft" [MinWidth]="IsMinimized ? 400 : 800" [MinHeight]="IsMinimized ? 60 : 600" [Draggable]="true" [Resizable]="!IsMinimized" [State]="WindowState" (Close)="OnClose()" (StateChange)="OnStateChange($event)" (Resize)="OnWindowResize()"> <mj-window-titlebar> <div class="window-title"> @if (Mode === 'agent' && Agent?.LogoURL) { <img [src]="Agent?.LogoURL" class="title-logo" alt="Agent logo" /> } @else if (Mode === 'agent') { <i class="fa-solid fa-robot title-icon"></i> } @else { <i class="fa-solid fa-comment-dots title-icon"></i> } <span>{{ WindowTitle }}</span> </div> <div class="window-actions"> <button (click)="Minimize()" title="Minimize" class="window-action-btn"> <i class="fa-solid fa-window-minimize"></i> </button> <button (click)="ToggleMaximize()" [title]="IsMaximized ? 'Restore' : 'Maximize'" class="window-action-btn"> <i class="fa-solid" [class.fa-window-maximize]="!IsMaximized" [class.fa-window-restore]="IsMaximized"></i> </button> <button (click)="CloseButtonClick()" title="Close" class="window-action-btn"> <i class="fa-solid fa-xmark"></i> </button> </div> </mj-window-titlebar> <div class="window-content"> @if (Loading) { <div class="loading-container"> <mj-loading [text]="'Loading ' + (Mode === 'agent' ? 'AI Agent' : 'AI Prompt') + '...'" size="large"></mj-loading> </div> } @else if (Error) { <div class="error-container"> <i class="fa-solid fa-exclamation-triangle"></i> <p>{{ Error }}</p> </div> } @else { <mj-ai-test-harness [entity]="(Agent || Prompt) || null" [mode]="Mode" [isVisible]="true" (runOpened)="OnRunOpened($event)"> </mj-ai-test-harness> } </div> </mj-window> `, styles: ["\n :host {\n display: contents;\n }\n\n .window-title {\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n\n .title-icon {\n color: var(--mj-text-muted);\n font-size: 16px;\n }\n\n .title-logo {\n width: 20px;\n height: 20px;\n object-fit: contain;\n }\n }\n\n .window-actions {\n display: flex;\n gap: 4px;\n align-items: center;\n }\n\n .window-action-btn {\n background: transparent;\n border: none;\n padding: 8px;\n cursor: pointer;\n border-radius: var(--mj-radius-sm);\n transition: var(--mj-transition-colors);\n display: flex;\n align-items: center;\n justify-content: center;\n\n &:hover {\n background: var(--mj-bg-surface-hover);\n }\n\n i {\n font-size: 14px;\n color: var(--mj-text-muted);\n }\n }\n\n .window-content {\n height: 100%;\n display: flex;\n flex-direction: column;\n }\n\n .loading-container,\n .error-container {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n gap: 1rem;\n }\n\n .error-container {\n color: var(--mj-status-error);\n\n i {\n font-size: 3rem;\n }\n }\n\n mj-ai-test-harness {\n flex: 1;\n overflow: hidden;\n }\n\n /* Ensure mj-window body fills available space for the test harness */\n :host ::ng-deep .mj-window-body {\n display: flex;\n flex-direction: column;\n padding: 0;\n }\n\n /* Hide the default close button since we have custom titlebar actions */\n :host ::ng-deep .mj-window-close {\n display: none;\n }\n "] }] }], null, { MjWindow: [{ type: ViewChild, args: ['mjWindow', { static: false }] }], TestHarness: [{ type: ViewChild, args: [AITestHarnessComponent, { static: false }] }], Data: [{ type: Input }], CloseWindow: [{ type: Output }], MinimizeWindow: [{ type: Output }], RestoreWindow: [{ type: Output }], ExecutionStateChange: [{ type: Output }] }); })(); (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(TestHarnessCustomWindowComponent, { className: "TestHarnessCustomWindowComponent", filePath: "lib/test-harness-custom-window.component.ts", lineNumber: 192 }); })(); //# sourceMappingURL=test-harness-custom-window.component.js.map