@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
JavaScript
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">
(Mode === 'agent' && Agent?.LogoURL) {
<img [src]="Agent?.LogoURL" class="title-logo" alt="Agent logo" />
} if (Mode === 'agent') {
<i class="fa-solid fa-robot title-icon"></i>
} {
<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">
(Loading) {
<div class="loading-container">
<mj-loading [text]="'Loading ' + (Mode === 'agent' ? 'AI Agent' : 'AI Prompt') + '...'" size="large"></mj-loading>
</div>
}
if (Error) {
<div class="error-container">
<i class="fa-solid fa-exclamation-triangle"></i>
<p>{{ Error }}</p>
</div>
}
{
<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