@memberjunction/ng-ai-test-harness 2.58.0 → 2.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/agent-execution-monitor.component.d.ts +49 -94
- package/dist/lib/agent-execution-monitor.component.d.ts.map +1 -1
- package/dist/lib/agent-execution-monitor.component.js +448 -772
- package/dist/lib/agent-execution-monitor.component.js.map +1 -1
- package/dist/lib/agent-execution-node.component.d.ts +28 -6
- package/dist/lib/agent-execution-node.component.d.ts.map +1 -1
- package/dist/lib/agent-execution-node.component.js +399 -199
- package/dist/lib/agent-execution-node.component.js.map +1 -1
- package/dist/lib/ai-test-harness-window.component.d.ts +1 -0
- package/dist/lib/ai-test-harness-window.component.d.ts.map +1 -1
- package/dist/lib/ai-test-harness-window.component.js +13 -4
- package/dist/lib/ai-test-harness-window.component.js.map +1 -1
- package/dist/lib/ai-test-harness.component.d.ts +33 -17
- package/dist/lib/ai-test-harness.component.d.ts.map +1 -1
- package/dist/lib/ai-test-harness.component.js +294 -200
- package/dist/lib/ai-test-harness.component.js.map +1 -1
- package/dist/lib/test-harness-window.service.d.ts.map +1 -1
- package/dist/lib/test-harness-window.service.js +10 -6
- package/dist/lib/test-harness-window.service.js.map +1 -1
- package/dist/module.d.ts +16 -16
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +9 -16
- package/dist/module.js.map +1 -1
- package/dist/public-api.d.ts +1 -1
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +1 -1
- package/dist/public-api.js.map +1 -1
- package/package.json +8 -8
|
@@ -4,6 +4,7 @@ import { MJNotificationService } from '@memberjunction/ng-notifications';
|
|
|
4
4
|
import { SharedService } from '@memberjunction/ng-shared';
|
|
5
5
|
import { Subject } from 'rxjs';
|
|
6
6
|
import { AIEngineBase } from '@memberjunction/ai-engine-base';
|
|
7
|
+
import { ParseJSONRecursive } from '@memberjunction/global';
|
|
7
8
|
import * as i0 from "@angular/core";
|
|
8
9
|
import * as i1 from "@angular/platform-browser";
|
|
9
10
|
import * as i2 from "@progress/kendo-angular-dialog";
|
|
@@ -375,12 +376,12 @@ function AITestHarnessComponent_div_0_Conditional_4_Conditional_25_Template(rf,
|
|
|
375
376
|
function AITestHarnessComponent_div_0_Conditional_4_Conditional_26_Template(rf, ctx) { if (rf & 1) {
|
|
376
377
|
const _r14 = i0.ɵɵgetCurrentView();
|
|
377
378
|
i0.ɵɵelementStart(0, "div", 46)(1, "mj-agent-execution-monitor", 89);
|
|
378
|
-
i0.ɵɵlistener("viewRunClick", function AITestHarnessComponent_div_0_Conditional_4_Conditional_26_Template_mj_agent_execution_monitor_viewRunClick_1_listener($event) { i0.ɵɵrestoreView(_r14); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.
|
|
379
|
+
i0.ɵɵlistener("viewRunClick", function AITestHarnessComponent_div_0_Conditional_4_Conditional_26_Template_mj_agent_execution_monitor_viewRunClick_1_listener($event) { i0.ɵɵrestoreView(_r14); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.navigateToAgentRun($event)); });
|
|
379
380
|
i0.ɵɵelementEnd()();
|
|
380
381
|
} if (rf & 2) {
|
|
381
382
|
const ctx_r1 = i0.ɵɵnextContext(3);
|
|
382
383
|
i0.ɵɵadvance();
|
|
383
|
-
i0.ɵɵproperty("mode", ctx_r1.executionMonitorMode)("
|
|
384
|
+
i0.ɵɵproperty("mode", ctx_r1.executionMonitorMode)("agentRun", ctx_r1.currentAgentRun)("liveSteps", ctx_r1.liveAgentSteps)("autoExpand", true)("runId", ctx_r1.getLastRunId())("runType", ctx_r1.mode);
|
|
384
385
|
} }
|
|
385
386
|
function AITestHarnessComponent_div_0_Conditional_4_Conditional_27_For_8_Template(rf, ctx) { if (rf & 1) {
|
|
386
387
|
const _r16 = i0.ɵɵgetCurrentView();
|
|
@@ -708,7 +709,7 @@ function AITestHarnessComponent_div_0_Conditional_4_Template(rf, ctx) { if (rf &
|
|
|
708
709
|
i0.ɵɵtemplate(23, AITestHarnessComponent_div_0_Conditional_4_Conditional_23_Template, 2, 1, "span", 43);
|
|
709
710
|
i0.ɵɵelementEnd()();
|
|
710
711
|
i0.ɵɵelementStart(24, "div", 44);
|
|
711
|
-
i0.ɵɵtemplate(25, AITestHarnessComponent_div_0_Conditional_4_Conditional_25_Template, 13, 2, "div", 45)(26, AITestHarnessComponent_div_0_Conditional_4_Conditional_26_Template, 2,
|
|
712
|
+
i0.ɵɵtemplate(25, AITestHarnessComponent_div_0_Conditional_4_Conditional_25_Template, 13, 2, "div", 45)(26, AITestHarnessComponent_div_0_Conditional_4_Conditional_26_Template, 2, 6, "div", 46)(27, AITestHarnessComponent_div_0_Conditional_4_Conditional_27_Template, 12, 0, "div", 45)(28, AITestHarnessComponent_div_0_Conditional_4_Conditional_28_Template, 80, 52, "div", 47)(29, AITestHarnessComponent_div_0_Conditional_4_Conditional_29_Template, 11, 2, "div", 45);
|
|
712
713
|
i0.ɵɵelementEnd()()()();
|
|
713
714
|
} if (rf & 2) {
|
|
714
715
|
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
@@ -806,7 +807,7 @@ function AITestHarnessComponent_div_0_Conditional_5_For_5_Conditional_10_Templat
|
|
|
806
807
|
function AITestHarnessComponent_div_0_Conditional_5_For_5_Conditional_11_Template(rf, ctx) { if (rf & 1) {
|
|
807
808
|
const _r29 = i0.ɵɵgetCurrentView();
|
|
808
809
|
i0.ɵɵelementStart(0, "button", 135);
|
|
809
|
-
i0.ɵɵlistener("click", function AITestHarnessComponent_div_0_Conditional_5_For_5_Conditional_11_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r29); const message_r26 = i0.ɵɵnextContext().$implicit; const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.navigateToAgentRun(message_r26.agentRunId)); });
|
|
810
|
+
i0.ɵɵlistener("click", function AITestHarnessComponent_div_0_Conditional_5_For_5_Conditional_11_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r29); const message_r26 = i0.ɵɵnextContext().$implicit; const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.navigateToAgentRun({ runId: message_r26.agentRunId, runType: "agent" })); });
|
|
810
811
|
i0.ɵɵelement(1, "i", 136);
|
|
811
812
|
i0.ɵɵelementEnd();
|
|
812
813
|
} }
|
|
@@ -1062,6 +1063,10 @@ export class AITestHarnessComponent {
|
|
|
1062
1063
|
* Emitted when the user navigates to view a run (agent or prompt)
|
|
1063
1064
|
*/
|
|
1064
1065
|
this.runOpened = new EventEmitter();
|
|
1066
|
+
/**
|
|
1067
|
+
* Event emitted when the component requests to be minimized (e.g., when navigating to a run)
|
|
1068
|
+
*/
|
|
1069
|
+
this.minimizeRequested = new EventEmitter();
|
|
1065
1070
|
// === Conversation State ===
|
|
1066
1071
|
/** Complete array of all messages in the current conversation session */
|
|
1067
1072
|
this.conversationMessages = [];
|
|
@@ -1134,11 +1139,17 @@ export class AITestHarnessComponent {
|
|
|
1134
1139
|
// === Execution Monitor Properties ===
|
|
1135
1140
|
/** Mode for the execution monitor component */
|
|
1136
1141
|
this.executionMonitorMode = 'historical';
|
|
1137
|
-
/** Current
|
|
1138
|
-
this.
|
|
1142
|
+
/** Current agent run being displayed in execution monitor */
|
|
1143
|
+
this.currentAgentRun = null;
|
|
1144
|
+
/**
|
|
1145
|
+
* Tracks agent steps during live execution (deprecated - now using agent run's Steps directly)
|
|
1146
|
+
*/
|
|
1147
|
+
this.liveAgentSteps = [];
|
|
1148
|
+
/** Track the last processed run ID to avoid reprocessing same data */
|
|
1149
|
+
this.lastProcessedRunId = null;
|
|
1139
1150
|
/** Agent conversation state tracking */
|
|
1140
1151
|
this.agentConversationState = null;
|
|
1141
|
-
this.
|
|
1152
|
+
this.lastAgentPayload = null;
|
|
1142
1153
|
this.subAgentHistory = [];
|
|
1143
1154
|
/** Whether to show the save conversation dialog */
|
|
1144
1155
|
this.showSaveDialog = false;
|
|
@@ -1270,51 +1281,60 @@ export class AITestHarnessComponent {
|
|
|
1270
1281
|
subscribeToEvents() {
|
|
1271
1282
|
// Set up direct GraphQL subscription for agent execution stream
|
|
1272
1283
|
const dataProvider = Metadata.Provider;
|
|
1273
|
-
const _providerPushStatusSub = dataProvider.PushStatusUpdates().subscribe((status) => {
|
|
1284
|
+
const _providerPushStatusSub = dataProvider.PushStatusUpdates().subscribe(async (status) => {
|
|
1274
1285
|
const message = JSON.parse(status.message || '{}');
|
|
1275
1286
|
if (message?.resolver === 'RunAIAgentResolver') {
|
|
1276
1287
|
// Handle different types of streaming messages
|
|
1277
|
-
if (message?.type === 'ExecutionProgress' && message.data?.
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
if (
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1288
|
+
if (message?.type === 'ExecutionProgress' && message.data?.agentRun) {
|
|
1289
|
+
// The server should be sending the full serialized agent run
|
|
1290
|
+
const serializedAgentRun = message.data.agentRun;
|
|
1291
|
+
// Update streaming message content if available
|
|
1292
|
+
if (message.data.progress?.message) {
|
|
1293
|
+
const streamingMessage = this.conversationMessages.find(m => m.isStreaming);
|
|
1294
|
+
if (streamingMessage) {
|
|
1295
|
+
// Clear any typing animation interval
|
|
1296
|
+
const typingInterval = streamingMessage._typingInterval;
|
|
1297
|
+
if (typingInterval) {
|
|
1298
|
+
clearInterval(typingInterval);
|
|
1299
|
+
delete streamingMessage._typingInterval;
|
|
1300
|
+
}
|
|
1301
|
+
// Update the streaming content with the status message
|
|
1302
|
+
streamingMessage.streamingContent = message.data.progress.message;
|
|
1303
|
+
this.scrollNeeded = true;
|
|
1287
1304
|
}
|
|
1288
|
-
// Update the streaming content with the status message
|
|
1289
|
-
streamingMessage.streamingContent = userMsg;
|
|
1290
|
-
this.scrollNeeded = true;
|
|
1291
1305
|
}
|
|
1292
|
-
// Update
|
|
1293
|
-
if (!this.
|
|
1294
|
-
|
|
1306
|
+
// Update or create the agent run entity from the serialized data
|
|
1307
|
+
if (!this.currentAgentRun) {
|
|
1308
|
+
// First time - create the entity
|
|
1309
|
+
const md = new Metadata();
|
|
1310
|
+
this.currentAgentRun = await md.GetEntityObject('MJ: AI Agent Runs');
|
|
1295
1311
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
StepType: message.data.progress.step || 'unknown',
|
|
1303
|
-
Status: 'Running',
|
|
1304
|
-
StartedAt: new Date()
|
|
1305
|
-
},
|
|
1306
|
-
executionType: message.data.progress.step || 'unknown',
|
|
1307
|
-
agentHierarchy: message.data.progress.agentHierarchy || [],
|
|
1308
|
-
depth: message.data.progress.depth || 0,
|
|
1309
|
-
startTime: new Date()
|
|
1310
|
-
};
|
|
1311
|
-
// For live mode, we'll append steps as they come
|
|
1312
|
-
if (!this.currentExecutionData.liveSteps) {
|
|
1313
|
-
this.currentExecutionData.liveSteps = [];
|
|
1312
|
+
// Load the serialized data into our entity
|
|
1313
|
+
await this.currentAgentRun.LoadFromData(serializedAgentRun);
|
|
1314
|
+
// Update assistant message with agent run ID if available
|
|
1315
|
+
const streamingMessage = this.conversationMessages.find(m => m.isStreaming);
|
|
1316
|
+
if (streamingMessage && this.currentAgentRun.ID) {
|
|
1317
|
+
streamingMessage.agentRunId = this.currentAgentRun.ID;
|
|
1314
1318
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1319
|
+
// We're in live mode during streaming
|
|
1320
|
+
this.executionMonitorMode = 'live';
|
|
1321
|
+
// Pass the steps from the agent run to the execution monitor for live display
|
|
1322
|
+
this.liveAgentSteps = this.currentAgentRun.Steps || [];
|
|
1323
|
+
console.log('📊 Agent run update:', {
|
|
1324
|
+
id: this.currentAgentRun.ID,
|
|
1325
|
+
status: this.currentAgentRun.Status,
|
|
1326
|
+
stepCount: this.currentAgentRun.Steps?.length || 0,
|
|
1327
|
+
executionMonitorMode: this.executionMonitorMode,
|
|
1328
|
+
steps: this.currentAgentRun.Steps?.map(s => ({
|
|
1329
|
+
id: s.ID,
|
|
1330
|
+
type: s.StepType,
|
|
1331
|
+
status: s.Status,
|
|
1332
|
+
hasSubAgentRun: !!s.SubAgentRun,
|
|
1333
|
+
subStepCount: s.SubAgentRun?.Steps?.length || 0
|
|
1334
|
+
}))
|
|
1335
|
+
});
|
|
1336
|
+
// Force change detection
|
|
1337
|
+
this.cdr.detectChanges();
|
|
1318
1338
|
}
|
|
1319
1339
|
else if (message?.type === 'StreamingContent' && message.data?.streaming) {
|
|
1320
1340
|
const streamingMessage = this.conversationMessages.find(m => m.isStreaming);
|
|
@@ -1685,11 +1705,13 @@ export class AITestHarnessComponent {
|
|
|
1685
1705
|
this.templateVariables = [];
|
|
1686
1706
|
this.currentConversationId = null;
|
|
1687
1707
|
this.showSidebar = true;
|
|
1688
|
-
|
|
1708
|
+
// Clear execution data and tracking when explicitly resetting
|
|
1709
|
+
this.currentAgentRun = null;
|
|
1710
|
+
this.lastProcessedRunId = null;
|
|
1689
1711
|
this.executionMonitorMode = 'historical';
|
|
1690
1712
|
// Reset conversation state
|
|
1691
1713
|
this.agentConversationState = null;
|
|
1692
|
-
this.
|
|
1714
|
+
this.lastAgentPayload = null;
|
|
1693
1715
|
this.subAgentHistory = [];
|
|
1694
1716
|
// Set default tab based on mode
|
|
1695
1717
|
this.activeTab = this.mode === 'agent' ? 'agentVariables' : 'templateVariables';
|
|
@@ -1721,6 +1743,9 @@ export class AITestHarnessComponent {
|
|
|
1721
1743
|
return;
|
|
1722
1744
|
}
|
|
1723
1745
|
}
|
|
1746
|
+
// Clear execution data when explicitly starting a new conversation
|
|
1747
|
+
this.currentAgentRun = null;
|
|
1748
|
+
this.lastProcessedRunId = null;
|
|
1724
1749
|
this.resetHarness();
|
|
1725
1750
|
MJNotificationService.Instance.CreateSimpleNotification('Started new conversation', 'info', 2000);
|
|
1726
1751
|
}
|
|
@@ -1744,25 +1769,66 @@ export class AITestHarnessComponent {
|
|
|
1744
1769
|
* @param tab - The tab to activate
|
|
1745
1770
|
*/
|
|
1746
1771
|
selectTab(tab) {
|
|
1772
|
+
console.log('🔄 Switching to tab:', tab, {
|
|
1773
|
+
currentAgentRun: !!this.currentAgentRun,
|
|
1774
|
+
agentRunStatus: this.currentAgentRun?.Status || 'none',
|
|
1775
|
+
conversationMessages: this.conversationMessages.length,
|
|
1776
|
+
executionMonitorMode: this.executionMonitorMode
|
|
1777
|
+
});
|
|
1747
1778
|
this.activeTab = tab;
|
|
1748
1779
|
// If switching to execution monitor tab, ensure it has the latest data
|
|
1749
|
-
if (tab === 'executionMonitor'
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1780
|
+
if (tab === 'executionMonitor') {
|
|
1781
|
+
console.log('📊 Switching to execution monitor tab');
|
|
1782
|
+
// Always ensure we have the latest execution data when switching to monitor
|
|
1783
|
+
if (this.conversationMessages.length > 0) {
|
|
1784
|
+
const lastAssistantMessage = this.conversationMessages
|
|
1785
|
+
.filter(m => m.role === 'assistant' && m.agentRunId)
|
|
1786
|
+
.pop();
|
|
1787
|
+
console.log('🔍 Last assistant message with agent run:', {
|
|
1788
|
+
found: !!lastAssistantMessage,
|
|
1789
|
+
hasAgentRunId: !!lastAssistantMessage?.agentRunId,
|
|
1790
|
+
agentRunId: lastAssistantMessage?.agentRunId
|
|
1791
|
+
});
|
|
1792
|
+
if (lastAssistantMessage && lastAssistantMessage.agentRunId) {
|
|
1793
|
+
// Always update agent run to ensure it's fresh
|
|
1794
|
+
const messageRunId = lastAssistantMessage.agentRunId;
|
|
1795
|
+
// Load the agent run
|
|
1796
|
+
this.loadAgentRun(messageRunId);
|
|
1797
|
+
this.executionMonitorMode = 'historical';
|
|
1798
|
+
this.lastProcessedRunId = messageRunId;
|
|
1799
|
+
console.log('✅ Loading agent run:', {
|
|
1800
|
+
mode: this.executionMonitorMode,
|
|
1801
|
+
runId: this.lastProcessedRunId
|
|
1802
|
+
});
|
|
1803
|
+
// Trigger change detection to ensure the execution monitor updates
|
|
1804
|
+
setTimeout(() => {
|
|
1805
|
+
this.cdr.detectChanges();
|
|
1806
|
+
}, 50);
|
|
1807
|
+
}
|
|
1808
|
+
else {
|
|
1809
|
+
console.log('❌ No agent run ID found in messages');
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
else {
|
|
1813
|
+
console.log('❌ No conversation messages found');
|
|
1756
1814
|
}
|
|
1757
1815
|
}
|
|
1816
|
+
else {
|
|
1817
|
+
console.log('📄 Switching away from execution monitor, preserving data:', {
|
|
1818
|
+
currentAgentRunExists: !!this.currentAgentRun
|
|
1819
|
+
});
|
|
1820
|
+
// Don't clear currentAgentRun when switching away from execution monitor
|
|
1821
|
+
// This preserves the state for when the user switches back
|
|
1822
|
+
}
|
|
1758
1823
|
}
|
|
1759
1824
|
/**
|
|
1760
1825
|
* Shows the execution history for a specific message
|
|
1761
1826
|
* @param message - The message to show execution history for
|
|
1762
1827
|
*/
|
|
1763
|
-
showMessageExecutionHistory(message) {
|
|
1764
|
-
if (message.
|
|
1765
|
-
|
|
1828
|
+
async showMessageExecutionHistory(message) {
|
|
1829
|
+
if (message.agentRunId && this.mode === 'agent') {
|
|
1830
|
+
// Load the agent run entity
|
|
1831
|
+
await this.loadAgentRun(message.agentRunId);
|
|
1766
1832
|
this.executionMonitorMode = 'historical';
|
|
1767
1833
|
// Switch to execution monitor tab if not already there
|
|
1768
1834
|
if (this.activeTab !== 'executionMonitor') {
|
|
@@ -1771,26 +1837,39 @@ export class AITestHarnessComponent {
|
|
|
1771
1837
|
}
|
|
1772
1838
|
}
|
|
1773
1839
|
/**
|
|
1774
|
-
*
|
|
1840
|
+
* Loads an agent run entity by ID
|
|
1841
|
+
* @param runId - The ID of the agent run to load
|
|
1775
1842
|
*/
|
|
1776
|
-
|
|
1777
|
-
const
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1843
|
+
async loadAgentRun(runId) {
|
|
1844
|
+
const md = new Metadata();
|
|
1845
|
+
const agentRunEntity = await md.GetEntityObject('MJ: AI Agent Runs');
|
|
1846
|
+
await agentRunEntity.Load(runId);
|
|
1847
|
+
await this.internalLoadAgenRun(agentRunEntity);
|
|
1848
|
+
}
|
|
1849
|
+
async loadAgentRunFromData(agentRunData) {
|
|
1850
|
+
const md = new Metadata();
|
|
1851
|
+
const agentRunEntity = await md.GetEntityObject('MJ: AI Agent Runs');
|
|
1852
|
+
await agentRunEntity.LoadFromData(agentRunData);
|
|
1853
|
+
await this.internalLoadAgenRun(agentRunEntity);
|
|
1854
|
+
}
|
|
1855
|
+
async internalLoadAgenRun(agentRunEntity) {
|
|
1856
|
+
try {
|
|
1857
|
+
this.currentAgentRun = agentRunEntity;
|
|
1858
|
+
// The Load method automatically loads related steps through InnerLoad override
|
|
1859
|
+
// No need to call LoadRelatedData explicitly as it's protected
|
|
1860
|
+
// Set execution monitor mode
|
|
1861
|
+
this.executionMonitorMode = 'historical';
|
|
1862
|
+
console.log('✅ Loaded agent run:', {
|
|
1863
|
+
id: agentRunEntity.ID,
|
|
1864
|
+
stepCount: this.currentAgentRun.Steps?.length || 0
|
|
1865
|
+
});
|
|
1866
|
+
// Force change detection to update the execution monitor
|
|
1867
|
+
this.cdr.detectChanges();
|
|
1868
|
+
}
|
|
1869
|
+
catch (error) {
|
|
1870
|
+
console.error('❌ Failed to load agent run:', error);
|
|
1871
|
+
this.currentAgentRun = null;
|
|
1872
|
+
}
|
|
1794
1873
|
}
|
|
1795
1874
|
/**
|
|
1796
1875
|
* Sends the current user message to the AI agent and initiates execution.
|
|
@@ -1818,13 +1897,20 @@ export class AITestHarnessComponent {
|
|
|
1818
1897
|
// Clear input and update change detection
|
|
1819
1898
|
const messageToSend = this.currentUserMessage;
|
|
1820
1899
|
this.currentUserMessage = '';
|
|
1821
|
-
|
|
1900
|
+
// Use Promise.resolve to defer the change detection to the next microtask
|
|
1901
|
+
// This prevents ExpressionChangedAfterItHasBeenCheckedError
|
|
1902
|
+
Promise.resolve().then(() => {
|
|
1903
|
+
this.cdr.detectChanges();
|
|
1904
|
+
});
|
|
1822
1905
|
// Scroll to bottom
|
|
1823
1906
|
this.scrollNeeded = true;
|
|
1824
1907
|
// Auto-switch to execution monitor tab if in agent mode
|
|
1825
1908
|
if (this.mode === 'agent' && this.activeTab !== 'executionMonitor') {
|
|
1826
1909
|
this.selectTab('executionMonitor');
|
|
1827
1910
|
}
|
|
1911
|
+
// Clear previous execution data when starting a new execution
|
|
1912
|
+
this.currentAgentRun = null;
|
|
1913
|
+
this.lastProcessedRunId = null;
|
|
1828
1914
|
// Execute based on mode
|
|
1829
1915
|
if (this.mode === 'agent') {
|
|
1830
1916
|
await this.executeAgent(messageToSend);
|
|
@@ -1838,7 +1924,14 @@ export class AITestHarnessComponent {
|
|
|
1838
1924
|
return;
|
|
1839
1925
|
this.isExecuting = true;
|
|
1840
1926
|
// Clear previous execution data when starting a new run
|
|
1841
|
-
|
|
1927
|
+
// Create a proper agent run entity for live tracking
|
|
1928
|
+
const md = new Metadata();
|
|
1929
|
+
this.currentAgentRun = await md.GetEntityObject('MJ: AI Agent Runs');
|
|
1930
|
+
this.currentAgentRun.ID = `temp-${Date.now()}`;
|
|
1931
|
+
this.currentAgentRun.Status = 'Running';
|
|
1932
|
+
this.currentAgentRun.StartedAt = new Date();
|
|
1933
|
+
// Steps will be populated by the agent updates
|
|
1934
|
+
this.liveAgentSteps = [];
|
|
1842
1935
|
this.executionMonitorMode = 'live';
|
|
1843
1936
|
// Add placeholder assistant message for streaming
|
|
1844
1937
|
const assistantMessage = {
|
|
@@ -1856,9 +1949,7 @@ export class AITestHarnessComponent {
|
|
|
1856
1949
|
this.scrollNeeded = true;
|
|
1857
1950
|
// Start elapsed time counter
|
|
1858
1951
|
this.startElapsedTimeCounter(assistantMessage);
|
|
1859
|
-
// Initialize execution monitor for live mode
|
|
1860
|
-
this.currentExecutionData = { liveSteps: [] };
|
|
1861
|
-
this.executionMonitorMode = 'live';
|
|
1952
|
+
// Initialize execution monitor for live mode - already created above
|
|
1862
1953
|
try {
|
|
1863
1954
|
// Get GraphQL data provider
|
|
1864
1955
|
const dataProvider = Metadata.Provider;
|
|
@@ -1876,8 +1967,8 @@ export class AITestHarnessComponent {
|
|
|
1876
1967
|
if (this.agentConversationState) {
|
|
1877
1968
|
dataContext._conversationState = this.agentConversationState;
|
|
1878
1969
|
}
|
|
1879
|
-
if (this.
|
|
1880
|
-
dataContext.
|
|
1970
|
+
if (this.lastAgentPayload) {
|
|
1971
|
+
dataContext._lastPayload = this.lastAgentPayload;
|
|
1881
1972
|
}
|
|
1882
1973
|
if (this.subAgentHistory.length > 0) {
|
|
1883
1974
|
dataContext._subAgentHistory = this.subAgentHistory;
|
|
@@ -1928,64 +2019,58 @@ export class AITestHarnessComponent {
|
|
|
1928
2019
|
assistantMessage.isStreaming = false;
|
|
1929
2020
|
if (executionResult?.success) {
|
|
1930
2021
|
// Parse the payload to get the full execution result
|
|
1931
|
-
const
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
2022
|
+
const parseOptions = {
|
|
2023
|
+
extractInlineJson: true,
|
|
2024
|
+
maxDepth: 100,
|
|
2025
|
+
debug: false
|
|
2026
|
+
};
|
|
2027
|
+
let fullResult = JSON.parse(executionResult.payload);
|
|
2028
|
+
// Apply recursive JSON parsing to extract any nested JSON strings
|
|
2029
|
+
fullResult = ParseJSONRecursive(fullResult, parseOptions);
|
|
2030
|
+
// Store agent run ID with the message
|
|
2031
|
+
if (fullResult && fullResult.agentRun?.ID) {
|
|
2032
|
+
assistantMessage.agentRunId = fullResult.agentRunID;
|
|
1938
2033
|
}
|
|
1939
|
-
//
|
|
1940
|
-
|
|
1941
|
-
|
|
2034
|
+
// Load the agent run for display
|
|
2035
|
+
if (fullResult && fullResult.agentRun) {
|
|
2036
|
+
await this.loadAgentRunFromData(fullResult.agentRun);
|
|
2037
|
+
// Only switch to historical mode after successfully loading
|
|
2038
|
+
this.executionMonitorMode = 'historical';
|
|
2039
|
+
// Clear live steps only after we have the historical data
|
|
2040
|
+
this.liveAgentSteps = [];
|
|
2041
|
+
}
|
|
2042
|
+
else {
|
|
2043
|
+
// If no agent run ID, keep showing live steps
|
|
2044
|
+
console.log('⚠️ No agent run ID in result, keeping live mode');
|
|
2045
|
+
}
|
|
2046
|
+
// Auto-expand all monitoring nodes once execution is complete
|
|
2047
|
+
setTimeout(() => {
|
|
2048
|
+
this.expandAllMonitoringNodes();
|
|
2049
|
+
}, 100);
|
|
1942
2050
|
// Preserve conversation state from the result
|
|
1943
|
-
if (fullResult.
|
|
1944
|
-
this.
|
|
2051
|
+
if (fullResult.payload) {
|
|
2052
|
+
this.lastAgentPayload = fullResult.payload;
|
|
1945
2053
|
// Extract conversation state if present
|
|
1946
|
-
if (fullResult.
|
|
1947
|
-
this.agentConversationState = fullResult.
|
|
1948
|
-
}
|
|
1949
|
-
// Track sub-agent executions
|
|
1950
|
-
if (fullResult.executionTree) {
|
|
1951
|
-
const subAgentNodes = this.extractSubAgentNodes(fullResult.executionTree);
|
|
1952
|
-
if (subAgentNodes.length > 0) {
|
|
1953
|
-
this.subAgentHistory.push(...subAgentNodes);
|
|
1954
|
-
}
|
|
2054
|
+
if (fullResult.payload.conversationState) {
|
|
2055
|
+
this.agentConversationState = fullResult.payload.conversationState;
|
|
1955
2056
|
}
|
|
1956
2057
|
}
|
|
1957
|
-
// Extract the user message from the nested
|
|
2058
|
+
// Extract the user message from the nested payload structure
|
|
1958
2059
|
let displayContent = 'No response generated';
|
|
1959
|
-
let payloadData =
|
|
1960
|
-
if (fullResult.
|
|
1961
|
-
|
|
1962
|
-
if (typeof fullResult.returnValue === 'object' && fullResult.returnValue.message) {
|
|
1963
|
-
displayContent = fullResult.returnValue.message;
|
|
1964
|
-
// If there's also a payload, store it separately
|
|
1965
|
-
if (fullResult.returnValue.payload) {
|
|
1966
|
-
payloadData = fullResult.returnValue.payload;
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
else if (typeof fullResult.returnValue === 'object' &&
|
|
1970
|
-
fullResult.returnValue.nextStep?.userMessage) {
|
|
1971
|
-
// This is the expected structure for chat responses
|
|
1972
|
-
displayContent = fullResult.returnValue.nextStep.userMessage;
|
|
1973
|
-
}
|
|
1974
|
-
else if (typeof fullResult.returnValue === 'string') {
|
|
1975
|
-
// Simple string response
|
|
1976
|
-
displayContent = fullResult.returnValue;
|
|
1977
|
-
}
|
|
1978
|
-
else {
|
|
1979
|
-
// Other structured response - show as JSON
|
|
1980
|
-
displayContent = JSON.stringify(fullResult.returnValue, null, 2);
|
|
1981
|
-
}
|
|
2060
|
+
let payloadData = fullResult.payload;
|
|
2061
|
+
if (fullResult.agentRun?.Message?.length > 0) {
|
|
2062
|
+
displayContent = fullResult.agentRun.Message;
|
|
1982
2063
|
}
|
|
1983
2064
|
assistantMessage.content = displayContent;
|
|
1984
2065
|
assistantMessage.payload = payloadData; // Store the payload if present
|
|
1985
2066
|
assistantMessage.executionTime = executionResult.executionTimeMs;
|
|
1986
2067
|
assistantMessage.agentRunId = fullResult.agentRun?.ID || assistantMessage.agentRunId;
|
|
2068
|
+
// Update the tracking ID when we set new execution data
|
|
2069
|
+
this.lastProcessedRunId = assistantMessage.agentRunId || null;
|
|
1987
2070
|
// Store the full result as raw content for debugging/inspection
|
|
1988
2071
|
assistantMessage.rawContent = executionResult.payload;
|
|
2072
|
+
// Force change detection to update the execution monitor
|
|
2073
|
+
this.cdr.detectChanges();
|
|
1989
2074
|
}
|
|
1990
2075
|
else {
|
|
1991
2076
|
console.error('❌ AI Test Harness: Execution failed', {
|
|
@@ -1995,6 +2080,11 @@ export class AITestHarnessComponent {
|
|
|
1995
2080
|
});
|
|
1996
2081
|
assistantMessage.content = 'I encountered an error processing your request.';
|
|
1997
2082
|
assistantMessage.error = executionResult?.errorMessage || 'Unknown error occurred';
|
|
2083
|
+
// On failure, clear live steps and switch to historical mode
|
|
2084
|
+
if (this.currentAgentRun) {
|
|
2085
|
+
this.executionMonitorMode = 'historical';
|
|
2086
|
+
this.liveAgentSteps = [];
|
|
2087
|
+
}
|
|
1998
2088
|
// Try to parse error payload if available
|
|
1999
2089
|
if (executionResult?.payload) {
|
|
2000
2090
|
try {
|
|
@@ -2010,6 +2100,12 @@ export class AITestHarnessComponent {
|
|
|
2010
2100
|
this.scrollNeeded = true;
|
|
2011
2101
|
// Auto-save conversation
|
|
2012
2102
|
this.autoSaveConversation();
|
|
2103
|
+
// Auto-expand all monitoring nodes once execution is complete (for prompt mode)
|
|
2104
|
+
if (this.mode === 'prompt') {
|
|
2105
|
+
setTimeout(() => {
|
|
2106
|
+
this.expandAllMonitoringNodes();
|
|
2107
|
+
}, 100);
|
|
2108
|
+
}
|
|
2013
2109
|
}
|
|
2014
2110
|
catch (error) {
|
|
2015
2111
|
console.error('❌ AI Test Harness: Caught error during agent execution', {
|
|
@@ -2026,6 +2122,11 @@ export class AITestHarnessComponent {
|
|
|
2026
2122
|
lastMessage.error = error.message;
|
|
2027
2123
|
delete lastMessage.streamingContent;
|
|
2028
2124
|
}
|
|
2125
|
+
// On error, clear live steps and switch to historical mode
|
|
2126
|
+
if (this.currentAgentRun) {
|
|
2127
|
+
this.executionMonitorMode = 'historical';
|
|
2128
|
+
this.liveAgentSteps = [];
|
|
2129
|
+
}
|
|
2029
2130
|
MJNotificationService.Instance.CreateSimpleNotification('Failed to execute agent: ' + error.message, 'error', 6000);
|
|
2030
2131
|
}
|
|
2031
2132
|
finally {
|
|
@@ -2122,6 +2223,12 @@ export class AITestHarnessComponent {
|
|
|
2122
2223
|
}
|
|
2123
2224
|
// Update assistant message with result
|
|
2124
2225
|
assistantMessage.isStreaming = false;
|
|
2226
|
+
// Define parse options for both success and error cases
|
|
2227
|
+
const parseOptions = {
|
|
2228
|
+
extractInlineJson: true,
|
|
2229
|
+
maxDepth: 100,
|
|
2230
|
+
debug: false
|
|
2231
|
+
};
|
|
2125
2232
|
if (executionResult?.success) {
|
|
2126
2233
|
// Use parsedResult if available, otherwise fall back to output
|
|
2127
2234
|
assistantMessage.content = executionResult.parsedResult || executionResult.output || 'No response generated';
|
|
@@ -2137,10 +2244,14 @@ export class AITestHarnessComponent {
|
|
|
2137
2244
|
// If parsing fails, keep it as a string
|
|
2138
2245
|
}
|
|
2139
2246
|
}
|
|
2140
|
-
|
|
2247
|
+
// Apply recursive JSON parsing to the entire result
|
|
2248
|
+
const recursivelyParsed = ParseJSONRecursive(resultForDisplay, parseOptions);
|
|
2249
|
+
assistantMessage.rawContent = JSON.stringify(recursivelyParsed, null, 2);
|
|
2141
2250
|
// Store execution metadata
|
|
2142
2251
|
if (executionResult.promptRunId) {
|
|
2143
2252
|
assistantMessage.agentRunId = executionResult.promptRunId;
|
|
2253
|
+
// Update the tracking ID when we set new execution data
|
|
2254
|
+
this.lastProcessedRunId = assistantMessage.agentRunId || null;
|
|
2144
2255
|
}
|
|
2145
2256
|
}
|
|
2146
2257
|
else {
|
|
@@ -2157,7 +2268,9 @@ export class AITestHarnessComponent {
|
|
|
2157
2268
|
// If parsing fails, keep it as a string
|
|
2158
2269
|
}
|
|
2159
2270
|
}
|
|
2160
|
-
|
|
2271
|
+
// Apply recursive JSON parsing to the error result
|
|
2272
|
+
const recursivelyParsed = ParseJSONRecursive(errorResult, parseOptions);
|
|
2273
|
+
assistantMessage.rawContent = JSON.stringify(recursivelyParsed, null, 2);
|
|
2161
2274
|
}
|
|
2162
2275
|
}
|
|
2163
2276
|
delete assistantMessage.streamingContent;
|
|
@@ -2578,25 +2691,20 @@ export class AITestHarnessComponent {
|
|
|
2578
2691
|
this.skipValidation = conversation.skipValidation;
|
|
2579
2692
|
}
|
|
2580
2693
|
}
|
|
2581
|
-
// Load
|
|
2694
|
+
// Load agent run from the last assistant message if available
|
|
2582
2695
|
const lastAssistantMessage = this.conversationMessages
|
|
2583
|
-
.filter(m => m.role === 'assistant' && m.
|
|
2696
|
+
.filter(m => m.role === 'assistant' && m.agentRunId)
|
|
2584
2697
|
.pop();
|
|
2585
|
-
if (lastAssistantMessage && lastAssistantMessage.
|
|
2586
|
-
this.
|
|
2698
|
+
if (lastAssistantMessage && lastAssistantMessage.agentRunId) {
|
|
2699
|
+
this.loadAgentRun(lastAssistantMessage.agentRunId);
|
|
2587
2700
|
this.executionMonitorMode = 'historical';
|
|
2588
|
-
|
|
2589
|
-
if (this.activeTab === 'executionMonitor') {
|
|
2590
|
-
// Force change detection
|
|
2591
|
-
setTimeout(() => {
|
|
2592
|
-
this.currentExecutionData = { ...lastAssistantMessage.executionData };
|
|
2593
|
-
}, 0);
|
|
2594
|
-
}
|
|
2701
|
+
this.lastProcessedRunId = lastAssistantMessage.agentRunId;
|
|
2595
2702
|
}
|
|
2596
2703
|
else {
|
|
2597
|
-
// Clear
|
|
2598
|
-
this.
|
|
2704
|
+
// Clear agent run if no execution found
|
|
2705
|
+
this.currentAgentRun = null;
|
|
2599
2706
|
this.executionMonitorMode = 'historical';
|
|
2707
|
+
this.lastProcessedRunId = null;
|
|
2600
2708
|
}
|
|
2601
2709
|
this.scrollNeeded = true;
|
|
2602
2710
|
MJNotificationService.Instance.CreateSimpleNotification('Conversation loaded', 'info', 3000);
|
|
@@ -2798,14 +2906,17 @@ export class AITestHarnessComponent {
|
|
|
2798
2906
|
// Try to parse and format the JSON
|
|
2799
2907
|
const parsed = JSON.parse(message.rawContent);
|
|
2800
2908
|
// If this is an agent result with execution tree, enhance the display
|
|
2801
|
-
if (parsed.
|
|
2802
|
-
|
|
2909
|
+
if (parsed.agentRunID) {
|
|
2910
|
+
// Add agent run ID for reference
|
|
2911
|
+
const enhancedParsed = {
|
|
2803
2912
|
...parsed,
|
|
2804
|
-
|
|
2805
|
-
}
|
|
2913
|
+
_agentRunID: parsed.agentRunID
|
|
2914
|
+
};
|
|
2915
|
+
this.currentJsonContent = this.formatJson(enhancedParsed);
|
|
2806
2916
|
}
|
|
2807
2917
|
else {
|
|
2808
|
-
|
|
2918
|
+
// Apply recursive JSON parsing
|
|
2919
|
+
this.currentJsonContent = this.formatJson(parsed);
|
|
2809
2920
|
}
|
|
2810
2921
|
}
|
|
2811
2922
|
catch {
|
|
@@ -2843,44 +2954,6 @@ export class AITestHarnessComponent {
|
|
|
2843
2954
|
});
|
|
2844
2955
|
}
|
|
2845
2956
|
}
|
|
2846
|
-
/**
|
|
2847
|
-
* Creates a summary of the execution tree for easier viewing
|
|
2848
|
-
* @param executionTree - The execution tree to summarize
|
|
2849
|
-
* @returns A human-readable summary of the execution
|
|
2850
|
-
*/
|
|
2851
|
-
summarizeExecutionTree(executionTree) {
|
|
2852
|
-
if (!executionTree || !Array.isArray(executionTree))
|
|
2853
|
-
return null;
|
|
2854
|
-
const summary = {
|
|
2855
|
-
totalSteps: 0,
|
|
2856
|
-
stepsByType: {},
|
|
2857
|
-
hierarchy: []
|
|
2858
|
-
};
|
|
2859
|
-
const processNode = (node, level = 0) => {
|
|
2860
|
-
summary.totalSteps++;
|
|
2861
|
-
// Count by type
|
|
2862
|
-
const type = node.executionType || 'unknown';
|
|
2863
|
-
summary.stepsByType[type] = (summary.stepsByType[type] || 0) + 1;
|
|
2864
|
-
// Create hierarchical summary
|
|
2865
|
-
const nodeSummary = {
|
|
2866
|
-
level,
|
|
2867
|
-
stepName: node.step?.StepName || 'Unknown Step',
|
|
2868
|
-
type: node.executionType,
|
|
2869
|
-
duration: node.durationMs ? `${node.durationMs}ms` : 'N/A',
|
|
2870
|
-
status: node.step?.Status || 'Unknown',
|
|
2871
|
-
agentPath: node.agentHierarchy?.join(' → ') || '',
|
|
2872
|
-
hasInputData: !!node.inputData,
|
|
2873
|
-
hasOutputData: !!node.outputData
|
|
2874
|
-
};
|
|
2875
|
-
summary.hierarchy.push(nodeSummary);
|
|
2876
|
-
// Process children recursively
|
|
2877
|
-
if (node.children && node.children.length > 0) {
|
|
2878
|
-
node.children.forEach((child) => processNode(child, level + 1));
|
|
2879
|
-
}
|
|
2880
|
-
};
|
|
2881
|
-
executionTree.forEach(node => processNode(node));
|
|
2882
|
-
return summary;
|
|
2883
|
-
}
|
|
2884
2957
|
/**
|
|
2885
2958
|
* Get the last run ID from conversation messages
|
|
2886
2959
|
*/
|
|
@@ -2890,6 +2963,17 @@ export class AITestHarnessComponent {
|
|
|
2890
2963
|
.pop();
|
|
2891
2964
|
return lastAssistantMessage?.agentRunId || null;
|
|
2892
2965
|
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Expands all nodes in the execution monitor
|
|
2968
|
+
* Called automatically when execution completes
|
|
2969
|
+
*/
|
|
2970
|
+
expandAllMonitoringNodes() {
|
|
2971
|
+
if (this.currentAgentRun && this.executionMonitorMode === 'historical') {
|
|
2972
|
+
// Force refresh the execution monitor by reassigning the entity
|
|
2973
|
+
this.currentAgentRun = this.currentAgentRun;
|
|
2974
|
+
// Note: The execution monitor component should handle auto-expansion internally
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2893
2977
|
/**
|
|
2894
2978
|
* Navigate to the run details form
|
|
2895
2979
|
*/
|
|
@@ -2948,17 +3032,27 @@ export class AITestHarnessComponent {
|
|
|
2948
3032
|
}
|
|
2949
3033
|
/**
|
|
2950
3034
|
* Formats JSON content with proper indentation for display.
|
|
3035
|
+
* Also recursively parses any nested JSON strings.
|
|
2951
3036
|
* @param content - JSON string to format
|
|
2952
3037
|
* @returns Formatted JSON string or original content if parsing fails
|
|
2953
3038
|
*/
|
|
2954
3039
|
formatJson(content) {
|
|
3040
|
+
const parseOptions = {
|
|
3041
|
+
extractInlineJson: true,
|
|
3042
|
+
maxDepth: 100,
|
|
3043
|
+
debug: false
|
|
3044
|
+
};
|
|
2955
3045
|
try {
|
|
3046
|
+
let parsed;
|
|
2956
3047
|
if (typeof content === 'string') {
|
|
2957
|
-
|
|
3048
|
+
parsed = JSON.parse(content);
|
|
2958
3049
|
}
|
|
2959
3050
|
else {
|
|
2960
|
-
|
|
3051
|
+
parsed = content;
|
|
2961
3052
|
}
|
|
3053
|
+
// Apply recursive JSON parsing
|
|
3054
|
+
const recursivelyParsed = ParseJSONRecursive(parsed, parseOptions);
|
|
3055
|
+
return JSON.stringify(recursivelyParsed, null, 2);
|
|
2962
3056
|
}
|
|
2963
3057
|
catch {
|
|
2964
3058
|
return typeof content === 'string' ? content : JSON.stringify(content);
|
|
@@ -3152,12 +3246,8 @@ export class AITestHarnessComponent {
|
|
|
3152
3246
|
return '';
|
|
3153
3247
|
try {
|
|
3154
3248
|
const parsed = JSON.parse(message.rawContent);
|
|
3155
|
-
if (parsed.
|
|
3156
|
-
|
|
3157
|
-
const stepCounts = Object.entries(summary.stepsByType)
|
|
3158
|
-
.map(([type, count]) => `${count} ${type}`)
|
|
3159
|
-
.join(', ');
|
|
3160
|
-
return `${summary.totalSteps} steps executed (${stepCounts})`;
|
|
3249
|
+
if (parsed.agentRunID) {
|
|
3250
|
+
return `Agent run ID: ${parsed.agentRunID}`;
|
|
3161
3251
|
}
|
|
3162
3252
|
}
|
|
3163
3253
|
catch {
|
|
@@ -3401,9 +3491,11 @@ export class AITestHarnessComponent {
|
|
|
3401
3491
|
* Navigates to the AI Agent Run form to view detailed execution information
|
|
3402
3492
|
* @param agentRunId - The ID of the agent run to view
|
|
3403
3493
|
*/
|
|
3404
|
-
navigateToAgentRun(
|
|
3405
|
-
if (
|
|
3406
|
-
|
|
3494
|
+
navigateToAgentRun({ runId, runType }) {
|
|
3495
|
+
if (runId && runType === 'agent') {
|
|
3496
|
+
SharedService.Instance.OpenEntityRecord('MJ: AI Agent Runs', CompositeKey.FromID(runId));
|
|
3497
|
+
// Request minimization from our container
|
|
3498
|
+
this.minimizeRequested.emit();
|
|
3407
3499
|
}
|
|
3408
3500
|
}
|
|
3409
3501
|
static { this.ɵfac = function AITestHarnessComponent_Factory(t) { return new (t || AITestHarnessComponent)(i0.ɵɵdirectiveInject(i1.DomSanitizer), i0.ɵɵdirectiveInject(i2.WindowService), i0.ɵɵdirectiveInject(i0.ViewContainerRef), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i3.Router)); }; }
|
|
@@ -3418,7 +3510,7 @@ export class AITestHarnessComponent {
|
|
|
3418
3510
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.fileInput = _t.first);
|
|
3419
3511
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.messageInput = _t.first);
|
|
3420
3512
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.saveDialogInput = _t.first);
|
|
3421
|
-
} }, inputs: { mode: "mode", entity: "entity", aiAgent: "aiAgent", isVisible: "isVisible" }, outputs: { visibilityChange: "visibilityChange", runOpened: "runOpened" }, features: [i0.ɵɵNgOnChangesFeature], decls: 3, vars: 3, consts: [["fileInput", ""], ["messagesContainer", ""], ["messageInput", ""], ["saveDialogInput", ""], ["class", "harness-container", "kendoWindowContainer", "", 4, "ngIf"], [1, "save-dialog", 3, "title", "width"], [1, "confirm-dialog", 3, "title", "width"], ["kendoWindowContainer", "", 1, "harness-container"], [1, "harness-header"], [1, "agent-info"], [1, "harness-body"], ["orientation", "horizontal", 2, "height", "100%", "width", "100%"], [1, "chat-area"], [1, "agent-details"], [1, "entity-logo", 3, "src", "alt"], [3, "class"], [1, "header-actions"], ["title", "Clear conversation", 1, "custom-button", 3, "click", "disabled"], [1, "fa-solid", "fa-broom"], ["title", "Save conversation", 1, "custom-button", 3, "click", "disabled"], [1, "fa-solid", "fa-save"], ["title", "Export conversation", 1, "custom-button", 3, "click", "disabled"], [1, "fa-solid", "fa-download"], ["title", "Import conversation", 1, "custom-button", 3, "click"], [1, "fa-solid", "fa-upload"], ["type", "file", "accept", ".json", 2, "display", "none", 3, "change"], ["title", "Toggle sidebar", 1, "custom-button", "icon-only", 3, "click"], [1, "fa-solid"], [3, "collapsible", "resizable", "min"], [1, "messages-container"], [1, "empty-state"], [3, "ngClass"], [1, "input-area"], [1, "input-container"], ["placeholder", "Type your message here... (Press Enter to send, Shift+Enter for new line)", 1, "message-input", 3, "ngModelChange", "keypress", "ngModel", "rows", "disabled"], ["title", "Send message", 1, "send-button", 3, "click", "disabled"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "fa-solid", "fa-paper-plane"], [3, "collapsible", "resizable", "size", "min"], [1, "sidebar"], [1, "sidebar-tabs"], [1, "tab-button", 3, "click", "disabled"], [1, "fa-solid", "fa-history"], [1, "badge"], [1, "sidebar-content"], [1, "tab-content"], [1, "tab-content", 2, "padding", "0", "height", "100%"], [1, "tab-content", 2, "overflow-y", "auto"], [1, "fa-solid", "fa-comments"], [1, "message-header"], [1, "message-role"], [1, "message-time"], [1, "execution-time", "streaming"], [1, "execution-time", 3, "title"], ["title", "View raw JSON response", 1, "raw-toggle"], [1, "message-content"], ["title", "Copy message", 1, "copy-button-in-bubble", 3, "click"], [1, "fa-solid", "fa-copy"], [1, "streaming-wrapper"], [3, "innerHTML"], [1, "error-details"], [1, "payload-section", 3, "collapsed"], [1, "fa-solid", "fa-user"], [1, "message-role-logo", 3, "src", "alt"], ["title", "View raw JSON response", 1, "raw-toggle", 3, "click"], [1, "fa-solid", "fa-code"], [1, "streaming-content", 3, "innerHTML"], [1, "cursor"], [1, "fa-solid", "fa-exclamation-triangle"], [1, "payload-section"], [1, "payload-header", 3, "click"], [1, "payload-label"], ["type", "button", 1, "payload-toggle"], [1, "payload-content"], [2, "height", "300px", "width", "100%", 3, "ngModel", "language", "readonly"], [1, "tab-button", 3, "click"], [1, "fa-solid", "fa-diagram-project"], [1, "fa-solid", "fa-sliders"], [1, "tab-header"], [1, "variables-list"], [1, "empty-variables"], [1, "variable-item"], [1, "add-button", 3, "click", "disabled"], [1, "fa-solid", "fa-plus"], ["placeholder", "Variable name", 1, "variable-name", 3, "ngModelChange", "ngModel", "disabled"], ["textField", "text", "valueField", "value", 1, "variable-type", 3, "ngModelChange", "ngModel", "data", "valuePrimitive", "disabled"], [1, "variable-value", 3, "ngModelChange", "ngModel", "placeholder", "disabled"], ["title", "Remove variable", 1, "remove-button", 3, "click", "disabled"], [1, "fa-solid", "fa-times"], [3, "viewRunClick", "mode", "
|
|
3513
|
+
} }, inputs: { mode: "mode", entity: "entity", aiAgent: "aiAgent", isVisible: "isVisible" }, outputs: { visibilityChange: "visibilityChange", runOpened: "runOpened", minimizeRequested: "minimizeRequested" }, features: [i0.ɵɵNgOnChangesFeature], decls: 3, vars: 3, consts: [["fileInput", ""], ["messagesContainer", ""], ["messageInput", ""], ["saveDialogInput", ""], ["class", "harness-container", "kendoWindowContainer", "", 4, "ngIf"], [1, "save-dialog", 3, "title", "width"], [1, "confirm-dialog", 3, "title", "width"], ["kendoWindowContainer", "", 1, "harness-container"], [1, "harness-header"], [1, "agent-info"], [1, "harness-body"], ["orientation", "horizontal", 2, "height", "100%", "width", "100%"], [1, "chat-area"], [1, "agent-details"], [1, "entity-logo", 3, "src", "alt"], [3, "class"], [1, "header-actions"], ["title", "Clear conversation", 1, "custom-button", 3, "click", "disabled"], [1, "fa-solid", "fa-broom"], ["title", "Save conversation", 1, "custom-button", 3, "click", "disabled"], [1, "fa-solid", "fa-save"], ["title", "Export conversation", 1, "custom-button", 3, "click", "disabled"], [1, "fa-solid", "fa-download"], ["title", "Import conversation", 1, "custom-button", 3, "click"], [1, "fa-solid", "fa-upload"], ["type", "file", "accept", ".json", 2, "display", "none", 3, "change"], ["title", "Toggle sidebar", 1, "custom-button", "icon-only", 3, "click"], [1, "fa-solid"], [3, "collapsible", "resizable", "min"], [1, "messages-container"], [1, "empty-state"], [3, "ngClass"], [1, "input-area"], [1, "input-container"], ["placeholder", "Type your message here... (Press Enter to send, Shift+Enter for new line)", 1, "message-input", 3, "ngModelChange", "keypress", "ngModel", "rows", "disabled"], ["title", "Send message", 1, "send-button", 3, "click", "disabled"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "fa-solid", "fa-paper-plane"], [3, "collapsible", "resizable", "size", "min"], [1, "sidebar"], [1, "sidebar-tabs"], [1, "tab-button", 3, "click", "disabled"], [1, "fa-solid", "fa-history"], [1, "badge"], [1, "sidebar-content"], [1, "tab-content"], [1, "tab-content", 2, "padding", "0", "height", "100%"], [1, "tab-content", 2, "overflow-y", "auto"], [1, "fa-solid", "fa-comments"], [1, "message-header"], [1, "message-role"], [1, "message-time"], [1, "execution-time", "streaming"], [1, "execution-time", 3, "title"], ["title", "View raw JSON response", 1, "raw-toggle"], [1, "message-content"], ["title", "Copy message", 1, "copy-button-in-bubble", 3, "click"], [1, "fa-solid", "fa-copy"], [1, "streaming-wrapper"], [3, "innerHTML"], [1, "error-details"], [1, "payload-section", 3, "collapsed"], [1, "fa-solid", "fa-user"], [1, "message-role-logo", 3, "src", "alt"], ["title", "View raw JSON response", 1, "raw-toggle", 3, "click"], [1, "fa-solid", "fa-code"], [1, "streaming-content", 3, "innerHTML"], [1, "cursor"], [1, "fa-solid", "fa-exclamation-triangle"], [1, "payload-section"], [1, "payload-header", 3, "click"], [1, "payload-label"], ["type", "button", 1, "payload-toggle"], [1, "payload-content"], [2, "height", "300px", "width", "100%", 3, "ngModel", "language", "readonly"], [1, "tab-button", 3, "click"], [1, "fa-solid", "fa-diagram-project"], [1, "fa-solid", "fa-sliders"], [1, "tab-header"], [1, "variables-list"], [1, "empty-variables"], [1, "variable-item"], [1, "add-button", 3, "click", "disabled"], [1, "fa-solid", "fa-plus"], ["placeholder", "Variable name", 1, "variable-name", 3, "ngModelChange", "ngModel", "disabled"], ["textField", "text", "valueField", "value", 1, "variable-type", 3, "ngModelChange", "ngModel", "data", "valuePrimitive", "disabled"], [1, "variable-value", 3, "ngModelChange", "ngModel", "placeholder", "disabled"], ["title", "Remove variable", 1, "remove-button", 3, "click", "disabled"], [1, "fa-solid", "fa-times"], [3, "viewRunClick", "mode", "agentRun", "liveSteps", "autoExpand", "runId", "runType"], [1, "add-button", 3, "click"], ["title", "Remove variable", 1, "remove-button", 3, "click"], ["title", "Reset to Prompt Settings", 1, "custom-button", "icon-only", 3, "click"], [1, "fa-solid", "fa-undo"], [1, "model-settings"], [1, "setting-group"], ["textField", "Name", "valueField", "ID", "placeholder", "Select a model...", 1, "model-selector", 3, "ngModelChange", "ngModel", "data", "valuePrimitive"], ["textField", "Name", "valueField", "ID", "placeholder", "Select a vendor...", 1, "vendor-selector", 3, "ngModelChange", "ngModel", "data", "valuePrimitive", "disabled"], [1, "setting-help"], ["textField", "text", "valueField", "value", "placeholder", "Select format...", 1, "format-selector", 3, "ngModelChange", "ngModel", "data"], ["placeholder", "Max tokens...", 1, "max-tokens-input", 3, "ngModelChange", "ngModel", "min", "max", "step"], [1, "checkbox-label"], ["type", "checkbox", 1, "skip-validation-checkbox", 3, "ngModelChange", "ngModel"], ["title", "Advanced Parameters", 1, "advanced-params-panel", 3, "action", "expanded"], [1, "advanced-settings"], ["title", "Controls randomness: 0 = more focused and deterministic, 2 = more random and creative"], ["placeholder", "None", 1, "advanced-param-input", 3, "ngModelChange", "ngModel", "min", "max", "step", "format"], ["title", "Nucleus sampling: only consider tokens with cumulative probability up to this value. 1 = consider all tokens"], ["title", "Only sample from the top K tokens. Lower values reduce randomness"], ["title", "Minimum probability for token selection. Filters out low-probability tokens"], ["title", "Penalizes tokens based on their frequency in the text. Positive values reduce repetition"], ["title", "Penalizes tokens that have appeared in the text. Positive values increase topic diversity"], ["title", "Random seed for deterministic generation. Same seed produces same output"], ["placeholder", "None", 1, "advanced-param-input", 3, "ngModelChange", "ngModel", "format"], ["title", "Sequences that will stop generation when encountered. Enter comma-separated values"], ["placeholder", "Enter comma-separated sequences", 1, "advanced-param-input", 3, "ngModelChange", "ngModel", "rows"], ["title", "Include log probabilities of the output tokens in the response"], [1, "advanced-param-switch", 3, "ngModelChange", "ngModel"], ["title", "Number of top log probabilities to include for each token"], ["placeholder", "2", 1, "advanced-param-input", 3, "ngModelChange", "ngModel", "min", "max", "step", "format"], ["title", "Start a new conversation", 1, "custom-button", "small", "primary"], [1, "saved-list"], [1, "empty-saved"], [1, "saved-item", 3, "current"], ["title", "Start a new conversation", 1, "custom-button", "small", "primary", 3, "click"], [1, "fa-solid", "fa-folder-open"], [1, "saved-item", 3, "click"], [1, "saved-info"], [1, "saved-date"], ["title", "Delete conversation", 1, "delete-button", 3, "click"], [1, "fa-solid", "fa-trash"], ["title", "View execution details", 1, "execution-history-toggle"], ["title", "View full run details", 1, "execution-history-toggle"], [1, "fa-solid", "fa-robot"], ["title", "View execution details", 1, "execution-history-toggle", 3, "click"], ["title", "View full run details", 1, "execution-history-toggle", 3, "click"], [1, "fa-solid", "fa-external-link-alt"], [1, "streaming-content"], [1, "save-dialog", 3, "close", "title", "width"], [1, "dialog-content"], ["type", "text", "placeholder", "Conversation name", 1, "k-textbox", "k-input", "k-input-md", "k-rounded-md", "k-input-solid", "full-width", 3, "keyup"], ["kendoButton", "", 3, "click"], ["kendoButton", "", 3, "click", "primary", "disabled"], [1, "confirm-dialog", 3, "close", "title", "width"], ["kendoButton", "", 3, "click", "primary"]], template: function AITestHarnessComponent_Template(rf, ctx) { if (rf & 1) {
|
|
3422
3514
|
i0.ɵɵtemplate(0, AITestHarnessComponent_div_0_Template, 6, 2, "div", 4)(1, AITestHarnessComponent_Conditional_1_Template, 11, 4, "kendo-dialog", 5)(2, AITestHarnessComponent_Conditional_2_Template, 11, 3, "kendo-dialog", 6);
|
|
3423
3515
|
} if (rf & 2) {
|
|
3424
3516
|
i0.ɵɵproperty("ngIf", ctx.isVisible);
|
|
@@ -3430,7 +3522,7 @@ export class AITestHarnessComponent {
|
|
|
3430
3522
|
}
|
|
3431
3523
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AITestHarnessComponent, [{
|
|
3432
3524
|
type: Component,
|
|
3433
|
-
args: [{ selector: 'mj-ai-test-harness', template: "<div class=\"harness-container\" *ngIf=\"isVisible\" kendoWindowContainer>\n <!-- Header -->\n <div class=\"harness-header\">\n @if (entity) {\n <div class=\"agent-info\">\n <div class=\"agent-details\">\n @if (hasEntityLogo()) {\n <img [src]=\"getEntityLogoURL()\" [alt]=\"getEntityName() + ' logo'\" class=\"entity-logo\">\n } @else {\n <i [class]=\"getEntityIconClass()\"></i>\n }\n <div>\n <h3>{{ getEntityName() }}</h3>\n @if (entity.Description) {\n <p>{{ entity.Description }}</p>\n }\n </div>\n </div>\n <div class=\"header-actions\">\n <button \n class=\"custom-button\"\n (click)=\"clearConversation()\"\n [disabled]=\"isExecuting || conversationMessages.length === 0\"\n title=\"Clear conversation\">\n <i class=\"fa-solid fa-broom\"></i>\n Clear\n </button>\n <button \n class=\"custom-button\"\n (click)=\"saveConversation()\"\n [disabled]=\"conversationMessages.length === 0\"\n title=\"Save conversation\">\n <i class=\"fa-solid fa-save\"></i>\n Save\n </button>\n <button \n class=\"custom-button\"\n (click)=\"exportConversation()\"\n [disabled]=\"conversationMessages.length === 0\"\n title=\"Export conversation\">\n <i class=\"fa-solid fa-download\"></i>\n Export\n </button>\n <button \n class=\"custom-button\"\n (click)=\"importConversation()\"\n title=\"Import conversation\">\n <i class=\"fa-solid fa-upload\"></i>\n Import\n </button>\n <input \n #fileInput\n type=\"file\"\n accept=\".json\"\n style=\"display: none\"\n (change)=\"onFileSelected($event)\">\n <button \n class=\"custom-button icon-only\"\n (click)=\"toggleSidebar()\"\n title=\"Toggle sidebar\">\n <i class=\"fa-solid\" [class.fa-chevron-right]=\"!showSidebar\" [class.fa-chevron-left]=\"showSidebar\"></i>\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Main Content Area with Splitter -->\n <div class=\"harness-body\">\n @if (showSidebar) {\n <kendo-splitter \n orientation=\"horizontal\"\n style=\"height: 100%; width: 100%;\">\n \n <!-- Chat Area (Left Pane) -->\n <kendo-splitter-pane \n [collapsible]=\"false\"\n [resizable]=\"true\"\n [min]=\"'400px'\">\n <div class=\"chat-area\">\n <!-- Messages Container -->\n <div class=\"messages-container\" #messagesContainer>\n @if (conversationMessages.length === 0) {\n <div class=\"empty-state\">\n <i class=\"fa-solid fa-comments\"></i>\n <h4>Start a conversation</h4>\n <p>Send a message to begin testing the AI agent</p>\n </div>\n }\n\n @for (message of conversationMessages; track message.id) {\n <div [ngClass]=\"getMessageClass(message)\">\n <div class=\"message-header\">\n <span class=\"message-role\">\n @if (message.role === 'user') {\n <i class=\"fa-solid fa-user\"></i> You\n } @else {\n @if (hasEntityLogo()) {\n <img [src]=\"getEntityLogoURL()\" [alt]=\"getEntityName() + ' logo'\" class=\"message-role-logo\">\n } @else {\n <i [class]=\"getEntityIconClass()\"></i>\n }\n {{ getEntityName() || 'Assistant' }}\n }\n </span>\n <span class=\"message-time\">{{ formatTimestamp(message.timestamp) }}</span>\n @if (message.isStreaming && message.elapsedTime !== undefined) {\n <span class=\"execution-time streaming\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i> {{ formatElapsedTime(message.elapsedTime) }}\n </span>\n } @else if (message.executionTime) {\n <span class=\"execution-time\" [title]=\"getExecutionSummary(message)\">{{ formatExecutionTime(message.executionTime) }}</span>\n }\n @if (showRawToggle(message)) {\n <button class=\"raw-toggle\" (click)=\"showRawJsonDialog(message)\" title=\"View raw JSON response\">\n <i class=\"fa-solid fa-code\"></i>\n </button>\n }\n </div>\n <div class=\"message-content\">\n <button class=\"copy-button-in-bubble\" (click)=\"copyMessage(message)\" title=\"Copy message\">\n <i class=\"fa-solid fa-copy\"></i>\n </button>\n @if (message.isStreaming) {\n <div class=\"streaming-wrapper\">\n <span class=\"streaming-content\" [innerHTML]=\"getFormattedStreamingContent(message)\"></span>\n <span class=\"cursor\">|</span>\n </div>\n } @else {\n <div [innerHTML]=\"getFormattedContent(message)\"></div>\n }\n @if (message.error) {\n <div class=\"error-details\">\n <i class=\"fa-solid fa-exclamation-triangle\"></i>\n {{ message.error }}\n </div>\n }\n @if (hasPayload(message.payload) && mode === 'agent' && message.role === 'assistant') {\n <div class=\"payload-section\" [class.collapsed]=\"message.payloadCollapsed\">\n <div class=\"payload-header\" (click)=\"togglePayloadCollapse(message)\">\n <span class=\"payload-label\">\n <i class=\"fa-solid fa-code\"></i> Payload\n </span>\n <button class=\"payload-toggle\" type=\"button\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!message.payloadCollapsed\" [class.fa-chevron-right]=\"message.payloadCollapsed\"></i>\n </button>\n </div>\n @if (!message.payloadCollapsed) {\n <div class=\"payload-content\">\n <mj-code-editor\n [ngModel]=\"formatJson(message.payload)\"\n [language]=\"'json'\"\n [readonly]=\"true\"\n style=\"height: 300px; width: 100%;\">\n </mj-code-editor>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Input Area -->\n <div class=\"input-area\">\n <div class=\"input-container\">\n <kendo-textarea\n #messageInput\n [(ngModel)]=\"currentUserMessage\"\n [rows]=\"3\"\n placeholder=\"Type your message here... (Press Enter to send, Shift+Enter for new line)\"\n (keypress)=\"handleKeyPress($event)\"\n [disabled]=\"isExecuting\"\n class=\"message-input\">\n </kendo-textarea>\n <button\n class=\"send-button\"\n (click)=\"sendMessage()\"\n [disabled]=\"isExecuting || !currentUserMessage.trim()\"\n title=\"Send message\">\n @if (isExecuting) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n } @else {\n <i class=\"fa-solid fa-paper-plane\"></i>\n }\n </button>\n </div>\n </div>\n </div>\n </kendo-splitter-pane>\n \n <!-- Sidebar (Right Pane) -->\n <kendo-splitter-pane \n [collapsible]=\"false\"\n [resizable]=\"true\"\n [size]=\"'30%'\"\n [min]=\"'300px'\">\n <div class=\"sidebar\">\n <!-- Tab Navigation -->\n <div class=\"sidebar-tabs\">\n @if (mode === 'agent') {\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'agentVariables'\"\n [disabled]=\"executionMonitorMode === 'live' && activeTab !== 'agentVariables'\"\n (click)=\"selectTab('agentVariables')\">\n <i class=\"fa-solid fa-code\"></i>\n Agent Variables\n </button>\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'executionMonitor'\"\n (click)=\"selectTab('executionMonitor')\">\n <i class=\"fa-solid fa-diagram-project\"></i>\n Execution Monitor\n </button>\n } @else {\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'templateVariables'\"\n (click)=\"selectTab('templateVariables')\">\n <i class=\"fa-solid fa-code\"></i>\n Template Variables\n </button>\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'modelSettings'\"\n (click)=\"selectTab('modelSettings')\">\n <i class=\"fa-solid fa-sliders\"></i>\n Model Settings\n </button>\n }\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'savedConversations'\"\n [disabled]=\"executionMonitorMode === 'live' && activeTab !== 'savedConversations'\"\n (click)=\"selectTab('savedConversations')\">\n <i class=\"fa-solid fa-history\"></i>\n Saved\n @if (savedConversations.length > 0) {\n <span class=\"badge\">{{ savedConversations.length }}</span>\n }\n </button>\n </div>\n\n <!-- Tab Content -->\n <div class=\"sidebar-content\">\n <!-- Agent Variables Tab (Unified) -->\n @if (activeTab === 'agentVariables') {\n <div class=\"tab-content\">\n <div class=\"tab-header\">\n <h4>Agent Variables</h4>\n <p>Variables available during agent execution and template rendering</p>\n </div>\n \n <div class=\"variables-list\">\n @if (agentVariables.length === 0) {\n <div class=\"empty-variables\">\n <p>No variables defined. Click \"Add Variable\" to get started.</p>\n </div>\n }\n \n @for (variable of agentVariables; track $index) {\n <div class=\"variable-item\">\n <kendo-textbox \n [(ngModel)]=\"variable.name\" \n placeholder=\"Variable name\"\n [disabled]=\"isExecuting\"\n class=\"variable-name\">\n </kendo-textbox>\n \n <kendo-dropdownlist \n [(ngModel)]=\"variable.type\"\n [data]=\"[\n {text: 'String', value: 'string'}, \n {text: 'Number', value: 'number'}, \n {text: 'Boolean', value: 'boolean'}, \n {text: 'Object', value: 'object'}\n ]\"\n textField=\"text\"\n valueField=\"value\"\n [valuePrimitive]=\"true\"\n [disabled]=\"isExecuting\"\n class=\"variable-type\">\n </kendo-dropdownlist>\n \n <kendo-textbox \n [(ngModel)]=\"variable.value\" \n [placeholder]=\"variable.type === 'object' ? 'JSON value' : 'Value'\"\n [disabled]=\"isExecuting\"\n class=\"variable-value\">\n </kendo-textbox>\n \n <button \n class=\"remove-button\"\n (click)=\"removeAgentVariable($index)\"\n [disabled]=\"isExecuting\"\n title=\"Remove variable\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n </div>\n \n <button \n class=\"add-button\"\n (click)=\"addAgentVariable()\"\n [disabled]=\"isExecuting\">\n <i class=\"fa-solid fa-plus\"></i> Add Variable\n </button>\n </div>\n }\n\n <!-- Execution Monitor Tab -->\n @if (activeTab === 'executionMonitor') {\n <div class=\"tab-content\" style=\"padding: 0; height: 100%;\">\n <mj-agent-execution-monitor\n [mode]=\"executionMonitorMode\"\n [executionData]=\"currentExecutionData\"\n [autoExpand]=\"true\"\n [runId]=\"getLastRunId()\"\n [runType]=\"mode\"\n (viewRunClick)=\"navigateToRun($event)\">\n </mj-agent-execution-monitor>\n </div>\n }\n\n <!-- Template Variables Tab (Prompt Mode) -->\n @if (activeTab === 'templateVariables') {\n <div class=\"tab-content\">\n <div class=\"tab-header\">\n <h4>Template Variables</h4>\n <p>Variables for prompt template rendering</p>\n </div>\n \n <div class=\"variables-list\">\n @for (variable of templateVariables; track $index) {\n <div class=\"variable-item\">\n <kendo-textbox \n [(ngModel)]=\"variable.name\" \n placeholder=\"Variable name\"\n [disabled]=\"isExecuting\"\n class=\"variable-name\">\n </kendo-textbox>\n \n <kendo-dropdownlist \n [(ngModel)]=\"variable.type\"\n [data]=\"[\n {text: 'String', value: 'string'}, \n {text: 'Number', value: 'number'}, \n {text: 'Boolean', value: 'boolean'}, \n {text: 'Object', value: 'object'}\n ]\"\n textField=\"text\"\n valueField=\"value\"\n [valuePrimitive]=\"true\"\n [disabled]=\"isExecuting\"\n class=\"variable-type\">\n </kendo-dropdownlist>\n \n <kendo-textbox \n [(ngModel)]=\"variable.value\" \n [placeholder]=\"variable.type === 'object' ? 'JSON value' : 'Value'\"\n [disabled]=\"isExecuting\"\n class=\"variable-value\">\n </kendo-textbox>\n \n <button \n class=\"remove-button\"\n (click)=\"removeTemplateVariable($index)\"\n title=\"Remove variable\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n </div>\n \n <button \n class=\"add-button\"\n (click)=\"addTemplateVariable()\">\n <i class=\"fa-solid fa-plus\"></i>\n Add Variable\n </button>\n </div>\n }\n\n <!-- Model Settings Tab (Prompt Mode) -->\n @if (activeTab === 'modelSettings') {\n <div class=\"tab-content\" style=\"overflow-y: auto;\">\n <div class=\"tab-header\">\n <h4>Model Settings</h4>\n <p>Configure AI model parameters</p>\n <button class=\"custom-button icon-only\" (click)=\"resetToPromptDefaults()\" title=\"Reset to Prompt Settings\">\n <i class=\"fa-solid fa-undo\"></i>\n </button>\n </div>\n \n <div class=\"model-settings\">\n <div class=\"setting-group\">\n <label>AI Model</label>\n <kendo-dropdownlist \n [(ngModel)]=\"selectedModelId\"\n (ngModelChange)=\"onModelSelectionChange()\"\n [data]=\"availableModels\"\n textField=\"Name\"\n valueField=\"ID\"\n [valuePrimitive]=\"true\"\n placeholder=\"Select a model...\"\n class=\"model-selector\">\n </kendo-dropdownlist>\n </div>\n \n <div class=\"setting-group\">\n <label>AI Vendor</label>\n <kendo-dropdownlist \n [(ngModel)]=\"selectedVendorId\"\n [data]=\"availableVendors\"\n textField=\"Name\"\n valueField=\"ID\"\n [valuePrimitive]=\"true\"\n [disabled]=\"availableVendors.length <= 1 || !selectedModelId\"\n placeholder=\"Select a vendor...\"\n class=\"vendor-selector\">\n </kendo-dropdownlist>\n <div class=\"setting-help\">\n @if (!selectedModelId) {\n Using default vendor for the default model\n } @else if (availableVendors.length <= 1) {\n Only one vendor available for this model\n } @else {\n Select the inference provider for this model\n }\n </div>\n </div>\n \n <div class=\"setting-group\">\n <label>Response Format</label>\n <kendo-dropdownlist \n [(ngModel)]=\"selectedResponseFormat\"\n [data]=\"responseFormatOptions\"\n textField=\"text\"\n valueField=\"value\"\n placeholder=\"Select format...\"\n class=\"format-selector\">\n </kendo-dropdownlist>\n <div class=\"setting-help\">\n Expected format for AI response (defaults to prompt setting)\n </div>\n </div>\n \n <div class=\"setting-group\">\n <label>Max Tokens</label>\n <kendo-numerictextbox \n [(ngModel)]=\"maxTokens\"\n [min]=\"100\"\n [max]=\"8000\"\n [step]=\"100\"\n placeholder=\"Max tokens...\"\n class=\"max-tokens-input\">\n </kendo-numerictextbox>\n <div class=\"setting-help\">\n Maximum length of generated response\n </div>\n </div>\n \n <div class=\"setting-group\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" \n [(ngModel)]=\"skipValidation\"\n class=\"skip-validation-checkbox\">\n Skip Validation\n </label>\n <div class=\"setting-help\">\n Skip output validation during testing\n </div>\n </div>\n \n <!-- Advanced Parameters Expansion Panel -->\n <kendo-expansionpanel \n [expanded]=\"advancedParamsExpanded\"\n (action)=\"toggleAdvancedParams()\"\n title=\"Advanced Parameters\"\n class=\"advanced-params-panel\">\n <div class=\"advanced-settings\">\n <div class=\"setting-group\">\n <label title=\"Controls randomness: 0 = more focused and deterministic, 2 = more random and creative\">\n Temperature\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.temperature\"\n [min]=\"0\"\n [max]=\"2\"\n [step]=\"0.1\"\n [format]=\"'n1'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Nucleus sampling: only consider tokens with cumulative probability up to this value. 1 = consider all tokens\">\n Top P\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.topP\"\n [min]=\"0\"\n [max]=\"1\"\n [step]=\"0.01\"\n [format]=\"'n2'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Only sample from the top K tokens. Lower values reduce randomness\">\n Top K\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.topK\"\n [min]=\"1\"\n [max]=\"100\"\n [step]=\"1\"\n [format]=\"'n0'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Minimum probability for token selection. Filters out low-probability tokens\">\n Min P\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.minP\"\n [min]=\"0\"\n [max]=\"1\"\n [step]=\"0.01\"\n [format]=\"'n2'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Penalizes tokens based on their frequency in the text. Positive values reduce repetition\">\n Frequency Penalty\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.frequencyPenalty\"\n [min]=\"-2\"\n [max]=\"2\"\n [step]=\"0.1\"\n [format]=\"'n1'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Penalizes tokens that have appeared in the text. Positive values increase topic diversity\">\n Presence Penalty\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.presencePenalty\"\n [min]=\"-2\"\n [max]=\"2\"\n [step]=\"0.1\"\n [format]=\"'n1'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Random seed for deterministic generation. Same seed produces same output\">\n Seed\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.seed\"\n [format]=\"'n0'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Sequences that will stop generation when encountered. Enter comma-separated values\">\n Stop Sequences\n </label>\n <kendo-textarea \n [(ngModel)]=\"stopSequencesText\"\n (ngModelChange)=\"updateStopSequences()\"\n [rows]=\"2\"\n placeholder=\"Enter comma-separated sequences\"\n class=\"advanced-param-input\">\n </kendo-textarea>\n <div class=\"setting-help\">\n Separate multiple sequences with commas\n </div>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Include log probabilities of the output tokens in the response\">\n Include Log Probs\n </label>\n <kendo-switch \n [(ngModel)]=\"advancedParams.includeLogProbs\"\n class=\"advanced-param-switch\">\n </kendo-switch>\n </div>\n \n @if (advancedParams.includeLogProbs) {\n <div class=\"setting-group\">\n <label title=\"Number of top log probabilities to include for each token\">\n Top Log Probs\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.topLogProbs\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\"\n [format]=\"'n0'\"\n placeholder=\"2\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n }\n </div>\n </kendo-expansionpanel>\n </div>\n </div>\n }\n\n <!-- Saved Conversations Tab -->\n @if (activeTab === 'savedConversations') {\n <div class=\"tab-content\">\n <div class=\"tab-header\">\n <h4>Saved Conversations</h4>\n <p>Previously saved test sessions</p>\n @if (currentConversationId) {\n <button class=\"custom-button small primary\" (click)=\"newConversation()\" title=\"Start a new conversation\">\n <i class=\"fa-solid fa-plus\"></i>\n New\n </button>\n }\n </div>\n \n <div class=\"saved-list\">\n @if (savedConversations.length === 0) {\n <div class=\"empty-saved\">\n <i class=\"fa-solid fa-folder-open\"></i>\n <p>No saved conversations</p>\n </div>\n }\n \n @for (conv of savedConversations; track conv.id) {\n <div class=\"saved-item\" \n [class.current]=\"conv.id === currentConversationId\"\n (click)=\"loadConversation(conv)\">\n <div class=\"saved-info\">\n <h5>{{ conv.name }}</h5>\n <p>{{ conv.messages.length }} messages</p>\n <span class=\"saved-date\">{{ conv.updatedAt | date:'short' }}</span>\n </div>\n <button \n class=\"delete-button\"\n (click)=\"deleteConversation(conv, $event)\"\n title=\"Delete conversation\">\n <i class=\"fa-solid fa-trash\"></i>\n </button>\n </div>\n }\n </div>\n </div>\n }\n </div>\n </div>\n </kendo-splitter-pane>\n </kendo-splitter>\n } @else {\n <!-- No sidebar - full width chat -->\n <div class=\"chat-area\">\n <!-- Messages Container -->\n <div class=\"messages-container\" #messagesContainer>\n @if (conversationMessages.length === 0) {\n <div class=\"empty-state\">\n <i class=\"fa-solid fa-comments\"></i>\n <h4>Start a conversation</h4>\n <p>Send a message to begin testing the AI agent</p>\n </div>\n }\n\n @for (message of conversationMessages; track message.id) {\n <div [ngClass]=\"getMessageClass(message)\">\n <div class=\"message-header\">\n <span class=\"message-role\">\n @if (message.role === 'user') {\n <i class=\"fa-solid fa-user\"></i> You\n } @else {\n <i class=\"fa-solid fa-robot\"></i> {{ getEntityName() || 'Assistant' }}\n }\n </span>\n <span class=\"message-time\">{{ formatTimestamp(message.timestamp) }}</span>\n @if (message.isStreaming && message.elapsedTime !== undefined) {\n <span class=\"execution-time streaming\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i> {{ formatElapsedTime(message.elapsedTime) }}\n </span>\n } @else if (message.executionTime) {\n <span class=\"execution-time\" [title]=\"getExecutionSummary(message)\">{{ formatExecutionTime(message.executionTime) }}</span>\n }\n @if (showRawToggle(message)) {\n <button class=\"raw-toggle\" (click)=\"showRawJsonDialog(message)\" title=\"View raw JSON response\">\n <i class=\"fa-solid fa-code\"></i>\n </button>\n }\n @if (message.executionData && mode === 'agent' && message.role === 'assistant') {\n <button class=\"execution-history-toggle\" (click)=\"showMessageExecutionHistory(message)\" title=\"View execution details\">\n <i class=\"fa-solid fa-diagram-project\"></i>\n </button>\n }\n @if (message.agentRunId && mode === 'agent' && message.role === 'assistant') {\n <button class=\"execution-history-toggle\" (click)=\"navigateToAgentRun(message.agentRunId)\" title=\"View full run details\">\n <i class=\"fa-solid fa-external-link-alt\"></i>\n </button>\n }\n </div>\n <div class=\"message-content\">\n <button class=\"copy-button-in-bubble\" (click)=\"copyMessage(message)\" title=\"Copy message\">\n <i class=\"fa-solid fa-copy\"></i>\n </button>\n @if (message.isStreaming) {\n <div class=\"streaming-wrapper\">\n <span class=\"streaming-content\">{{ message.streamingContent }}<span class=\"cursor\">|</span></span>\n </div>\n } @else {\n <div [innerHTML]=\"getFormattedContent(message)\"></div>\n }\n @if (message.error) {\n <div class=\"error-details\">\n <i class=\"fa-solid fa-exclamation-triangle\"></i>\n {{ message.error }}\n </div>\n }\n @if (hasPayload(message.payload) && mode === 'agent' && message.role === 'assistant') {\n <div class=\"payload-section\" [class.collapsed]=\"message.payloadCollapsed\">\n <div class=\"payload-header\" (click)=\"togglePayloadCollapse(message)\">\n <span class=\"payload-label\">\n <i class=\"fa-solid fa-code\"></i> Payload\n </span>\n <button class=\"payload-toggle\" type=\"button\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!message.payloadCollapsed\" [class.fa-chevron-right]=\"message.payloadCollapsed\"></i>\n </button>\n </div>\n @if (!message.payloadCollapsed) {\n <div class=\"payload-content\">\n <pre>{{ formatJson(message.payload) }}</pre>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Input Area -->\n <div class=\"input-area\">\n <div class=\"input-container\">\n <kendo-textarea\n #messageInput\n [(ngModel)]=\"currentUserMessage\"\n [rows]=\"3\"\n placeholder=\"Type your message here... (Press Enter to send, Shift+Enter for new line)\"\n (keypress)=\"handleKeyPress($event)\"\n [disabled]=\"isExecuting\"\n class=\"message-input\">\n </kendo-textarea>\n <button\n class=\"send-button\"\n (click)=\"sendMessage()\"\n [disabled]=\"isExecuting || !currentUserMessage.trim()\"\n title=\"Send message\">\n @if (isExecuting) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n } @else {\n <i class=\"fa-solid fa-paper-plane\"></i>\n }\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Save Conversation Dialog -->\n @if (showSaveDialog) {\n <kendo-dialog \n [title]=\"'Save Conversation'\" \n [width]=\"400\"\n (close)=\"cancelSaveDialog()\"\n class=\"save-dialog\">\n <div class=\"dialog-content\">\n <p>Enter a name for this conversation:</p>\n <input \n #saveDialogInput\n type=\"text\"\n (keyup)=\"updateTempConversation()\"\n placeholder=\"Conversation name\"\n class=\"k-textbox k-input k-input-md k-rounded-md k-input-solid full-width\" />\n </div>\n <kendo-dialog-actions>\n <button kendoButton (click)=\"cancelSaveDialog()\">Cancel</button>\n <button kendoButton \n [primary]=\"true\" \n (click)=\"confirmSaveConversation()\"\n [disabled]=\"tempConversationName === ''\">\n Save\n </button>\n </kendo-dialog-actions>\n </kendo-dialog>\n }\n \n <!-- Load Confirmation Dialog -->\n @if (showLoadConfirmDialog) {\n <kendo-dialog \n [title]=\"'Replace Current Conversation?'\" \n [width]=\"400\"\n (close)=\"cancelLoadConversation()\"\n class=\"confirm-dialog\">\n <div class=\"dialog-content\">\n <p>Loading this conversation will replace the current one. Any unsaved changes will be lost.</p>\n <p>Do you want to continue?</p>\n </div>\n <kendo-dialog-actions>\n <button kendoButton (click)=\"cancelLoadConversation()\">Cancel</button>\n <button kendoButton \n [primary]=\"true\" \n (click)=\"confirmLoadConversation()\">\n Load Conversation\n </button>\n </kendo-dialog-actions>\n </kendo-dialog>\n }\n\n", styles: ["/* Container Styles */\n.harness-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: #f8f9fa;\n}\n\n/* Custom Button Styles */\n.custom-button {\n background: white;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 13px;\n color: #495057;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n}\n\n.custom-button:hover:not(:disabled) {\n background: #f8f9fa;\n border-color: #adb5bd;\n color: #212529;\n}\n\n.custom-button:active:not(:disabled) {\n background: #e9ecef;\n border-color: #adb5bd;\n}\n\n.custom-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.custom-button.icon-only {\n padding: 6px 8px;\n}\n\n.custom-button i {\n font-size: 14px;\n}\n\n/* Send Button - Primary Style */\n.send-button {\n background: #0d6efd;\n border: 1px solid #0d6efd;\n color: white;\n border-radius: 4px;\n padding: 8px 16px;\n font-size: 14px;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 44px;\n font-family: inherit;\n}\n\n.send-button:hover:not(:disabled) {\n background: #0b5ed7;\n border-color: #0a58ca;\n}\n\n.send-button:active:not(:disabled) {\n background: #0a58ca;\n border-color: #0a58ca;\n}\n\n.send-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.send-button i {\n font-size: 16px;\n}\n\n/* Add Button */\n.add-button {\n background: white;\n border: 1px solid #0d6efd;\n color: #0d6efd;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 13px;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n}\n\n.add-button:hover {\n background: #0d6efd;\n color: white;\n}\n\n/* Remove/Delete Buttons */\n.remove-button,\n.delete-button {\n background: transparent;\n border: none;\n color: #dc3545;\n padding: 4px 8px;\n font-size: 12px;\n cursor: pointer;\n transition: all 0.2s;\n border-radius: 4px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.remove-button:hover,\n.delete-button:hover {\n background: rgba(220, 53, 69, 0.1);\n color: #bd2130;\n}\n\n/* Copy Button in Message Bubble */\n.copy-button-in-bubble {\n position: absolute;\n top: 8px;\n right: 8px;\n background: transparent;\n border: none;\n padding: 6px;\n font-size: 14px;\n color: #adb5bd;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n opacity: 0;\n z-index: 10; /* Ensure it's above content */\n}\n\n.message-content:hover .copy-button-in-bubble {\n opacity: 1;\n}\n\n.copy-button-in-bubble:hover {\n background: rgba(0, 0, 0, 0.08);\n color: #6c757d;\n}\n\n.copy-button-in-bubble i {\n font-size: 14px;\n}\n\n/* Model Settings Styles (Prompt Mode) */\n.model-settings {\n padding: 16px;\n}\n\n.setting-group {\n margin-bottom: 24px;\n}\n\n.setting-group label {\n display: block;\n margin-bottom: 8px;\n font-weight: 600;\n color: #495057;\n font-size: 14px;\n}\n\n.model-selector,\n.vendor-selector {\n width: 100%;\n}\n\n.temperature-slider {\n width: 100%;\n margin-top: 8px;\n}\n\n.max-tokens-input {\n width: 100%;\n}\n\n.setting-help {\n margin-top: 6px;\n font-size: 12px;\n color: #6c757d;\n font-style: italic;\n}\n\n/* Checkbox Styles */\n.checkbox-label {\n display: flex;\n align-items: center;\n font-weight: 600;\n color: #495057;\n font-size: 14px;\n cursor: pointer;\n}\n\n.skip-validation-checkbox {\n margin-right: 8px;\n cursor: pointer;\n width: 16px;\n height: 16px;\n}\n\n/* Header Styles */\n.harness-header {\n background: white;\n border-bottom: 1px solid #dee2e6;\n padding: 16px 20px;\n}\n\n.agent-info {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.agent-details {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.agent-details i {\n font-size: 28px;\n color: #17a2b8;\n}\n\n.agent-details .entity-logo {\n width: 40px;\n height: 40px;\n object-fit: cover;\n border-radius: 8px;\n}\n\n.agent-details h3 {\n margin: 0;\n font-size: 20px;\n font-weight: 600;\n color: #212529;\n}\n\n.agent-details p {\n margin: 4px 0 0 0;\n font-size: 14px;\n color: #6c757d;\n}\n\n.header-actions {\n display: flex;\n gap: 8px;\n}\n\n/* Body Layout */\n.harness-body {\n flex: 1;\n display: flex;\n overflow: hidden;\n}\n\n/* Chat Area */\n.chat-area {\n flex: 1;\n display: flex;\n flex-direction: column;\n background: white;\n border-right: 1px solid #dee2e6;\n height: 100%;\n min-height: 0; /* Important for flexbox children */\n}\n\n/* Messages Container */\n.messages-container {\n flex: 1;\n overflow-y: auto;\n padding: 20px;\n scroll-behavior: smooth;\n}\n\n.empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n text-align: center;\n color: #6c757d;\n}\n\n.empty-state i {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.empty-state h4 {\n margin: 0 0 8px 0;\n font-size: 18px;\n font-weight: 500;\n}\n\n.empty-state p {\n margin: 0;\n font-size: 14px;\n}\n\n/* Message Styles */\n.message {\n margin-bottom: 20px;\n animation: fadeIn 0.3s ease-in;\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.message-user {\n margin-left: auto;\n max-width: 70%;\n}\n\n.message-assistant {\n margin-right: auto;\n max-width: 70%;\n}\n\n.message-header {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 8px;\n font-size: 13px;\n}\n\n.message-role {\n font-weight: 600;\n color: #495057;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.message-role i {\n font-size: 14px;\n}\n\n.message-role .message-role-logo {\n width: 16px;\n height: 16px;\n object-fit: cover;\n border-radius: 3px;\n display: inline-block;\n vertical-align: middle;\n}\n\n.message-user .message-role {\n color: #6f42c1;\n}\n\n.message-assistant .message-role {\n color: #17a2b8;\n}\n\n.message-time {\n color: #6c757d;\n font-size: 12px;\n}\n\n.execution-time {\n background: #e9ecef;\n padding: 2px 8px;\n border-radius: 12px;\n font-size: 11px;\n color: #495057;\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.execution-time.streaming {\n background: #fff3cd;\n color: #856404;\n border: 1px solid #ffeaa7;\n}\n\n.raw-toggle {\n background: transparent;\n border: none;\n padding: 4px 8px;\n font-size: 12px;\n color: #6c757d;\n cursor: pointer;\n transition: all 0.2s;\n margin-left: 8px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n}\n\n.raw-toggle:hover {\n background: rgba(0, 0, 0, 0.05);\n color: #495057;\n}\n\n.raw-toggle i {\n font-size: 12px;\n}\n\n/* Message content styles are now defined above in enhanced content formatting section */\n\n.message-user .message-content {\n background: #e7f3ff;\n border: 1px solid #b8daff;\n}\n\n.message-assistant .message-content {\n background: #f8f9fa;\n border: 1px solid #e9ecef;\n}\n\n.message.streaming .message-content {\n background: #fff3cd;\n border: 1px solid #ffeaa7;\n}\n\n.streaming-content {\n display: inline;\n}\n\n.streaming-wrapper {\n position: relative;\n}\n\n.streaming-wrapper .cursor {\n display: inline;\n margin-left: 2px;\n}\n\n.cursor {\n animation: blink 1s infinite;\n font-weight: 100;\n color: #495057;\n}\n\n@keyframes blink {\n 0%, 50% { opacity: 1; }\n 51%, 100% { opacity: 0; }\n}\n\n.error-details {\n margin-top: 8px;\n padding: 8px 12px;\n background: #f8d7da;\n border: 1px solid #f5c6cb;\n border-radius: 6px;\n color: #721c24;\n font-size: 13px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.error-details i {\n color: #dc3545;\n}\n\n/* Payload Section Styles */\n.payload-section {\n margin-top: 12px;\n border: 1px solid #e9ecef;\n border-radius: 6px;\n overflow: hidden;\n background: #ffffff;\n}\n\n.payload-header {\n background: #f8f9fa;\n padding: 8px 12px;\n border-bottom: 1px solid #e9ecef;\n display: flex;\n align-items: center;\n justify-content: space-between;\n cursor: pointer;\n transition: background-color 0.2s;\n}\n\n.payload-header:hover {\n background: #e9ecef;\n}\n\n.payload-label {\n font-size: 13px;\n font-weight: 600;\n color: #495057;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.payload-label i {\n color: #6c757d;\n font-size: 12px;\n}\n\n.payload-content {\n max-height: 250px;\n overflow: auto;\n background: #f8f9fa;\n}\n\n.payload-content pre {\n margin: 0;\n padding: 12px;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n font-size: 12px;\n line-height: 1.5;\n color: #212529;\n white-space: pre-wrap;\n word-wrap: break-word;\n background: transparent;\n}\n\n.payload-toggle {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #6c757d;\n transition: color 0.2s;\n}\n\n.payload-toggle:hover {\n color: #495057;\n}\n\n.payload-toggle i {\n font-size: 12px;\n}\n\n.payload-section.collapsed .payload-header {\n border-bottom: none;\n}\n\n/* Dialog styles */\n.save-dialog,\n.confirm-dialog {\n .dialog-content {\n padding: 20px;\n \n p {\n margin: 0 0 16px 0;\n color: #333;\n font-size: 14px;\n line-height: 1.5;\n \n &:last-child {\n margin-bottom: 0;\n }\n }\n \n .full-width {\n width: 100%;\n }\n \n input.k-textbox {\n padding: 8px 12px;\n font-size: 14px;\n line-height: 1.5;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n \n &:focus {\n outline: none;\n border-color: #ff6358;\n box-shadow: 0 0 0 0.25rem rgba(255, 99, 88, 0.25);\n }\n }\n }\n}\n\n/* Enhanced content formatting styles */\n.markdown-content {\n line-height: 1.6;\n word-wrap: break-word;\n overflow-wrap: break-word;\n max-width: 100%;\n}\n\n/* Inline JSON editor styles */\n.inline-json-editor {\n margin: 8px 0;\n \n .json-editor-container {\n background: #f8f9fa;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n overflow: hidden;\n position: relative;\n max-height: 400px;\n \n pre {\n margin: 0;\n padding: 12px;\n font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;\n font-size: 13px;\n line-height: 1.5;\n overflow: auto;\n height: 100%;\n background: transparent;\n color: #333;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n }\n}\n\n.markdown-paragraph {\n margin: 8px 0;\n word-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.markdown-h1 {\n font-size: 1.5em;\n font-weight: 600;\n margin: 16px 0 8px 0;\n color: #212529;\n border-bottom: 2px solid #e9ecef;\n padding-bottom: 4px;\n}\n\n.markdown-h2 {\n font-size: 1.3em;\n font-weight: 600;\n margin: 14px 0 6px 0;\n color: #212529;\n border-bottom: 1px solid #e9ecef;\n padding-bottom: 2px;\n}\n\n.markdown-h3 {\n font-size: 1.1em;\n font-weight: 600;\n margin: 12px 0 4px 0;\n color: #212529;\n}\n\n.code-block {\n background: #f8f9fa;\n border: 1px solid #e9ecef;\n border-radius: 6px;\n padding: 16px;\n margin: 12px 0;\n overflow-x: auto;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n font-size: 14px;\n line-height: 1.5;\n color: #212529;\n white-space: pre;\n word-wrap: normal;\n max-width: 100%;\n}\n\n.code-block code {\n background: none;\n border: none;\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre;\n word-wrap: normal;\n}\n\n.language-json {\n color: #d73e48;\n}\n\n.inline-code {\n background: #f1f3f4;\n border: 1px solid #dadce0;\n border-radius: 4px;\n padding: 2px 6px;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n font-size: 0.9em;\n color: #d73e48;\n white-space: nowrap;\n}\n\n.markdown-list {\n margin: 8px 0;\n padding-left: 24px;\n}\n\n.markdown-list li {\n margin: 4px 0;\n line-height: 1.5;\n}\n\n.markdown-link {\n color: #0d6efd;\n text-decoration: underline;\n transition: color 0.2s;\n}\n\n.markdown-link:hover {\n color: #0a58ca;\n text-decoration: none;\n}\n\n/* Ensure proper text wrapping in message content */\n.message-content {\n background: #f8f9fa;\n border-radius: 12px;\n padding: 12px 40px 12px 16px; /* Extra right padding for copy button */\n font-size: 14px;\n line-height: 1.5;\n word-wrap: break-word;\n position: relative;\n overflow-wrap: break-word;\n hyphens: auto;\n max-width: 100%;\n overflow-x: hidden;\n}\n\n/* Enhanced JSON content display */\n.json-content-wrapper {\n margin: 8px 0;\n}\n\n.json-main-content {\n margin-bottom: 12px;\n}\n\n.json-raw-section {\n margin-top: 12px;\n}\n\n.json-toggle-button {\n background: white;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 12px;\n color: #495057;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n}\n\n.json-toggle-button:hover {\n background: #f8f9fa;\n border-color: #adb5bd;\n color: #212529;\n}\n\n.json-toggle-button i {\n font-size: 12px;\n}\n\n/* Compact code block for JSON in collapsible sections */\n.json-raw-section .code-block {\n margin: 8px 0 0 0;\n font-size: 12px;\n max-height: 300px;\n overflow-y: auto;\n}\n\n/* Better mobile responsiveness for JSON content */\n@media (max-width: 768px) {\n .json-raw-section .code-block {\n font-size: 11px;\n max-height: 200px;\n }\n}\n\n/* JSON raw section styling - simplified since we use code editor in dialog */\n.json-raw-section {\n margin-top: 12px;\n}\n\n/* Input Area */\n.input-area {\n padding: 16px;\n background: white;\n border-top: 1px solid #dee2e6;\n flex-shrink: 0; /* Prevent input area from shrinking */\n margin-top: auto; /* Push to bottom */\n}\n\n.input-container {\n display: flex;\n gap: 12px;\n align-items: center;\n}\n\n.message-input {\n flex: 1;\n}\n\n.message-input textarea {\n resize: none;\n font-size: 14px;\n}\n\n.send-button {\n margin-bottom: 1px;\n}\n\n/* Sidebar */\n.sidebar {\n width: 100%; /* Changed from fixed 360px to 100% */\n background: white;\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n\n/* Kendo Splitter Specific Styles */\n.harness-body kendo-splitter {\n height: 100%;\n}\n\n.harness-body kendo-splitter-pane {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n/* Sidebar Tabs */\n.sidebar-tabs {\n display: flex;\n background: #f8f9fa;\n border-bottom: 1px solid #dee2e6;\n}\n\n.tab-button {\n flex: 1;\n padding: 12px 8px;\n background: none;\n border: none;\n border-bottom: 2px solid transparent;\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: #6c757d;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n transition: all 0.2s;\n position: relative;\n}\n\n.tab-button:hover {\n background: #e9ecef;\n color: #495057;\n}\n\n.tab-button.active {\n color: #0d6efd;\n border-bottom-color: #0d6efd;\n background: white;\n}\n\n.tab-button i {\n font-size: 14px;\n}\n\n.tab-button .badge {\n background: #6c757d;\n color: white;\n padding: 2px 6px;\n border-radius: 10px;\n font-size: 10px;\n margin-left: 4px;\n}\n\n.tab-button.active .badge {\n background: #0d6efd;\n}\n\n.tab-button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n/* Sidebar Content */\n.sidebar-content {\n flex: 1;\n overflow-y: auto;\n width: 100%; /* Ensure content takes full width */\n}\n\n.tab-content {\n padding: 20px;\n width: 100%; /* Ensure content takes full width */\n box-sizing: border-box; /* Include padding in width calculation */\n}\n\n.tab-header {\n margin-bottom: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n position: relative;\n}\n\n.tab-header .custom-button.icon-only {\n position: absolute;\n top: 0;\n right: 0;\n}\n\n.tab-header h4 {\n margin: 0 0 4px 0;\n font-size: 16px;\n font-weight: 600;\n color: #212529;\n}\n\n.tab-header p {\n margin: 0;\n font-size: 13px;\n color: #6c757d;\n}\n\n.tab-header .custom-button.small {\n align-self: flex-start;\n padding: 6px 12px;\n font-size: 13px;\n}\n\n.tab-header .custom-button.small i {\n margin-right: 4px;\n}\n\n/* Variables */\n.variables-list {\n margin-bottom: 16px;\n width: 100%;\n}\n\n.variable-item {\n display: flex;\n gap: 8px;\n margin-bottom: 12px;\n align-items: center;\n width: 100%;\n}\n\n.variable-name {\n width: 30%; /* Changed from fixed width to percentage */\n min-width: 100px;\n}\n\n.variable-type {\n width: 25%; /* Changed from fixed width to percentage */\n min-width: 80px;\n}\n\n.variable-value {\n flex: 1;\n min-width: 0; /* Allow flex item to shrink below content size */\n}\n\n.remove-button {\n width: 32px;\n height: 32px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.add-variable-button {\n width: 100%;\n}\n\n.empty-variables {\n text-align: center;\n padding: 40px 20px;\n color: #6c757d;\n}\n\n/* Saved Conversations */\n.saved-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.empty-saved {\n text-align: center;\n padding: 40px 20px;\n color: #6c757d;\n}\n\n.empty-saved i {\n font-size: 36px;\n margin-bottom: 12px;\n opacity: 0.5;\n}\n\n.saved-item {\n border: 1px solid #dee2e6;\n border-radius: 8px;\n padding: 12px;\n cursor: pointer;\n transition: all 0.2s;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.saved-item:hover {\n background: #f8f9fa;\n border-color: #adb5bd;\n}\n\n.saved-item.current {\n background: #e7f3ff;\n border-color: #0d6efd;\n}\n\n.saved-info h5 {\n margin: 0 0 4px 0;\n font-size: 14px;\n font-weight: 600;\n color: #212529;\n}\n\n.saved-info p {\n margin: 0 0 4px 0;\n font-size: 12px;\n color: #6c757d;\n}\n\n.saved-date {\n font-size: 11px;\n color: #6c757d;\n}\n\n/* Responsive adjustments */\n@media (max-width: 1200px) {\n .sidebar {\n width: 320px;\n }\n}\n\n@media (max-width: 992px) {\n .message-user,\n .message-assistant {\n max-width: 85%;\n }\n \n .sidebar {\n width: 280px;\n }\n}\n\n/* Custom scrollbar */\n.messages-container::-webkit-scrollbar,\n.sidebar-content::-webkit-scrollbar {\n width: 8px;\n}\n\n.messages-container::-webkit-scrollbar-track,\n.sidebar-content::-webkit-scrollbar-track {\n background: #f1f1f1;\n}\n\n.messages-container::-webkit-scrollbar-thumb,\n.sidebar-content::-webkit-scrollbar-thumb {\n background: #c1c1c1;\n border-radius: 4px;\n}\n\n.messages-container::-webkit-scrollbar-thumb:hover,\n.sidebar-content::-webkit-scrollbar-thumb:hover {\n background: #a1a1a1;\n}\n\n/* Dialog customization */\n::ng-deep .ai-agent-test-harness .k-dialog-wrapper {\n border-radius: 8px;\n overflow: hidden;\n}\n\n::ng-deep .ai-agent-test-harness .k-dialog-titlebar {\n background: #343a40;\n color: white;\n}\n\n::ng-deep .ai-agent-test-harness .k-dialog-title {\n font-weight: 600;\n}\n\n::ng-deep .ai-agent-test-harness .k-dialog-titlebar-actions .k-button {\n color: white;\n}\n\n::ng-deep .ai-agent-test-harness .k-dialog-content {\n padding: 0;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n/* JSON Dialog Styles - removed since we're using WindowService */\n\n/* Advanced Parameters Styles */\n.advanced-params-panel {\n margin-top: 24px;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n}\n\n.advanced-params-panel ::ng-deep .k-expander-title {\n font-weight: 600;\n color: #495057;\n}\n\n.advanced-settings {\n padding: 16px;\n}\n\n.advanced-param-input {\n width: 100%;\n}\n\n.advanced-param-switch {\n margin-top: 8px;\n}\n\n/* Format selector dropdown */\n.format-selector {\n width: 100%;\n}"] }]
|
|
3525
|
+
args: [{ selector: 'mj-ai-test-harness', template: "<div class=\"harness-container\" *ngIf=\"isVisible\" kendoWindowContainer>\n <!-- Header -->\n <div class=\"harness-header\">\n @if (entity) {\n <div class=\"agent-info\">\n <div class=\"agent-details\">\n @if (hasEntityLogo()) {\n <img [src]=\"getEntityLogoURL()\" [alt]=\"getEntityName() + ' logo'\" class=\"entity-logo\">\n } @else {\n <i [class]=\"getEntityIconClass()\"></i>\n }\n <div>\n <h3>{{ getEntityName() }}</h3>\n @if (entity.Description) {\n <p>{{ entity.Description }}</p>\n }\n </div>\n </div>\n <div class=\"header-actions\">\n <button \n class=\"custom-button\"\n (click)=\"clearConversation()\"\n [disabled]=\"isExecuting || conversationMessages.length === 0\"\n title=\"Clear conversation\">\n <i class=\"fa-solid fa-broom\"></i>\n Clear\n </button>\n <button \n class=\"custom-button\"\n (click)=\"saveConversation()\"\n [disabled]=\"conversationMessages.length === 0\"\n title=\"Save conversation\">\n <i class=\"fa-solid fa-save\"></i>\n Save\n </button>\n <button \n class=\"custom-button\"\n (click)=\"exportConversation()\"\n [disabled]=\"conversationMessages.length === 0\"\n title=\"Export conversation\">\n <i class=\"fa-solid fa-download\"></i>\n Export\n </button>\n <button \n class=\"custom-button\"\n (click)=\"importConversation()\"\n title=\"Import conversation\">\n <i class=\"fa-solid fa-upload\"></i>\n Import\n </button>\n <input \n #fileInput\n type=\"file\"\n accept=\".json\"\n style=\"display: none\"\n (change)=\"onFileSelected($event)\">\n <button \n class=\"custom-button icon-only\"\n (click)=\"toggleSidebar()\"\n title=\"Toggle sidebar\">\n <i class=\"fa-solid\" [class.fa-chevron-right]=\"!showSidebar\" [class.fa-chevron-left]=\"showSidebar\"></i>\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Main Content Area with Splitter -->\n <div class=\"harness-body\">\n @if (showSidebar) {\n <kendo-splitter \n orientation=\"horizontal\"\n style=\"height: 100%; width: 100%;\">\n \n <!-- Chat Area (Left Pane) -->\n <kendo-splitter-pane \n [collapsible]=\"false\"\n [resizable]=\"true\"\n [min]=\"'400px'\">\n <div class=\"chat-area\">\n <!-- Messages Container -->\n <div class=\"messages-container\" #messagesContainer>\n @if (conversationMessages.length === 0) {\n <div class=\"empty-state\">\n <i class=\"fa-solid fa-comments\"></i>\n <h4>Start a conversation</h4>\n <p>Send a message to begin testing the AI agent</p>\n </div>\n }\n\n @for (message of conversationMessages; track message.id) {\n <div [ngClass]=\"getMessageClass(message)\">\n <div class=\"message-header\">\n <span class=\"message-role\">\n @if (message.role === 'user') {\n <i class=\"fa-solid fa-user\"></i> You\n } @else {\n @if (hasEntityLogo()) {\n <img [src]=\"getEntityLogoURL()\" [alt]=\"getEntityName() + ' logo'\" class=\"message-role-logo\">\n } @else {\n <i [class]=\"getEntityIconClass()\"></i>\n }\n {{ getEntityName() || 'Assistant' }}\n }\n </span>\n <span class=\"message-time\">{{ formatTimestamp(message.timestamp) }}</span>\n @if (message.isStreaming && message.elapsedTime !== undefined) {\n <span class=\"execution-time streaming\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i> {{ formatElapsedTime(message.elapsedTime) }}\n </span>\n } @else if (message.executionTime) {\n <span class=\"execution-time\" [title]=\"getExecutionSummary(message)\">{{ formatExecutionTime(message.executionTime) }}</span>\n }\n @if (showRawToggle(message)) {\n <button class=\"raw-toggle\" (click)=\"showRawJsonDialog(message)\" title=\"View raw JSON response\">\n <i class=\"fa-solid fa-code\"></i>\n </button>\n }\n </div>\n <div class=\"message-content\">\n <button class=\"copy-button-in-bubble\" (click)=\"copyMessage(message)\" title=\"Copy message\">\n <i class=\"fa-solid fa-copy\"></i>\n </button>\n @if (message.isStreaming) {\n <div class=\"streaming-wrapper\">\n <span class=\"streaming-content\" [innerHTML]=\"getFormattedStreamingContent(message)\"></span>\n <span class=\"cursor\">|</span>\n </div>\n } @else {\n <div [innerHTML]=\"getFormattedContent(message)\"></div>\n }\n @if (message.error) {\n <div class=\"error-details\">\n <i class=\"fa-solid fa-exclamation-triangle\"></i>\n {{ message.error }}\n </div>\n }\n @if (hasPayload(message.payload) && mode === 'agent' && message.role === 'assistant') {\n <div class=\"payload-section\" [class.collapsed]=\"message.payloadCollapsed\">\n <div class=\"payload-header\" (click)=\"togglePayloadCollapse(message)\">\n <span class=\"payload-label\">\n <i class=\"fa-solid fa-code\"></i> Payload\n </span>\n <button class=\"payload-toggle\" type=\"button\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!message.payloadCollapsed\" [class.fa-chevron-right]=\"message.payloadCollapsed\"></i>\n </button>\n </div>\n @if (!message.payloadCollapsed) {\n <div class=\"payload-content\">\n <mj-code-editor\n [ngModel]=\"formatJson(message.payload)\"\n [language]=\"'json'\"\n [readonly]=\"true\"\n style=\"height: 300px; width: 100%;\">\n </mj-code-editor>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Input Area -->\n <div class=\"input-area\">\n <div class=\"input-container\">\n <kendo-textarea\n #messageInput\n [(ngModel)]=\"currentUserMessage\"\n [rows]=\"3\"\n placeholder=\"Type your message here... (Press Enter to send, Shift+Enter for new line)\"\n (keypress)=\"handleKeyPress($event)\"\n [disabled]=\"isExecuting\"\n class=\"message-input\">\n </kendo-textarea>\n <button\n class=\"send-button\"\n (click)=\"sendMessage()\"\n [disabled]=\"isExecuting || !currentUserMessage.trim()\"\n title=\"Send message\">\n @if (isExecuting) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n } @else {\n <i class=\"fa-solid fa-paper-plane\"></i>\n }\n </button>\n </div>\n </div>\n </div>\n </kendo-splitter-pane>\n \n <!-- Sidebar (Right Pane) -->\n <kendo-splitter-pane \n [collapsible]=\"false\"\n [resizable]=\"true\"\n [size]=\"'30%'\"\n [min]=\"'300px'\">\n <div class=\"sidebar\">\n <!-- Tab Navigation -->\n <div class=\"sidebar-tabs\">\n @if (mode === 'agent') {\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'agentVariables'\"\n [disabled]=\"executionMonitorMode === 'live' && activeTab !== 'agentVariables'\"\n (click)=\"selectTab('agentVariables')\">\n <i class=\"fa-solid fa-code\"></i>\n Agent Variables\n </button>\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'executionMonitor'\"\n (click)=\"selectTab('executionMonitor')\">\n <i class=\"fa-solid fa-diagram-project\"></i>\n Execution Monitor\n </button>\n } @else {\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'templateVariables'\"\n (click)=\"selectTab('templateVariables')\">\n <i class=\"fa-solid fa-code\"></i>\n Template Variables\n </button>\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'modelSettings'\"\n (click)=\"selectTab('modelSettings')\">\n <i class=\"fa-solid fa-sliders\"></i>\n Model Settings\n </button>\n }\n <button \n class=\"tab-button\"\n [class.active]=\"activeTab === 'savedConversations'\"\n [disabled]=\"executionMonitorMode === 'live' && activeTab !== 'savedConversations'\"\n (click)=\"selectTab('savedConversations')\">\n <i class=\"fa-solid fa-history\"></i>\n Saved\n @if (savedConversations.length > 0) {\n <span class=\"badge\">{{ savedConversations.length }}</span>\n }\n </button>\n </div>\n\n <!-- Tab Content -->\n <div class=\"sidebar-content\">\n <!-- Agent Variables Tab (Unified) -->\n @if (activeTab === 'agentVariables') {\n <div class=\"tab-content\">\n <div class=\"tab-header\">\n <h4>Agent Variables</h4>\n <p>Variables available during agent execution and template rendering</p>\n </div>\n \n <div class=\"variables-list\">\n @if (agentVariables.length === 0) {\n <div class=\"empty-variables\">\n <p>No variables defined. Click \"Add Variable\" to get started.</p>\n </div>\n }\n \n @for (variable of agentVariables; track $index) {\n <div class=\"variable-item\">\n <kendo-textbox \n [(ngModel)]=\"variable.name\" \n placeholder=\"Variable name\"\n [disabled]=\"isExecuting\"\n class=\"variable-name\">\n </kendo-textbox>\n \n <kendo-dropdownlist \n [(ngModel)]=\"variable.type\"\n [data]=\"[\n {text: 'String', value: 'string'}, \n {text: 'Number', value: 'number'}, \n {text: 'Boolean', value: 'boolean'}, \n {text: 'Object', value: 'object'}\n ]\"\n textField=\"text\"\n valueField=\"value\"\n [valuePrimitive]=\"true\"\n [disabled]=\"isExecuting\"\n class=\"variable-type\">\n </kendo-dropdownlist>\n \n <kendo-textbox \n [(ngModel)]=\"variable.value\" \n [placeholder]=\"variable.type === 'object' ? 'JSON value' : 'Value'\"\n [disabled]=\"isExecuting\"\n class=\"variable-value\">\n </kendo-textbox>\n \n <button \n class=\"remove-button\"\n (click)=\"removeAgentVariable($index)\"\n [disabled]=\"isExecuting\"\n title=\"Remove variable\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n </div>\n \n <button \n class=\"add-button\"\n (click)=\"addAgentVariable()\"\n [disabled]=\"isExecuting\">\n <i class=\"fa-solid fa-plus\"></i> Add Variable\n </button>\n </div>\n }\n\n <!-- Execution Monitor Tab -->\n @if (activeTab === 'executionMonitor') {\n <div class=\"tab-content\" style=\"padding: 0; height: 100%;\">\n <mj-agent-execution-monitor\n [mode]=\"executionMonitorMode\"\n [agentRun]=\"currentAgentRun\"\n [liveSteps]=\"liveAgentSteps\"\n [autoExpand]=\"true\"\n (viewRunClick)=\"navigateToAgentRun($event)\"\n [runId]=\"getLastRunId()\"\n [runType]=\"mode\">\n </mj-agent-execution-monitor>\n </div>\n }\n\n <!-- Template Variables Tab (Prompt Mode) -->\n @if (activeTab === 'templateVariables') {\n <div class=\"tab-content\">\n <div class=\"tab-header\">\n <h4>Template Variables</h4>\n <p>Variables for prompt template rendering</p>\n </div>\n \n <div class=\"variables-list\">\n @for (variable of templateVariables; track $index) {\n <div class=\"variable-item\">\n <kendo-textbox \n [(ngModel)]=\"variable.name\" \n placeholder=\"Variable name\"\n [disabled]=\"isExecuting\"\n class=\"variable-name\">\n </kendo-textbox>\n \n <kendo-dropdownlist \n [(ngModel)]=\"variable.type\"\n [data]=\"[\n {text: 'String', value: 'string'}, \n {text: 'Number', value: 'number'}, \n {text: 'Boolean', value: 'boolean'}, \n {text: 'Object', value: 'object'}\n ]\"\n textField=\"text\"\n valueField=\"value\"\n [valuePrimitive]=\"true\"\n [disabled]=\"isExecuting\"\n class=\"variable-type\">\n </kendo-dropdownlist>\n \n <kendo-textbox \n [(ngModel)]=\"variable.value\" \n [placeholder]=\"variable.type === 'object' ? 'JSON value' : 'Value'\"\n [disabled]=\"isExecuting\"\n class=\"variable-value\">\n </kendo-textbox>\n \n <button \n class=\"remove-button\"\n (click)=\"removeTemplateVariable($index)\"\n title=\"Remove variable\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n </div>\n \n <button \n class=\"add-button\"\n (click)=\"addTemplateVariable()\">\n <i class=\"fa-solid fa-plus\"></i>\n Add Variable\n </button>\n </div>\n }\n\n <!-- Model Settings Tab (Prompt Mode) -->\n @if (activeTab === 'modelSettings') {\n <div class=\"tab-content\" style=\"overflow-y: auto;\">\n <div class=\"tab-header\">\n <h4>Model Settings</h4>\n <p>Configure AI model parameters</p>\n <button class=\"custom-button icon-only\" (click)=\"resetToPromptDefaults()\" title=\"Reset to Prompt Settings\">\n <i class=\"fa-solid fa-undo\"></i>\n </button>\n </div>\n \n <div class=\"model-settings\">\n <div class=\"setting-group\">\n <label>AI Model</label>\n <kendo-dropdownlist \n [(ngModel)]=\"selectedModelId\"\n (ngModelChange)=\"onModelSelectionChange()\"\n [data]=\"availableModels\"\n textField=\"Name\"\n valueField=\"ID\"\n [valuePrimitive]=\"true\"\n placeholder=\"Select a model...\"\n class=\"model-selector\">\n </kendo-dropdownlist>\n </div>\n \n <div class=\"setting-group\">\n <label>AI Vendor</label>\n <kendo-dropdownlist \n [(ngModel)]=\"selectedVendorId\"\n [data]=\"availableVendors\"\n textField=\"Name\"\n valueField=\"ID\"\n [valuePrimitive]=\"true\"\n [disabled]=\"availableVendors.length <= 1 || !selectedModelId\"\n placeholder=\"Select a vendor...\"\n class=\"vendor-selector\">\n </kendo-dropdownlist>\n <div class=\"setting-help\">\n @if (!selectedModelId) {\n Using default vendor for the default model\n } @else if (availableVendors.length <= 1) {\n Only one vendor available for this model\n } @else {\n Select the inference provider for this model\n }\n </div>\n </div>\n \n <div class=\"setting-group\">\n <label>Response Format</label>\n <kendo-dropdownlist \n [(ngModel)]=\"selectedResponseFormat\"\n [data]=\"responseFormatOptions\"\n textField=\"text\"\n valueField=\"value\"\n placeholder=\"Select format...\"\n class=\"format-selector\">\n </kendo-dropdownlist>\n <div class=\"setting-help\">\n Expected format for AI response (defaults to prompt setting)\n </div>\n </div>\n \n <div class=\"setting-group\">\n <label>Max Tokens</label>\n <kendo-numerictextbox \n [(ngModel)]=\"maxTokens\"\n [min]=\"100\"\n [max]=\"8000\"\n [step]=\"100\"\n placeholder=\"Max tokens...\"\n class=\"max-tokens-input\">\n </kendo-numerictextbox>\n <div class=\"setting-help\">\n Maximum length of generated response\n </div>\n </div>\n \n <div class=\"setting-group\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" \n [(ngModel)]=\"skipValidation\"\n class=\"skip-validation-checkbox\">\n Skip Validation\n </label>\n <div class=\"setting-help\">\n Skip output validation during testing\n </div>\n </div>\n \n <!-- Advanced Parameters Expansion Panel -->\n <kendo-expansionpanel \n [expanded]=\"advancedParamsExpanded\"\n (action)=\"toggleAdvancedParams()\"\n title=\"Advanced Parameters\"\n class=\"advanced-params-panel\">\n <div class=\"advanced-settings\">\n <div class=\"setting-group\">\n <label title=\"Controls randomness: 0 = more focused and deterministic, 2 = more random and creative\">\n Temperature\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.temperature\"\n [min]=\"0\"\n [max]=\"2\"\n [step]=\"0.1\"\n [format]=\"'n1'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Nucleus sampling: only consider tokens with cumulative probability up to this value. 1 = consider all tokens\">\n Top P\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.topP\"\n [min]=\"0\"\n [max]=\"1\"\n [step]=\"0.01\"\n [format]=\"'n2'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Only sample from the top K tokens. Lower values reduce randomness\">\n Top K\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.topK\"\n [min]=\"1\"\n [max]=\"100\"\n [step]=\"1\"\n [format]=\"'n0'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Minimum probability for token selection. Filters out low-probability tokens\">\n Min P\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.minP\"\n [min]=\"0\"\n [max]=\"1\"\n [step]=\"0.01\"\n [format]=\"'n2'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Penalizes tokens based on their frequency in the text. Positive values reduce repetition\">\n Frequency Penalty\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.frequencyPenalty\"\n [min]=\"-2\"\n [max]=\"2\"\n [step]=\"0.1\"\n [format]=\"'n1'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Penalizes tokens that have appeared in the text. Positive values increase topic diversity\">\n Presence Penalty\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.presencePenalty\"\n [min]=\"-2\"\n [max]=\"2\"\n [step]=\"0.1\"\n [format]=\"'n1'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Random seed for deterministic generation. Same seed produces same output\">\n Seed\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.seed\"\n [format]=\"'n0'\"\n placeholder=\"None\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Sequences that will stop generation when encountered. Enter comma-separated values\">\n Stop Sequences\n </label>\n <kendo-textarea \n [(ngModel)]=\"stopSequencesText\"\n (ngModelChange)=\"updateStopSequences()\"\n [rows]=\"2\"\n placeholder=\"Enter comma-separated sequences\"\n class=\"advanced-param-input\">\n </kendo-textarea>\n <div class=\"setting-help\">\n Separate multiple sequences with commas\n </div>\n </div>\n \n <div class=\"setting-group\">\n <label title=\"Include log probabilities of the output tokens in the response\">\n Include Log Probs\n </label>\n <kendo-switch \n [(ngModel)]=\"advancedParams.includeLogProbs\"\n class=\"advanced-param-switch\">\n </kendo-switch>\n </div>\n \n @if (advancedParams.includeLogProbs) {\n <div class=\"setting-group\">\n <label title=\"Number of top log probabilities to include for each token\">\n Top Log Probs\n </label>\n <kendo-numerictextbox \n [(ngModel)]=\"advancedParams.topLogProbs\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\"\n [format]=\"'n0'\"\n placeholder=\"2\"\n class=\"advanced-param-input\">\n </kendo-numerictextbox>\n </div>\n }\n </div>\n </kendo-expansionpanel>\n </div>\n </div>\n }\n\n <!-- Saved Conversations Tab -->\n @if (activeTab === 'savedConversations') {\n <div class=\"tab-content\">\n <div class=\"tab-header\">\n <h4>Saved Conversations</h4>\n <p>Previously saved test sessions</p>\n @if (currentConversationId) {\n <button class=\"custom-button small primary\" (click)=\"newConversation()\" title=\"Start a new conversation\">\n <i class=\"fa-solid fa-plus\"></i>\n New\n </button>\n }\n </div>\n \n <div class=\"saved-list\">\n @if (savedConversations.length === 0) {\n <div class=\"empty-saved\">\n <i class=\"fa-solid fa-folder-open\"></i>\n <p>No saved conversations</p>\n </div>\n }\n \n @for (conv of savedConversations; track conv.id) {\n <div class=\"saved-item\" \n [class.current]=\"conv.id === currentConversationId\"\n (click)=\"loadConversation(conv)\">\n <div class=\"saved-info\">\n <h5>{{ conv.name }}</h5>\n <p>{{ conv.messages.length }} messages</p>\n <span class=\"saved-date\">{{ conv.updatedAt | date:'short' }}</span>\n </div>\n <button \n class=\"delete-button\"\n (click)=\"deleteConversation(conv, $event)\"\n title=\"Delete conversation\">\n <i class=\"fa-solid fa-trash\"></i>\n </button>\n </div>\n }\n </div>\n </div>\n }\n </div>\n </div>\n </kendo-splitter-pane>\n </kendo-splitter>\n } @else {\n <!-- No sidebar - full width chat -->\n <div class=\"chat-area\">\n <!-- Messages Container -->\n <div class=\"messages-container\" #messagesContainer>\n @if (conversationMessages.length === 0) {\n <div class=\"empty-state\">\n <i class=\"fa-solid fa-comments\"></i>\n <h4>Start a conversation</h4>\n <p>Send a message to begin testing the AI agent</p>\n </div>\n }\n\n @for (message of conversationMessages; track message.id) {\n <div [ngClass]=\"getMessageClass(message)\">\n <div class=\"message-header\">\n <span class=\"message-role\">\n @if (message.role === 'user') {\n <i class=\"fa-solid fa-user\"></i> You\n } @else {\n <i class=\"fa-solid fa-robot\"></i> {{ getEntityName() || 'Assistant' }}\n }\n </span>\n <span class=\"message-time\">{{ formatTimestamp(message.timestamp) }}</span>\n @if (message.isStreaming && message.elapsedTime !== undefined) {\n <span class=\"execution-time streaming\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i> {{ formatElapsedTime(message.elapsedTime) }}\n </span>\n } @else if (message.executionTime) {\n <span class=\"execution-time\" [title]=\"getExecutionSummary(message)\">{{ formatExecutionTime(message.executionTime) }}</span>\n }\n @if (showRawToggle(message)) {\n <button class=\"raw-toggle\" (click)=\"showRawJsonDialog(message)\" title=\"View raw JSON response\">\n <i class=\"fa-solid fa-code\"></i>\n </button>\n }\n @if (message.executionData && mode === 'agent' && message.role === 'assistant') {\n <button class=\"execution-history-toggle\" (click)=\"showMessageExecutionHistory(message)\" title=\"View execution details\">\n <i class=\"fa-solid fa-diagram-project\"></i>\n </button>\n }\n @if (message.agentRunId && mode === 'agent' && message.role === 'assistant') {\n <button class=\"execution-history-toggle\" (click)=\"navigateToAgentRun({runId: message.agentRunId, runType: 'agent'})\" title=\"View full run details\">\n <i class=\"fa-solid fa-external-link-alt\"></i>\n </button>\n }\n </div>\n <div class=\"message-content\">\n <button class=\"copy-button-in-bubble\" (click)=\"copyMessage(message)\" title=\"Copy message\">\n <i class=\"fa-solid fa-copy\"></i>\n </button>\n @if (message.isStreaming) {\n <div class=\"streaming-wrapper\">\n <span class=\"streaming-content\">{{ message.streamingContent }}<span class=\"cursor\">|</span></span>\n </div>\n } @else {\n <div [innerHTML]=\"getFormattedContent(message)\"></div>\n }\n @if (message.error) {\n <div class=\"error-details\">\n <i class=\"fa-solid fa-exclamation-triangle\"></i>\n {{ message.error }}\n </div>\n }\n @if (hasPayload(message.payload) && mode === 'agent' && message.role === 'assistant') {\n <div class=\"payload-section\" [class.collapsed]=\"message.payloadCollapsed\">\n <div class=\"payload-header\" (click)=\"togglePayloadCollapse(message)\">\n <span class=\"payload-label\">\n <i class=\"fa-solid fa-code\"></i> Payload\n </span>\n <button class=\"payload-toggle\" type=\"button\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!message.payloadCollapsed\" [class.fa-chevron-right]=\"message.payloadCollapsed\"></i>\n </button>\n </div>\n @if (!message.payloadCollapsed) {\n <div class=\"payload-content\">\n <pre>{{ formatJson(message.payload) }}</pre>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Input Area -->\n <div class=\"input-area\">\n <div class=\"input-container\">\n <kendo-textarea\n #messageInput\n [(ngModel)]=\"currentUserMessage\"\n [rows]=\"3\"\n placeholder=\"Type your message here... (Press Enter to send, Shift+Enter for new line)\"\n (keypress)=\"handleKeyPress($event)\"\n [disabled]=\"isExecuting\"\n class=\"message-input\">\n </kendo-textarea>\n <button\n class=\"send-button\"\n (click)=\"sendMessage()\"\n [disabled]=\"isExecuting || !currentUserMessage.trim()\"\n title=\"Send message\">\n @if (isExecuting) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n } @else {\n <i class=\"fa-solid fa-paper-plane\"></i>\n }\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Save Conversation Dialog -->\n @if (showSaveDialog) {\n <kendo-dialog \n [title]=\"'Save Conversation'\" \n [width]=\"400\"\n (close)=\"cancelSaveDialog()\"\n class=\"save-dialog\">\n <div class=\"dialog-content\">\n <p>Enter a name for this conversation:</p>\n <input \n #saveDialogInput\n type=\"text\"\n (keyup)=\"updateTempConversation()\"\n placeholder=\"Conversation name\"\n class=\"k-textbox k-input k-input-md k-rounded-md k-input-solid full-width\" />\n </div>\n <kendo-dialog-actions>\n <button kendoButton (click)=\"cancelSaveDialog()\">Cancel</button>\n <button kendoButton \n [primary]=\"true\" \n (click)=\"confirmSaveConversation()\"\n [disabled]=\"tempConversationName === ''\">\n Save\n </button>\n </kendo-dialog-actions>\n </kendo-dialog>\n }\n \n <!-- Load Confirmation Dialog -->\n @if (showLoadConfirmDialog) {\n <kendo-dialog \n [title]=\"'Replace Current Conversation?'\" \n [width]=\"400\"\n (close)=\"cancelLoadConversation()\"\n class=\"confirm-dialog\">\n <div class=\"dialog-content\">\n <p>Loading this conversation will replace the current one. Any unsaved changes will be lost.</p>\n <p>Do you want to continue?</p>\n </div>\n <kendo-dialog-actions>\n <button kendoButton (click)=\"cancelLoadConversation()\">Cancel</button>\n <button kendoButton \n [primary]=\"true\" \n (click)=\"confirmLoadConversation()\">\n Load Conversation\n </button>\n </kendo-dialog-actions>\n </kendo-dialog>\n }\n\n", styles: ["/* Container Styles */\n.harness-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: #f8f9fa;\n}\n\n/* Custom Button Styles */\n.custom-button {\n background: white;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 13px;\n color: #495057;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n}\n\n.custom-button:hover:not(:disabled) {\n background: #f8f9fa;\n border-color: #adb5bd;\n color: #212529;\n}\n\n.custom-button:active:not(:disabled) {\n background: #e9ecef;\n border-color: #adb5bd;\n}\n\n.custom-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.custom-button.icon-only {\n padding: 6px 8px;\n}\n\n.custom-button i {\n font-size: 14px;\n}\n\n/* Send Button - Primary Style */\n.send-button {\n background: #0d6efd;\n border: 1px solid #0d6efd;\n color: white;\n border-radius: 4px;\n padding: 8px 16px;\n font-size: 14px;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 44px;\n font-family: inherit;\n}\n\n.send-button:hover:not(:disabled) {\n background: #0b5ed7;\n border-color: #0a58ca;\n}\n\n.send-button:active:not(:disabled) {\n background: #0a58ca;\n border-color: #0a58ca;\n}\n\n.send-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.send-button i {\n font-size: 16px;\n}\n\n/* Add Button */\n.add-button {\n background: white;\n border: 1px solid #0d6efd;\n color: #0d6efd;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 13px;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n}\n\n.add-button:hover {\n background: #0d6efd;\n color: white;\n}\n\n/* Remove/Delete Buttons */\n.remove-button,\n.delete-button {\n background: transparent;\n border: none;\n color: #dc3545;\n padding: 4px 8px;\n font-size: 12px;\n cursor: pointer;\n transition: all 0.2s;\n border-radius: 4px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.remove-button:hover,\n.delete-button:hover {\n background: rgba(220, 53, 69, 0.1);\n color: #bd2130;\n}\n\n/* Copy Button in Message Bubble */\n.copy-button-in-bubble {\n position: absolute;\n top: 8px;\n right: 8px;\n background: transparent;\n border: none;\n padding: 6px;\n font-size: 14px;\n color: #adb5bd;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n opacity: 0;\n z-index: 10; /* Ensure it's above content */\n}\n\n.message-content:hover .copy-button-in-bubble {\n opacity: 1;\n}\n\n.copy-button-in-bubble:hover {\n background: rgba(0, 0, 0, 0.08);\n color: #6c757d;\n}\n\n.copy-button-in-bubble i {\n font-size: 14px;\n}\n\n/* Model Settings Styles (Prompt Mode) */\n.model-settings {\n padding: 16px;\n}\n\n.setting-group {\n margin-bottom: 24px;\n}\n\n.setting-group label {\n display: block;\n margin-bottom: 8px;\n font-weight: 600;\n color: #495057;\n font-size: 14px;\n}\n\n.model-selector,\n.vendor-selector {\n width: 100%;\n}\n\n.temperature-slider {\n width: 100%;\n margin-top: 8px;\n}\n\n.max-tokens-input {\n width: 100%;\n}\n\n.setting-help {\n margin-top: 6px;\n font-size: 12px;\n color: #6c757d;\n font-style: italic;\n}\n\n/* Checkbox Styles */\n.checkbox-label {\n display: flex;\n align-items: center;\n font-weight: 600;\n color: #495057;\n font-size: 14px;\n cursor: pointer;\n}\n\n.skip-validation-checkbox {\n margin-right: 8px;\n cursor: pointer;\n width: 16px;\n height: 16px;\n}\n\n/* Header Styles */\n.harness-header {\n background: white;\n border-bottom: 1px solid #dee2e6;\n padding: 16px 20px;\n}\n\n.agent-info {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.agent-details {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.agent-details i {\n font-size: 28px;\n color: #17a2b8;\n}\n\n.agent-details .entity-logo {\n width: 40px;\n height: 40px;\n object-fit: cover;\n border-radius: 8px;\n}\n\n.agent-details h3 {\n margin: 0;\n font-size: 20px;\n font-weight: 600;\n color: #212529;\n}\n\n.agent-details p {\n margin: 4px 0 0 0;\n font-size: 14px;\n color: #6c757d;\n}\n\n.header-actions {\n display: flex;\n gap: 8px;\n}\n\n/* Body Layout */\n.harness-body {\n flex: 1;\n display: flex;\n overflow: hidden;\n}\n\n/* Chat Area */\n.chat-area {\n flex: 1;\n display: flex;\n flex-direction: column;\n background: white;\n border-right: 1px solid #dee2e6;\n height: 100%;\n min-height: 0; /* Important for flexbox children */\n}\n\n/* Messages Container */\n.messages-container {\n flex: 1;\n overflow-y: auto;\n padding: 20px;\n scroll-behavior: smooth;\n}\n\n.empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n text-align: center;\n color: #6c757d;\n}\n\n.empty-state i {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.empty-state h4 {\n margin: 0 0 8px 0;\n font-size: 18px;\n font-weight: 500;\n}\n\n.empty-state p {\n margin: 0;\n font-size: 14px;\n}\n\n/* Message Styles */\n.message {\n margin-bottom: 20px;\n animation: fadeIn 0.3s ease-in;\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.message-user {\n margin-left: auto;\n max-width: 70%;\n}\n\n.message-assistant {\n margin-right: auto;\n max-width: 70%;\n}\n\n.message-header {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 8px;\n font-size: 13px;\n}\n\n.message-role {\n font-weight: 600;\n color: #495057;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.message-role i {\n font-size: 14px;\n}\n\n.message-role .message-role-logo {\n width: 16px;\n height: 16px;\n object-fit: cover;\n border-radius: 3px;\n display: inline-block;\n vertical-align: middle;\n}\n\n.message-user .message-role {\n color: #6f42c1;\n}\n\n.message-assistant .message-role {\n color: #17a2b8;\n}\n\n.message-time {\n color: #6c757d;\n font-size: 12px;\n}\n\n.execution-time {\n background: #e9ecef;\n padding: 2px 8px;\n border-radius: 12px;\n font-size: 11px;\n color: #495057;\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.execution-time.streaming {\n background: #fff3cd;\n color: #856404;\n border: 1px solid #ffeaa7;\n}\n\n.raw-toggle {\n background: transparent;\n border: none;\n padding: 4px 8px;\n font-size: 12px;\n color: #6c757d;\n cursor: pointer;\n transition: all 0.2s;\n margin-left: 8px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n}\n\n.raw-toggle:hover {\n background: rgba(0, 0, 0, 0.05);\n color: #495057;\n}\n\n.raw-toggle i {\n font-size: 12px;\n}\n\n/* Message content styles are now defined above in enhanced content formatting section */\n\n.message-user .message-content {\n background: #e7f3ff;\n border: 1px solid #b8daff;\n}\n\n.message-assistant .message-content {\n background: #f8f9fa;\n border: 1px solid #e9ecef;\n}\n\n.message.streaming .message-content {\n background: #fff3cd;\n border: 1px solid #ffeaa7;\n}\n\n.streaming-content {\n display: inline;\n}\n\n.streaming-wrapper {\n position: relative;\n}\n\n.streaming-wrapper .cursor {\n display: inline;\n margin-left: 2px;\n}\n\n.cursor {\n animation: blink 1s infinite;\n font-weight: 100;\n color: #495057;\n}\n\n@keyframes blink {\n 0%, 50% { opacity: 1; }\n 51%, 100% { opacity: 0; }\n}\n\n.error-details {\n margin-top: 8px;\n padding: 8px 12px;\n background: #f8d7da;\n border: 1px solid #f5c6cb;\n border-radius: 6px;\n color: #721c24;\n font-size: 13px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.error-details i {\n color: #dc3545;\n}\n\n/* Payload Section Styles */\n.payload-section {\n margin-top: 12px;\n border: 1px solid #e9ecef;\n border-radius: 6px;\n overflow: hidden;\n background: #ffffff;\n}\n\n.payload-header {\n background: #f8f9fa;\n padding: 8px 12px;\n border-bottom: 1px solid #e9ecef;\n display: flex;\n align-items: center;\n justify-content: space-between;\n cursor: pointer;\n transition: background-color 0.2s;\n}\n\n.payload-header:hover {\n background: #e9ecef;\n}\n\n.payload-label {\n font-size: 13px;\n font-weight: 600;\n color: #495057;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.payload-label i {\n color: #6c757d;\n font-size: 12px;\n}\n\n.payload-content {\n max-height: 250px;\n overflow: auto;\n background: #f8f9fa;\n}\n\n.payload-content pre {\n margin: 0;\n padding: 12px;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n font-size: 12px;\n line-height: 1.5;\n color: #212529;\n white-space: pre-wrap;\n word-wrap: break-word;\n background: transparent;\n}\n\n.payload-toggle {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #6c757d;\n transition: color 0.2s;\n}\n\n.payload-toggle:hover {\n color: #495057;\n}\n\n.payload-toggle i {\n font-size: 12px;\n}\n\n.payload-section.collapsed .payload-header {\n border-bottom: none;\n}\n\n/* Dialog styles */\n.save-dialog,\n.confirm-dialog {\n .dialog-content {\n padding: 20px;\n \n p {\n margin: 0 0 16px 0;\n color: #333;\n font-size: 14px;\n line-height: 1.5;\n \n &:last-child {\n margin-bottom: 0;\n }\n }\n \n .full-width {\n width: 100%;\n }\n \n input.k-textbox {\n padding: 8px 12px;\n font-size: 14px;\n line-height: 1.5;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n \n &:focus {\n outline: none;\n border-color: #ff6358;\n box-shadow: 0 0 0 0.25rem rgba(255, 99, 88, 0.25);\n }\n }\n }\n}\n\n/* Enhanced content formatting styles */\n.markdown-content {\n line-height: 1.6;\n word-wrap: break-word;\n overflow-wrap: break-word;\n max-width: 100%;\n}\n\n/* Inline JSON editor styles */\n.inline-json-editor {\n margin: 8px 0;\n \n .json-editor-container {\n background: #f8f9fa;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n overflow: hidden;\n position: relative;\n max-height: 400px;\n \n pre {\n margin: 0;\n padding: 12px;\n font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;\n font-size: 13px;\n line-height: 1.5;\n overflow: auto;\n height: 100%;\n background: transparent;\n color: #333;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n }\n}\n\n.markdown-paragraph {\n margin: 8px 0;\n word-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.markdown-h1 {\n font-size: 1.5em;\n font-weight: 600;\n margin: 16px 0 8px 0;\n color: #212529;\n border-bottom: 2px solid #e9ecef;\n padding-bottom: 4px;\n}\n\n.markdown-h2 {\n font-size: 1.3em;\n font-weight: 600;\n margin: 14px 0 6px 0;\n color: #212529;\n border-bottom: 1px solid #e9ecef;\n padding-bottom: 2px;\n}\n\n.markdown-h3 {\n font-size: 1.1em;\n font-weight: 600;\n margin: 12px 0 4px 0;\n color: #212529;\n}\n\n.code-block {\n background: #f8f9fa;\n border: 1px solid #e9ecef;\n border-radius: 6px;\n padding: 16px;\n margin: 12px 0;\n overflow-x: auto;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n font-size: 14px;\n line-height: 1.5;\n color: #212529;\n white-space: pre;\n word-wrap: normal;\n max-width: 100%;\n}\n\n.code-block code {\n background: none;\n border: none;\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre;\n word-wrap: normal;\n}\n\n.language-json {\n color: #d73e48;\n}\n\n.inline-code {\n background: #f1f3f4;\n border: 1px solid #dadce0;\n border-radius: 4px;\n padding: 2px 6px;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n font-size: 0.9em;\n color: #d73e48;\n white-space: nowrap;\n}\n\n.markdown-list {\n margin: 8px 0;\n padding-left: 24px;\n}\n\n.markdown-list li {\n margin: 4px 0;\n line-height: 1.5;\n}\n\n.markdown-link {\n color: #0d6efd;\n text-decoration: underline;\n transition: color 0.2s;\n}\n\n.markdown-link:hover {\n color: #0a58ca;\n text-decoration: none;\n}\n\n/* Ensure proper text wrapping in message content */\n.message-content {\n background: #f8f9fa;\n border-radius: 12px;\n padding: 12px 40px 12px 16px; /* Extra right padding for copy button */\n font-size: 14px;\n line-height: 1.5;\n word-wrap: break-word;\n position: relative;\n overflow-wrap: break-word;\n hyphens: auto;\n max-width: 100%;\n overflow-x: hidden;\n}\n\n/* Enhanced JSON content display */\n.json-content-wrapper {\n margin: 8px 0;\n}\n\n.json-main-content {\n margin-bottom: 12px;\n}\n\n.json-raw-section {\n margin-top: 12px;\n}\n\n.json-toggle-button {\n background: white;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 12px;\n color: #495057;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n}\n\n.json-toggle-button:hover {\n background: #f8f9fa;\n border-color: #adb5bd;\n color: #212529;\n}\n\n.json-toggle-button i {\n font-size: 12px;\n}\n\n/* Compact code block for JSON in collapsible sections */\n.json-raw-section .code-block {\n margin: 8px 0 0 0;\n font-size: 12px;\n max-height: 300px;\n overflow-y: auto;\n}\n\n/* Better mobile responsiveness for JSON content */\n@media (max-width: 768px) {\n .json-raw-section .code-block {\n font-size: 11px;\n max-height: 200px;\n }\n}\n\n/* JSON raw section styling - simplified since we use code editor in dialog */\n.json-raw-section {\n margin-top: 12px;\n}\n\n/* Input Area */\n.input-area {\n padding: 16px;\n background: white;\n border-top: 1px solid #dee2e6;\n flex-shrink: 0; /* Prevent input area from shrinking */\n margin-top: auto; /* Push to bottom */\n}\n\n.input-container {\n display: flex;\n gap: 12px;\n align-items: center;\n}\n\n.message-input {\n flex: 1;\n}\n\n.message-input textarea {\n resize: none;\n font-size: 14px;\n}\n\n.send-button {\n margin-bottom: 1px;\n}\n\n/* Sidebar */\n.sidebar {\n width: 100%; /* Changed from fixed 360px to 100% */\n background: white;\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n\n/* Kendo Splitter Specific Styles */\n.harness-body kendo-splitter {\n height: 100%;\n}\n\n.harness-body kendo-splitter-pane {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n/* Sidebar Tabs */\n.sidebar-tabs {\n display: flex;\n background: #f8f9fa;\n border-bottom: 1px solid #dee2e6;\n}\n\n.tab-button {\n flex: 1;\n padding: 12px 8px;\n background: none;\n border: none;\n border-bottom: 2px solid transparent;\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: #6c757d;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n transition: all 0.2s;\n position: relative;\n}\n\n.tab-button:hover {\n background: #e9ecef;\n color: #495057;\n}\n\n.tab-button.active {\n color: #0d6efd;\n border-bottom-color: #0d6efd;\n background: white;\n}\n\n.tab-button i {\n font-size: 14px;\n}\n\n.tab-button .badge {\n background: #6c757d;\n color: white;\n padding: 2px 6px;\n border-radius: 10px;\n font-size: 10px;\n margin-left: 4px;\n}\n\n.tab-button.active .badge {\n background: #0d6efd;\n}\n\n.tab-button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n/* Sidebar Content */\n.sidebar-content {\n flex: 1;\n overflow-y: auto;\n width: 100%; /* Ensure content takes full width */\n}\n\n.tab-content {\n padding: 20px;\n width: 100%; /* Ensure content takes full width */\n box-sizing: border-box; /* Include padding in width calculation */\n}\n\n.tab-header {\n margin-bottom: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n position: relative;\n}\n\n.tab-header .custom-button.icon-only {\n position: absolute;\n top: 0;\n right: 0;\n}\n\n.tab-header h4 {\n margin: 0 0 4px 0;\n font-size: 16px;\n font-weight: 600;\n color: #212529;\n}\n\n.tab-header p {\n margin: 0;\n font-size: 13px;\n color: #6c757d;\n}\n\n.tab-header .custom-button.small {\n align-self: flex-start;\n padding: 6px 12px;\n font-size: 13px;\n}\n\n.tab-header .custom-button.small i {\n margin-right: 4px;\n}\n\n/* Variables */\n.variables-list {\n margin-bottom: 16px;\n width: 100%;\n}\n\n.variable-item {\n display: flex;\n gap: 8px;\n margin-bottom: 12px;\n align-items: center;\n width: 100%;\n}\n\n.variable-name {\n width: 30%; /* Changed from fixed width to percentage */\n min-width: 100px;\n}\n\n.variable-type {\n width: 25%; /* Changed from fixed width to percentage */\n min-width: 80px;\n}\n\n.variable-value {\n flex: 1;\n min-width: 0; /* Allow flex item to shrink below content size */\n}\n\n.remove-button {\n width: 32px;\n height: 32px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.add-variable-button {\n width: 100%;\n}\n\n.empty-variables {\n text-align: center;\n padding: 40px 20px;\n color: #6c757d;\n}\n\n/* Saved Conversations */\n.saved-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.empty-saved {\n text-align: center;\n padding: 40px 20px;\n color: #6c757d;\n}\n\n.empty-saved i {\n font-size: 36px;\n margin-bottom: 12px;\n opacity: 0.5;\n}\n\n.saved-item {\n border: 1px solid #dee2e6;\n border-radius: 8px;\n padding: 12px;\n cursor: pointer;\n transition: all 0.2s;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.saved-item:hover {\n background: #f8f9fa;\n border-color: #adb5bd;\n}\n\n.saved-item.current {\n background: #e7f3ff;\n border-color: #0d6efd;\n}\n\n.saved-info h5 {\n margin: 0 0 4px 0;\n font-size: 14px;\n font-weight: 600;\n color: #212529;\n}\n\n.saved-info p {\n margin: 0 0 4px 0;\n font-size: 12px;\n color: #6c757d;\n}\n\n.saved-date {\n font-size: 11px;\n color: #6c757d;\n}\n\n/* Responsive adjustments */\n@media (max-width: 1200px) {\n .sidebar {\n width: 320px;\n }\n}\n\n@media (max-width: 992px) {\n .message-user,\n .message-assistant {\n max-width: 85%;\n }\n \n .sidebar {\n width: 280px;\n }\n}\n\n/* Custom scrollbar */\n.messages-container::-webkit-scrollbar,\n.sidebar-content::-webkit-scrollbar {\n width: 8px;\n}\n\n.messages-container::-webkit-scrollbar-track,\n.sidebar-content::-webkit-scrollbar-track {\n background: #f1f1f1;\n}\n\n.messages-container::-webkit-scrollbar-thumb,\n.sidebar-content::-webkit-scrollbar-thumb {\n background: #c1c1c1;\n border-radius: 4px;\n}\n\n.messages-container::-webkit-scrollbar-thumb:hover,\n.sidebar-content::-webkit-scrollbar-thumb:hover {\n background: #a1a1a1;\n}\n\n/* Dialog customization */\n::ng-deep .ai-agent-test-harness .k-dialog-wrapper {\n border-radius: 8px;\n overflow: hidden;\n}\n\n::ng-deep .ai-agent-test-harness .k-dialog-titlebar {\n background: #343a40;\n color: white;\n}\n\n::ng-deep .ai-agent-test-harness .k-dialog-title {\n font-weight: 600;\n}\n\n::ng-deep .ai-agent-test-harness .k-dialog-titlebar-actions .k-button {\n color: white;\n}\n\n::ng-deep .ai-agent-test-harness .k-dialog-content {\n padding: 0;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n/* JSON Dialog Styles - removed since we're using WindowService */\n\n/* Advanced Parameters Styles */\n.advanced-params-panel {\n margin-top: 24px;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n}\n\n.advanced-params-panel ::ng-deep .k-expander-title {\n font-weight: 600;\n color: #495057;\n}\n\n.advanced-settings {\n padding: 16px;\n}\n\n.advanced-param-input {\n width: 100%;\n}\n\n.advanced-param-switch {\n margin-top: 8px;\n}\n\n/* Format selector dropdown */\n.format-selector {\n width: 100%;\n}"] }]
|
|
3434
3526
|
}], () => [{ type: i1.DomSanitizer }, { type: i2.WindowService }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: i3.Router }], { mode: [{
|
|
3435
3527
|
type: Input
|
|
3436
3528
|
}], entity: [{
|
|
@@ -3443,6 +3535,8 @@ export class AITestHarnessComponent {
|
|
|
3443
3535
|
type: Output
|
|
3444
3536
|
}], runOpened: [{
|
|
3445
3537
|
type: Output
|
|
3538
|
+
}], minimizeRequested: [{
|
|
3539
|
+
type: Output
|
|
3446
3540
|
}], messagesContainer: [{
|
|
3447
3541
|
type: ViewChild,
|
|
3448
3542
|
args: ['messagesContainer']
|
|
@@ -3456,5 +3550,5 @@ export class AITestHarnessComponent {
|
|
|
3456
3550
|
type: ViewChild,
|
|
3457
3551
|
args: ['saveDialogInput']
|
|
3458
3552
|
}] }); })();
|
|
3459
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(AITestHarnessComponent, { className: "AITestHarnessComponent", filePath: "lib/ai-test-harness.component.ts", lineNumber:
|
|
3553
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(AITestHarnessComponent, { className: "AITestHarnessComponent", filePath: "lib/ai-test-harness.component.ts", lineNumber: 151 }); })();
|
|
3460
3554
|
//# sourceMappingURL=ai-test-harness.component.js.map
|