@runflow-ai/cli 0.2.11 → 0.2.13
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/CLI-DOCS.md +89 -0
- package/QUICK-TEST-GUIDE.md +273 -0
- package/dist/commands/test/test.command.d.ts +19 -12
- package/dist/commands/test/test.command.js +703 -212
- package/dist/commands/test/test.command.js.map +1 -1
- package/package.json +3 -2
- package/static/dist-test/assets/favico.avif +0 -0
- package/static/dist-test/assets/logo_runflow_positive.png +0 -0
- package/static/dist-test/assets/main-ClrC9fUE.css +1 -0
- package/static/dist-test/assets/main-rM2NnEnW.js +53 -0
- package/static/dist-test/assets/runflow-logo.png +0 -0
- package/static/dist-test/index-test.html +16 -0
- package/static/dist-test/widget/runflow-widget.js +221 -0
- package/static/app.js +0 -1381
- package/static/frontend-server-template.js +0 -24
- package/static/index.html +0 -340
- package/static/style.css +0 -1354
- package/static/test-server-template.js +0 -641
package/static/app.js
DELETED
|
@@ -1,1381 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runflow Test Interface - Enterprise Modular Architecture
|
|
3
|
-
* @version 2.0.0
|
|
4
|
-
* @description Professional frontend for testing Runflow agents with improved developer experience
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// ============================================================================
|
|
8
|
-
// Module System - Simple but Effective Module Pattern
|
|
9
|
-
// ============================================================================
|
|
10
|
-
|
|
11
|
-
const Runflow = (() => {
|
|
12
|
-
'use strict';
|
|
13
|
-
|
|
14
|
-
// Private module storage
|
|
15
|
-
const modules = {};
|
|
16
|
-
const config = {};
|
|
17
|
-
|
|
18
|
-
// Public API
|
|
19
|
-
return {
|
|
20
|
-
define(name, dependencies, factory) {
|
|
21
|
-
if (typeof dependencies === 'function') {
|
|
22
|
-
factory = dependencies;
|
|
23
|
-
dependencies = [];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const resolvedDeps = dependencies.map(dep => modules[dep]);
|
|
27
|
-
modules[name] = factory(...resolvedDeps);
|
|
28
|
-
return modules[name];
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
get(name) {
|
|
32
|
-
return modules[name];
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
config: config
|
|
36
|
-
};
|
|
37
|
-
})();
|
|
38
|
-
|
|
39
|
-
// ============================================================================
|
|
40
|
-
// Configuration Module
|
|
41
|
-
// ============================================================================
|
|
42
|
-
|
|
43
|
-
Runflow.define('Config', () => {
|
|
44
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
45
|
-
const backendPort = urlParams.get('backend') || '8547';
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
api: {
|
|
49
|
-
baseURL: `http://localhost:${backendPort}`,
|
|
50
|
-
timeout: 30000,
|
|
51
|
-
retryAttempts: 3,
|
|
52
|
-
retryDelay: 2000
|
|
53
|
-
},
|
|
54
|
-
ui: {
|
|
55
|
-
maxMessages: 100,
|
|
56
|
-
autoScroll: true,
|
|
57
|
-
theme: 'light',
|
|
58
|
-
animations: true
|
|
59
|
-
},
|
|
60
|
-
session: {
|
|
61
|
-
prefix: 'runflow-test',
|
|
62
|
-
storageKey: 'runflow-session-data'
|
|
63
|
-
},
|
|
64
|
-
features: {
|
|
65
|
-
scenarios: true,
|
|
66
|
-
export: true,
|
|
67
|
-
keyboard: true,
|
|
68
|
-
autosave: false
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// ============================================================================
|
|
74
|
-
// Event Bus - Central Communication Hub
|
|
75
|
-
// ============================================================================
|
|
76
|
-
|
|
77
|
-
Runflow.define('EventBus', () => {
|
|
78
|
-
const events = {};
|
|
79
|
-
let debug = false;
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
on(event, handler, context = null) {
|
|
83
|
-
if (!events[event]) events[event] = [];
|
|
84
|
-
events[event].push({ handler, context });
|
|
85
|
-
|
|
86
|
-
// Return unsubscribe function
|
|
87
|
-
return () => this.off(event, handler);
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
off(event, handler) {
|
|
91
|
-
if (!events[event]) return;
|
|
92
|
-
events[event] = events[event].filter(h => h.handler !== handler);
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
emit(event, data = {}) {
|
|
96
|
-
if (debug) console.log(`[Event] ${event}`, data);
|
|
97
|
-
if (!events[event]) return;
|
|
98
|
-
|
|
99
|
-
events[event].forEach(({ handler, context }) => {
|
|
100
|
-
try {
|
|
101
|
-
handler.call(context, data);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.error(`Error in event handler for ${event}:`, error);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
once(event, handler, context = null) {
|
|
109
|
-
const wrapper = (data) => {
|
|
110
|
-
this.off(event, wrapper);
|
|
111
|
-
handler.call(context, data);
|
|
112
|
-
};
|
|
113
|
-
this.on(event, wrapper);
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
setDebug(enabled) {
|
|
117
|
-
debug = enabled;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// ============================================================================
|
|
123
|
-
// State Manager - Centralized State with Subscriptions
|
|
124
|
-
// ============================================================================
|
|
125
|
-
|
|
126
|
-
Runflow.define('StateManager', ['EventBus'], (EventBus) => {
|
|
127
|
-
const state = {
|
|
128
|
-
connection: {
|
|
129
|
-
status: 'disconnected',
|
|
130
|
-
backend: null,
|
|
131
|
-
lastPing: null
|
|
132
|
-
},
|
|
133
|
-
session: {
|
|
134
|
-
id: generateSessionId(),
|
|
135
|
-
startTime: new Date(),
|
|
136
|
-
messageCount: 0,
|
|
137
|
-
lastResponseTime: null
|
|
138
|
-
},
|
|
139
|
-
ui: {
|
|
140
|
-
loading: false,
|
|
141
|
-
testMode: 'simple',
|
|
142
|
-
currentScenario: null
|
|
143
|
-
},
|
|
144
|
-
messages: [],
|
|
145
|
-
scenarios: []
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const subscribers = new Map();
|
|
149
|
-
|
|
150
|
-
function generateSessionId() {
|
|
151
|
-
return `runflow-test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function getValueByPath(obj, path) {
|
|
155
|
-
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function setValueByPath(obj, path, value) {
|
|
159
|
-
const keys = path.split('.');
|
|
160
|
-
const lastKey = keys.pop();
|
|
161
|
-
const target = keys.reduce((current, key) => {
|
|
162
|
-
if (!current[key]) current[key] = {};
|
|
163
|
-
return current[key];
|
|
164
|
-
}, obj);
|
|
165
|
-
|
|
166
|
-
const oldValue = target[lastKey];
|
|
167
|
-
target[lastKey] = value;
|
|
168
|
-
return oldValue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
get(path) {
|
|
173
|
-
return path ? getValueByPath(state, path) : state;
|
|
174
|
-
},
|
|
175
|
-
|
|
176
|
-
set(path, value) {
|
|
177
|
-
const oldValue = setValueByPath(state, path, value);
|
|
178
|
-
EventBus.emit('state:changed', { path, oldValue, newValue: value });
|
|
179
|
-
this.notifySubscribers(path, value);
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
update(path, updater) {
|
|
183
|
-
const current = this.get(path);
|
|
184
|
-
this.set(path, updater(current));
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
subscribe(path, callback) {
|
|
188
|
-
if (!subscribers.has(path)) {
|
|
189
|
-
subscribers.set(path, new Set());
|
|
190
|
-
}
|
|
191
|
-
subscribers.get(path).add(callback);
|
|
192
|
-
|
|
193
|
-
return () => {
|
|
194
|
-
subscribers.get(path)?.delete(callback);
|
|
195
|
-
};
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
notifySubscribers(path, value) {
|
|
199
|
-
subscribers.get(path)?.forEach(callback => {
|
|
200
|
-
try {
|
|
201
|
-
callback(value);
|
|
202
|
-
} catch (error) {
|
|
203
|
-
console.error(`Subscriber error for ${path}:`, error);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// ============================================================================
|
|
211
|
-
// API Client - Clean HTTP Communication Layer
|
|
212
|
-
// ============================================================================
|
|
213
|
-
|
|
214
|
-
Runflow.define('APIClient', ['Config', 'EventBus'], (Config, EventBus) => {
|
|
215
|
-
class APIClient {
|
|
216
|
-
constructor() {
|
|
217
|
-
this.baseURL = Config.api.baseURL;
|
|
218
|
-
this.timeout = Config.api.timeout;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async request(method, endpoint, data = null) {
|
|
222
|
-
const controller = new AbortController();
|
|
223
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
const options = {
|
|
227
|
-
method,
|
|
228
|
-
headers: { 'Content-Type': 'application/json' },
|
|
229
|
-
signal: controller.signal
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
if (data) {
|
|
233
|
-
options.body = JSON.stringify(data);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const response = await fetch(`${this.baseURL}${endpoint}`, options);
|
|
237
|
-
clearTimeout(timeoutId);
|
|
238
|
-
|
|
239
|
-
if (!response.ok) {
|
|
240
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const contentType = response.headers.get('content-type');
|
|
244
|
-
return contentType?.includes('application/json')
|
|
245
|
-
? await response.json()
|
|
246
|
-
: await response.text();
|
|
247
|
-
|
|
248
|
-
} catch (error) {
|
|
249
|
-
clearTimeout(timeoutId);
|
|
250
|
-
EventBus.emit('api:error', { endpoint, error: error.message });
|
|
251
|
-
throw error;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
get(endpoint) {
|
|
256
|
-
return this.request('GET', endpoint);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
post(endpoint, data) {
|
|
260
|
-
return this.request('POST', endpoint, data);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async checkHealth() {
|
|
264
|
-
try {
|
|
265
|
-
await this.get('/api/health');
|
|
266
|
-
EventBus.emit('api:health', { status: 'healthy' });
|
|
267
|
-
return true;
|
|
268
|
-
} catch (error) {
|
|
269
|
-
EventBus.emit('api:health', { status: 'unhealthy', error });
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return new APIClient();
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// ============================================================================
|
|
279
|
-
// UI Components - Reusable UI Elements
|
|
280
|
-
// ============================================================================
|
|
281
|
-
|
|
282
|
-
Runflow.define('UIComponents', () => {
|
|
283
|
-
// Helper to safely stringify objects with circular references
|
|
284
|
-
function safeStringify(obj, indent = 2) {
|
|
285
|
-
const cache = new Set();
|
|
286
|
-
try {
|
|
287
|
-
return JSON.stringify(obj, (key, value) => {
|
|
288
|
-
if (typeof value === 'object' && value !== null) {
|
|
289
|
-
if (cache.has(value)) {
|
|
290
|
-
return '[Circular Reference]';
|
|
291
|
-
}
|
|
292
|
-
cache.add(value);
|
|
293
|
-
}
|
|
294
|
-
return value;
|
|
295
|
-
}, indent);
|
|
296
|
-
} catch (error) {
|
|
297
|
-
return JSON.stringify({ error: 'Could not serialize object', message: error.message }, null, indent);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
Message: {
|
|
303
|
-
create(type, content, metadata = {}) {
|
|
304
|
-
const div = document.createElement('div');
|
|
305
|
-
div.className = `message ${type}`;
|
|
306
|
-
|
|
307
|
-
// Format content
|
|
308
|
-
let formattedContent = content;
|
|
309
|
-
if (typeof content === 'object') {
|
|
310
|
-
formattedContent = `<pre>${JSON.stringify(content, null, 2)}</pre>`;
|
|
311
|
-
} else {
|
|
312
|
-
// Convert markdown code blocks to pre tags
|
|
313
|
-
formattedContent = content.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
|
314
|
-
// Convert line breaks to <br> for better formatting
|
|
315
|
-
formattedContent = formattedContent.replace(/\n/g, '<br>');
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Get display settings
|
|
319
|
-
const showMetadata = document.getElementById('showMetadata')?.checked !== false;
|
|
320
|
-
const showJsonToggle = document.getElementById('showJsonToggle')?.checked !== false;
|
|
321
|
-
const autoExpandJson = document.getElementById('autoExpandJson')?.checked === true;
|
|
322
|
-
|
|
323
|
-
// Create metadata display if available and enabled
|
|
324
|
-
let metadataDisplay = '';
|
|
325
|
-
if (showMetadata && (metadata.model || metadata.provider)) {
|
|
326
|
-
metadataDisplay = `
|
|
327
|
-
<div class="message-metadata">
|
|
328
|
-
${metadata.model ? `<span class="meta-item">${metadata.model}</span>` : ''}
|
|
329
|
-
${metadata.provider ? `<span class="meta-item">${metadata.provider}</span>` : ''}
|
|
330
|
-
${metadata.sessionId ? `<span class="meta-item">Session: ${metadata.sessionId}</span>` : ''}
|
|
331
|
-
</div>
|
|
332
|
-
`;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Create collapsible JSON view if full response available and enabled
|
|
336
|
-
let jsonToggle = '';
|
|
337
|
-
if (showJsonToggle && metadata.fullResponse && type === 'assistant') {
|
|
338
|
-
const jsonId = `json-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
339
|
-
const isExpanded = autoExpandJson ? 'block' : 'none';
|
|
340
|
-
const buttonText = autoExpandJson ? 'Hide JSON' : 'Show JSON';
|
|
341
|
-
|
|
342
|
-
// Use UIComponents.safeStringify to handle any circular references
|
|
343
|
-
const UIComponents = Runflow.get('UIComponents');
|
|
344
|
-
const jsonString = UIComponents.safeStringify(metadata.fullResponse, 2);
|
|
345
|
-
|
|
346
|
-
jsonToggle = `
|
|
347
|
-
<div class="json-toggle-wrapper">
|
|
348
|
-
<button class="json-toggle" onclick="
|
|
349
|
-
const el = document.getElementById('${jsonId}');
|
|
350
|
-
const btn = this;
|
|
351
|
-
if (el.style.display === 'none') {
|
|
352
|
-
el.style.display = 'block';
|
|
353
|
-
btn.textContent = 'Hide JSON';
|
|
354
|
-
} else {
|
|
355
|
-
el.style.display = 'none';
|
|
356
|
-
btn.textContent = 'Show JSON';
|
|
357
|
-
}
|
|
358
|
-
">${buttonText}</button>
|
|
359
|
-
<div id="${jsonId}" class="json-response" style="display: ${isExpanded};">
|
|
360
|
-
<pre>${jsonString}</pre>
|
|
361
|
-
</div>
|
|
362
|
-
</div>
|
|
363
|
-
`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
div.innerHTML = `
|
|
367
|
-
<div class="message-bubble">
|
|
368
|
-
<div class="content">${formattedContent}</div>
|
|
369
|
-
${metadataDisplay}
|
|
370
|
-
<div class="message-footer">
|
|
371
|
-
<span class="timestamp">${new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}</span>
|
|
372
|
-
${metadata.duration ? `<span class="duration">${metadata.duration}ms</span>` : ''}
|
|
373
|
-
</div>
|
|
374
|
-
${jsonToggle}
|
|
375
|
-
</div>
|
|
376
|
-
`;
|
|
377
|
-
|
|
378
|
-
return div;
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
|
|
382
|
-
StatusIndicator: {
|
|
383
|
-
update(status, message) {
|
|
384
|
-
const el = document.getElementById('status');
|
|
385
|
-
if (el) {
|
|
386
|
-
el.className = `connection-status status ${status}`;
|
|
387
|
-
el.innerHTML = `<span class="status-indicator dot"></span><span class="status-text">${message}</span>`;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
},
|
|
391
|
-
|
|
392
|
-
Toast: {
|
|
393
|
-
show(message, type = 'info', duration = 3000) {
|
|
394
|
-
const toast = document.createElement('div');
|
|
395
|
-
toast.className = `toast toast-${type}`;
|
|
396
|
-
toast.textContent = message;
|
|
397
|
-
toast.style.cssText = `
|
|
398
|
-
position: fixed;
|
|
399
|
-
bottom: 20px;
|
|
400
|
-
right: 20px;
|
|
401
|
-
padding: 12px 20px;
|
|
402
|
-
background: ${type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#3b82f6'};
|
|
403
|
-
color: white;
|
|
404
|
-
border-radius: 8px;
|
|
405
|
-
font-size: 14px;
|
|
406
|
-
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
407
|
-
transform: translateX(400px);
|
|
408
|
-
transition: transform 0.3s ease;
|
|
409
|
-
z-index: 1000;
|
|
410
|
-
`;
|
|
411
|
-
|
|
412
|
-
document.body.appendChild(toast);
|
|
413
|
-
|
|
414
|
-
setTimeout(() => {
|
|
415
|
-
toast.style.transform = 'translateX(0)';
|
|
416
|
-
}, 10);
|
|
417
|
-
|
|
418
|
-
setTimeout(() => {
|
|
419
|
-
toast.style.transform = 'translateX(400px)';
|
|
420
|
-
setTimeout(() => toast.remove(), 300);
|
|
421
|
-
}, duration);
|
|
422
|
-
}
|
|
423
|
-
},
|
|
424
|
-
|
|
425
|
-
safeStringify // Export the helper function
|
|
426
|
-
};
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// ============================================================================
|
|
430
|
-
// Chat Manager - Message Handling Logic
|
|
431
|
-
// ============================================================================
|
|
432
|
-
|
|
433
|
-
Runflow.define('ChatManager', ['APIClient', 'StateManager', 'EventBus', 'UIComponents'],
|
|
434
|
-
(APIClient, StateManager, EventBus, UIComponents) => {
|
|
435
|
-
|
|
436
|
-
let messagesContainer = null;
|
|
437
|
-
|
|
438
|
-
async function sendMessage(content, mode = 'simple', payload = null) {
|
|
439
|
-
const startTime = Date.now();
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
// Add user message
|
|
443
|
-
addMessage('user', content || 'Custom payload sent');
|
|
444
|
-
|
|
445
|
-
// Update state
|
|
446
|
-
StateManager.set('ui.loading', true);
|
|
447
|
-
StateManager.update('session.messageCount', c => c + 1);
|
|
448
|
-
|
|
449
|
-
// Prepare request
|
|
450
|
-
const requestData = mode === 'custom' && payload
|
|
451
|
-
? JSON.parse(payload)
|
|
452
|
-
: { message: content };
|
|
453
|
-
|
|
454
|
-
// Send to backend
|
|
455
|
-
const response = await APIClient.post('/api/chat', requestData);
|
|
456
|
-
|
|
457
|
-
// Calculate duration
|
|
458
|
-
const duration = Date.now() - startTime;
|
|
459
|
-
StateManager.set('session.lastResponseTime', duration);
|
|
460
|
-
|
|
461
|
-
// Handle response format
|
|
462
|
-
let messageContent = response;
|
|
463
|
-
let metadata = {};
|
|
464
|
-
let fullResponseCopy = null;
|
|
465
|
-
|
|
466
|
-
if (typeof response === 'object') {
|
|
467
|
-
// Get configured response path
|
|
468
|
-
const responsePath = document.getElementById('responsePath')?.value || 'data.message';
|
|
469
|
-
|
|
470
|
-
// Try to get message from configured path
|
|
471
|
-
messageContent = getValueByPath(response, responsePath);
|
|
472
|
-
|
|
473
|
-
// Fallback to common paths if configured path doesn't work
|
|
474
|
-
if (!messageContent) {
|
|
475
|
-
if (response.data?.message) {
|
|
476
|
-
messageContent = response.data.message;
|
|
477
|
-
metadata = response.data.metadata || {};
|
|
478
|
-
} else if (response.data?.content) {
|
|
479
|
-
messageContent = response.data.content;
|
|
480
|
-
metadata = response.data.metadata || {};
|
|
481
|
-
} else if (response.message) {
|
|
482
|
-
messageContent = response.message;
|
|
483
|
-
metadata = response.metadata || {};
|
|
484
|
-
} else if (response.content) {
|
|
485
|
-
messageContent = response.content;
|
|
486
|
-
metadata = response.metadata || {};
|
|
487
|
-
} else if (response.reply) {
|
|
488
|
-
messageContent = response.reply;
|
|
489
|
-
}
|
|
490
|
-
} else {
|
|
491
|
-
// Try to get metadata from data.metadata or response.metadata
|
|
492
|
-
metadata = response.data?.metadata || response.metadata || {};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Create a clean copy of the response for display (avoid circular references)
|
|
496
|
-
try {
|
|
497
|
-
fullResponseCopy = JSON.parse(JSON.stringify(response));
|
|
498
|
-
} catch (e) {
|
|
499
|
-
// If circular reference exists, create a simplified version
|
|
500
|
-
fullResponseCopy = {
|
|
501
|
-
success: response.success,
|
|
502
|
-
data: {
|
|
503
|
-
message: messageContent,
|
|
504
|
-
metadata: {...metadata}
|
|
505
|
-
},
|
|
506
|
-
timestamp: response.timestamp || new Date().toISOString()
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Helper function to get value by path
|
|
512
|
-
function getValueByPath(obj, path) {
|
|
513
|
-
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Add assistant response with clean metadata
|
|
517
|
-
addMessage('assistant', messageContent, {
|
|
518
|
-
...metadata,
|
|
519
|
-
duration,
|
|
520
|
-
fullResponse: fullResponseCopy
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
EventBus.emit('message:sent', { response, duration });
|
|
524
|
-
|
|
525
|
-
} catch (error) {
|
|
526
|
-
addMessage('error', `Error: ${error.message}`);
|
|
527
|
-
UIComponents.Toast.show(error.message, 'error');
|
|
528
|
-
} finally {
|
|
529
|
-
StateManager.set('ui.loading', false);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function addMessage(type, content, metadata = {}) {
|
|
534
|
-
if (!messagesContainer) {
|
|
535
|
-
messagesContainer = document.getElementById('messages');
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const messageEl = UIComponents.Message.create(type, content, metadata);
|
|
539
|
-
messagesContainer.appendChild(messageEl);
|
|
540
|
-
|
|
541
|
-
// Auto scroll
|
|
542
|
-
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
543
|
-
|
|
544
|
-
// Update state
|
|
545
|
-
StateManager.update('messages', msgs => [
|
|
546
|
-
...msgs,
|
|
547
|
-
{ type, content, metadata, timestamp: new Date() }
|
|
548
|
-
]);
|
|
549
|
-
|
|
550
|
-
// Limit messages
|
|
551
|
-
const messages = messagesContainer.querySelectorAll('.message');
|
|
552
|
-
if (messages.length > 100) {
|
|
553
|
-
messages[0].remove();
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function clearAll() {
|
|
558
|
-
if (!messagesContainer) {
|
|
559
|
-
messagesContainer = document.getElementById('messages');
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
messagesContainer.innerHTML = `
|
|
563
|
-
<div class="message system">
|
|
564
|
-
<div class="message-bubble">
|
|
565
|
-
<div class="content">Chat cleared. Ready for new messages.</div>
|
|
566
|
-
<div class="timestamp">${new Date().toLocaleTimeString()}</div>
|
|
567
|
-
</div>
|
|
568
|
-
</div>
|
|
569
|
-
`;
|
|
570
|
-
|
|
571
|
-
StateManager.set('messages', []);
|
|
572
|
-
StateManager.set('session.messageCount', 0);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function exportChat() {
|
|
576
|
-
const messages = StateManager.get('messages');
|
|
577
|
-
const session = StateManager.get('session');
|
|
578
|
-
|
|
579
|
-
const data = {
|
|
580
|
-
session: {
|
|
581
|
-
id: session.id,
|
|
582
|
-
startTime: session.startTime,
|
|
583
|
-
messageCount: session.messageCount
|
|
584
|
-
},
|
|
585
|
-
messages: messages
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
589
|
-
const url = URL.createObjectURL(blob);
|
|
590
|
-
const a = document.createElement('a');
|
|
591
|
-
a.href = url;
|
|
592
|
-
a.download = `runflow-chat-${Date.now()}.json`;
|
|
593
|
-
a.click();
|
|
594
|
-
URL.revokeObjectURL(url);
|
|
595
|
-
|
|
596
|
-
UIComponents.Toast.show('Chat exported successfully', 'success');
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
return {
|
|
600
|
-
sendMessage,
|
|
601
|
-
addMessage,
|
|
602
|
-
clearAll,
|
|
603
|
-
exportChat
|
|
604
|
-
};
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
// ============================================================================
|
|
608
|
-
// Scenario Manager - Test Scenarios Handling
|
|
609
|
-
// ============================================================================
|
|
610
|
-
|
|
611
|
-
Runflow.define('ScenarioManager', ['APIClient', 'StateManager', 'EventBus'],
|
|
612
|
-
(APIClient, StateManager, EventBus) => {
|
|
613
|
-
|
|
614
|
-
let scenarioSelect = null;
|
|
615
|
-
let variablesSection = null;
|
|
616
|
-
let variablesContainer = null;
|
|
617
|
-
let payloadInput = null;
|
|
618
|
-
|
|
619
|
-
async function loadScenarios() {
|
|
620
|
-
try {
|
|
621
|
-
const response = await APIClient.get('/api/scenarios');
|
|
622
|
-
// Handle both formats: direct array or {scenarios: [...]}
|
|
623
|
-
const scenarios = Array.isArray(response) ? response : (response.scenarios || []);
|
|
624
|
-
StateManager.set('scenarios', scenarios);
|
|
625
|
-
populateScenarioSelect(scenarios);
|
|
626
|
-
} catch (error) {
|
|
627
|
-
console.warn('Failed to load scenarios:', error);
|
|
628
|
-
populateScenarioSelect([]);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function populateScenarioSelect(scenarios) {
|
|
633
|
-
if (!scenarioSelect) {
|
|
634
|
-
scenarioSelect = document.getElementById('scenarioSelect');
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
scenarioSelect.innerHTML = '<option value="">-- Select scenario --</option>';
|
|
638
|
-
|
|
639
|
-
// Group scenarios by category
|
|
640
|
-
const grouped = {};
|
|
641
|
-
scenarios.forEach(scenario => {
|
|
642
|
-
const category = scenario.category || 'Other';
|
|
643
|
-
if (!grouped[category]) {
|
|
644
|
-
grouped[category] = [];
|
|
645
|
-
}
|
|
646
|
-
grouped[category].push(scenario);
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
// Create optgroups for each category
|
|
650
|
-
Object.keys(grouped).sort().forEach(category => {
|
|
651
|
-
const optgroup = document.createElement('optgroup');
|
|
652
|
-
optgroup.label = category;
|
|
653
|
-
|
|
654
|
-
grouped[category].forEach(scenario => {
|
|
655
|
-
const option = document.createElement('option');
|
|
656
|
-
option.value = scenario.id;
|
|
657
|
-
option.textContent = scenario.name;
|
|
658
|
-
optgroup.appendChild(option);
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
scenarioSelect.appendChild(optgroup);
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function loadScenario(scenarioId) {
|
|
666
|
-
if (!scenarioId) {
|
|
667
|
-
clearScenario();
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const scenarios = StateManager.get('scenarios');
|
|
672
|
-
const scenario = scenarios.find(s => s.id === scenarioId);
|
|
673
|
-
|
|
674
|
-
if (!scenario) return;
|
|
675
|
-
|
|
676
|
-
StateManager.set('ui.currentScenario', scenario);
|
|
677
|
-
|
|
678
|
-
if (scenario.variables && Object.keys(scenario.variables).length > 0) {
|
|
679
|
-
showVariables(scenario.variables);
|
|
680
|
-
updatePayloadPreview(scenario);
|
|
681
|
-
} else {
|
|
682
|
-
hideVariables();
|
|
683
|
-
if (!payloadInput) payloadInput = document.getElementById('payloadInput');
|
|
684
|
-
payloadInput.value = JSON.stringify(scenario.payload, null, 2);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
EventBus.emit('scenario:loaded', scenario);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function showVariables(variables) {
|
|
691
|
-
if (!variablesSection) {
|
|
692
|
-
variablesSection = document.getElementById('variablesSection');
|
|
693
|
-
variablesContainer = document.getElementById('variablesContainer');
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
variablesSection.style.display = 'block';
|
|
697
|
-
variablesContainer.innerHTML = '';
|
|
698
|
-
|
|
699
|
-
Object.entries(variables).forEach(([key, config]) => {
|
|
700
|
-
const div = document.createElement('div');
|
|
701
|
-
div.style.marginBottom = '8px';
|
|
702
|
-
|
|
703
|
-
if (config.type === 'select' && config.options) {
|
|
704
|
-
div.innerHTML = `
|
|
705
|
-
<label style="display: block; font-size: 11px; color: #64748b; margin-bottom: 2px;">
|
|
706
|
-
${config.description || key}:
|
|
707
|
-
</label>
|
|
708
|
-
<select data-variable="${key}" style="width: 100%; padding: 4px; font-size: 11px; border: 1px solid #cbd5e1; border-radius: 3px;">
|
|
709
|
-
${config.options.map(opt =>
|
|
710
|
-
`<option value="${opt}" ${opt === config.default ? 'selected' : ''}>${opt}</option>`
|
|
711
|
-
).join('')}
|
|
712
|
-
</select>
|
|
713
|
-
`;
|
|
714
|
-
} else {
|
|
715
|
-
div.innerHTML = `
|
|
716
|
-
<label style="display: block; font-size: 11px; color: #64748b; margin-bottom: 2px;">
|
|
717
|
-
${config.description || key}:
|
|
718
|
-
</label>
|
|
719
|
-
<input
|
|
720
|
-
type="${config.type || 'text'}"
|
|
721
|
-
data-variable="${key}"
|
|
722
|
-
value="${config.default || ''}"
|
|
723
|
-
placeholder="${config.placeholder || ''}"
|
|
724
|
-
style="width: 100%; padding: 4px; font-size: 11px; border: 1px solid #cbd5e1; border-radius: 3px;"
|
|
725
|
-
/>
|
|
726
|
-
`;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
variablesContainer.appendChild(div);
|
|
730
|
-
|
|
731
|
-
const input = div.querySelector('input, select');
|
|
732
|
-
input.addEventListener('input', () => updatePayloadPreview());
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function hideVariables() {
|
|
737
|
-
if (!variablesSection) {
|
|
738
|
-
variablesSection = document.getElementById('variablesSection');
|
|
739
|
-
variablesContainer = document.getElementById('variablesContainer');
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
variablesSection.style.display = 'none';
|
|
743
|
-
variablesContainer.innerHTML = '';
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function updatePayloadPreview(scenario) {
|
|
747
|
-
if (!scenario) {
|
|
748
|
-
scenario = StateManager.get('ui.currentScenario');
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (!scenario || !scenario.payload) return;
|
|
752
|
-
|
|
753
|
-
if (!payloadInput) payloadInput = document.getElementById('payloadInput');
|
|
754
|
-
if (!variablesContainer) variablesContainer = document.getElementById('variablesContainer');
|
|
755
|
-
|
|
756
|
-
let payloadStr = JSON.stringify(scenario.payload, null, 2);
|
|
757
|
-
|
|
758
|
-
// Replace variables
|
|
759
|
-
variablesContainer.querySelectorAll('[data-variable]').forEach(input => {
|
|
760
|
-
const key = input.dataset.variable;
|
|
761
|
-
const value = input.value;
|
|
762
|
-
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
|
|
763
|
-
payloadStr = payloadStr.replace(regex, value);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
payloadInput.value = payloadStr;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function clearScenario() {
|
|
770
|
-
StateManager.set('ui.currentScenario', null);
|
|
771
|
-
hideVariables();
|
|
772
|
-
if (!payloadInput) payloadInput = document.getElementById('payloadInput');
|
|
773
|
-
payloadInput.value = '';
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
return {
|
|
777
|
-
loadScenarios,
|
|
778
|
-
loadScenario,
|
|
779
|
-
clearScenario
|
|
780
|
-
};
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
// ============================================================================
|
|
784
|
-
// Connection Monitor - Health Check Service
|
|
785
|
-
// ============================================================================
|
|
786
|
-
|
|
787
|
-
Runflow.define('ConnectionMonitor', ['APIClient', 'StateManager', 'EventBus', 'UIComponents', 'Config'],
|
|
788
|
-
(APIClient, StateManager, EventBus, UIComponents, Config) => {
|
|
789
|
-
|
|
790
|
-
let healthInterval = null;
|
|
791
|
-
let retryCount = 0;
|
|
792
|
-
|
|
793
|
-
async function start() {
|
|
794
|
-
await checkHealth();
|
|
795
|
-
startPeriodicChecks();
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
async function checkHealth() {
|
|
799
|
-
try {
|
|
800
|
-
const isHealthy = await APIClient.checkHealth();
|
|
801
|
-
|
|
802
|
-
if (isHealthy) {
|
|
803
|
-
onConnectionSuccess();
|
|
804
|
-
} else {
|
|
805
|
-
onConnectionFailure();
|
|
806
|
-
}
|
|
807
|
-
} catch (error) {
|
|
808
|
-
onConnectionFailure(error);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function onConnectionSuccess() {
|
|
813
|
-
StateManager.set('connection.status', 'connected');
|
|
814
|
-
StateManager.set('connection.lastPing', new Date());
|
|
815
|
-
|
|
816
|
-
UIComponents.StatusIndicator.update('connected', 'Connected');
|
|
817
|
-
|
|
818
|
-
const agentStatus = document.getElementById('agentStatus');
|
|
819
|
-
if (agentStatus) {
|
|
820
|
-
agentStatus.textContent = 'Ready';
|
|
821
|
-
agentStatus.className = 'status-ready';
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
retryCount = 0;
|
|
825
|
-
hideLoadingOverlay();
|
|
826
|
-
|
|
827
|
-
EventBus.emit('connection:established');
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function onConnectionFailure(error) {
|
|
831
|
-
StateManager.set('connection.status', 'disconnected');
|
|
832
|
-
UIComponents.StatusIndicator.update('error', 'Disconnected');
|
|
833
|
-
|
|
834
|
-
const agentStatus = document.getElementById('agentStatus');
|
|
835
|
-
if (agentStatus) {
|
|
836
|
-
agentStatus.textContent = 'Offline';
|
|
837
|
-
agentStatus.className = 'status-error';
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
retryCount++;
|
|
841
|
-
|
|
842
|
-
if (retryCount <= Config.api.retryAttempts) {
|
|
843
|
-
setTimeout(() => checkHealth(), Config.api.retryDelay * retryCount);
|
|
844
|
-
} else {
|
|
845
|
-
UIComponents.Toast.show('Unable to connect to backend', 'error');
|
|
846
|
-
EventBus.emit('connection:failed');
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function startPeriodicChecks() {
|
|
851
|
-
healthInterval = setInterval(() => checkHealth(), 30000);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function stop() {
|
|
855
|
-
if (healthInterval) {
|
|
856
|
-
clearInterval(healthInterval);
|
|
857
|
-
healthInterval = null;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function hideLoadingOverlay() {
|
|
862
|
-
const overlay = document.getElementById('loadingOverlay');
|
|
863
|
-
if (overlay) {
|
|
864
|
-
overlay.style.opacity = '0';
|
|
865
|
-
setTimeout(() => overlay.style.display = 'none', 300);
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
return {
|
|
870
|
-
start,
|
|
871
|
-
checkHealth,
|
|
872
|
-
stop
|
|
873
|
-
};
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
// ============================================================================
|
|
877
|
-
// Traces Manager - Local Traces Visualization
|
|
878
|
-
// ============================================================================
|
|
879
|
-
|
|
880
|
-
Runflow.define('TracesManager', ['APIClient', 'StateManager', 'EventBus', 'UIComponents'],
|
|
881
|
-
(APIClient, StateManager, EventBus, UIComponents) => {
|
|
882
|
-
|
|
883
|
-
async function loadTraces() {
|
|
884
|
-
try {
|
|
885
|
-
const data = await APIClient.get('/api/traces');
|
|
886
|
-
renderThreads(data.threads || []);
|
|
887
|
-
} catch (error) {
|
|
888
|
-
console.error('Failed to load traces:', error);
|
|
889
|
-
document.getElementById('tracesContainer').innerHTML = `
|
|
890
|
-
<div style="text-align: center; padding: 20px; color: var(--error);">
|
|
891
|
-
❌ Failed to load traces
|
|
892
|
-
</div>
|
|
893
|
-
`;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
function renderThreads(threads) {
|
|
898
|
-
const container = document.getElementById('tracesContainer');
|
|
899
|
-
|
|
900
|
-
if (threads.length === 0) {
|
|
901
|
-
container.innerHTML = `
|
|
902
|
-
<div style="text-align: center; padding: 20px; color: var(--gray-500); font-size: 13px;">
|
|
903
|
-
No traces yet. Send a message to generate traces.
|
|
904
|
-
</div>
|
|
905
|
-
`;
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
container.innerHTML = threads.map(thread => `
|
|
910
|
-
<div class="trace-item" onclick="Runflow.get('TracesManager').viewThread('${thread.threadId}')">
|
|
911
|
-
<div class="trace-item-header">
|
|
912
|
-
<div class="trace-item-title">
|
|
913
|
-
${thread.entityValue || thread.threadId}
|
|
914
|
-
<span class="trace-item-badge ${thread.entityType || 'session'}">${thread.entityType || 'session'}</span>
|
|
915
|
-
</div>
|
|
916
|
-
</div>
|
|
917
|
-
<div class="trace-item-meta">
|
|
918
|
-
<div class="trace-item-stat">
|
|
919
|
-
<span>📊</span> ${thread.totalTraces} traces
|
|
920
|
-
</div>
|
|
921
|
-
<div class="trace-item-stat">
|
|
922
|
-
<span>🔄</span> ${thread.executions?.length || 1} exec
|
|
923
|
-
</div>
|
|
924
|
-
</div>
|
|
925
|
-
</div>
|
|
926
|
-
`).join('');
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
async function viewThread(threadId) {
|
|
930
|
-
try {
|
|
931
|
-
const data = await APIClient.get('/api/traces');
|
|
932
|
-
const thread = data.threads.find(t => t.threadId === threadId);
|
|
933
|
-
|
|
934
|
-
if (!thread || !thread.executions || thread.executions.length === 0) {
|
|
935
|
-
UIComponents.Toast.show('No executions found', 'warning');
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Show first execution
|
|
940
|
-
const execution = thread.executions[0];
|
|
941
|
-
await viewExecution(execution.executionId);
|
|
942
|
-
} catch (error) {
|
|
943
|
-
console.error('Failed to view thread:', error);
|
|
944
|
-
UIComponents.Toast.show('Failed to load thread details', 'error');
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
async function viewExecution(executionId) {
|
|
949
|
-
try {
|
|
950
|
-
const data = await APIClient.get(`/api/traces/executions/${executionId}`);
|
|
951
|
-
renderExecutionDetails(data);
|
|
952
|
-
} catch (error) {
|
|
953
|
-
console.error('Failed to view execution:', error);
|
|
954
|
-
UIComponents.Toast.show('Failed to load execution details', 'error');
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function renderExecutionDetails(data) {
|
|
959
|
-
const panel = document.getElementById('traceDetailsPanel');
|
|
960
|
-
const container = document.getElementById('traceDetailsContainer');
|
|
961
|
-
|
|
962
|
-
container.innerHTML = `
|
|
963
|
-
<div style="margin-bottom: 12px; padding: 12px; background: var(--gray-50); border-radius: var(--radius-md);">
|
|
964
|
-
<div style="font-weight: 600; margin-bottom: 8px; font-size: 13px;">📋 Execution Info</div>
|
|
965
|
-
<div style="font-size: 11px; color: var(--gray-600); line-height: 1.6;">
|
|
966
|
-
<div><strong>ID:</strong> <code style="font-size: 10px; background: white; padding: 2px 4px; border-radius: 2px;">${data.execution.executionId}</code></div>
|
|
967
|
-
<div><strong>Thread:</strong> ${data.execution.threadId || 'N/A'}</div>
|
|
968
|
-
<div><strong>Agent:</strong> ${data.execution.agentName || 'Unknown'}</div>
|
|
969
|
-
${data.execution.entityType ? `<div><strong>Entity:</strong> ${data.execution.entityType} - ${data.execution.entityValue || 'N/A'}</div>` : ''}
|
|
970
|
-
${data.execution.userId ? `<div><strong>User:</strong> ${data.execution.userId}</div>` : ''}
|
|
971
|
-
<div><strong>Total Traces:</strong> ${data.execution.totalTraces}</div>
|
|
972
|
-
<div><strong>Started:</strong> ${new Date(data.execution.startedAt).toLocaleTimeString()}</div>
|
|
973
|
-
</div>
|
|
974
|
-
</div>
|
|
975
|
-
<div style="margin-bottom: 8px; font-weight: 600; font-size: 13px;">🔍 Trace Hierarchy</div>
|
|
976
|
-
<div class="trace-details-tree">
|
|
977
|
-
${renderTraceTree(data.traces)}
|
|
978
|
-
</div>
|
|
979
|
-
`;
|
|
980
|
-
|
|
981
|
-
panel.style.display = 'block';
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function renderTraceTree(traces, depth = 0) {
|
|
985
|
-
if (!traces || traces.length === 0) return '<div style="color: var(--gray-500); font-size: 11px;">No traces</div>';
|
|
986
|
-
|
|
987
|
-
return traces.map(trace => {
|
|
988
|
-
const metadata = trace.metadata || {};
|
|
989
|
-
const hasDetails = trace.input || trace.output || Object.keys(metadata).length > 0;
|
|
990
|
-
|
|
991
|
-
return `
|
|
992
|
-
<div class="trace-node ${trace.status}" style="margin-left: ${depth * 8}px;">
|
|
993
|
-
<div class="trace-node-header">
|
|
994
|
-
<span style="color: var(--gray-800);">
|
|
995
|
-
${getTraceIcon(trace.type)} ${trace.type || trace.operation}
|
|
996
|
-
</span>
|
|
997
|
-
<span style="font-size: 10px; font-weight: normal; color: var(--gray-600);">
|
|
998
|
-
${trace.duration || 0}ms
|
|
999
|
-
</span>
|
|
1000
|
-
</div>
|
|
1001
|
-
<div class="trace-node-meta">
|
|
1002
|
-
<div><strong>Operation:</strong> ${trace.operation}</div>
|
|
1003
|
-
<div><strong>Status:</strong> <span style="color: ${trace.status === 'success' ? 'var(--success)' : 'var(--error)'};">${trace.status}</span></div>
|
|
1004
|
-
${trace.error ? `<div style="color: var(--error);"><strong>Error:</strong> ${trace.error}</div>` : ''}
|
|
1005
|
-
</div>
|
|
1006
|
-
|
|
1007
|
-
${hasDetails ? `
|
|
1008
|
-
<details style="margin-top: 8px; font-size: 10px;">
|
|
1009
|
-
<summary style="cursor: pointer; color: var(--primary); font-weight: 500;">
|
|
1010
|
-
📄 View Details
|
|
1011
|
-
</summary>
|
|
1012
|
-
<div style="margin-top: 8px; padding: 8px; background: white; border-radius: 4px; border: 1px solid var(--gray-200);">
|
|
1013
|
-
${trace.input ? `
|
|
1014
|
-
<div style="margin-bottom: 8px;">
|
|
1015
|
-
<strong style="color: var(--gray-700);">Input:</strong>
|
|
1016
|
-
<pre style="margin: 4px 0; padding: 6px; background: var(--gray-50); border-radius: 3px; overflow-x: auto; font-size: 10px; line-height: 1.4;">${JSON.stringify(trace.input, null, 2)}</pre>
|
|
1017
|
-
</div>
|
|
1018
|
-
` : ''}
|
|
1019
|
-
${trace.output ? `
|
|
1020
|
-
<div style="margin-bottom: 8px;">
|
|
1021
|
-
<strong style="color: var(--gray-700);">Output:</strong>
|
|
1022
|
-
<pre style="margin: 4px 0; padding: 6px; background: var(--gray-50); border-radius: 3px; overflow-x: auto; font-size: 10px; line-height: 1.4;">${JSON.stringify(trace.output, null, 2)}</pre>
|
|
1023
|
-
</div>
|
|
1024
|
-
` : ''}
|
|
1025
|
-
${Object.keys(metadata).length > 0 ? `
|
|
1026
|
-
<div>
|
|
1027
|
-
<strong style="color: var(--gray-700);">Metadata:</strong>
|
|
1028
|
-
<pre style="margin: 4px 0; padding: 6px; background: var(--gray-50); border-radius: 3px; overflow-x: auto; font-size: 10px; line-height: 1.4;">${JSON.stringify(metadata, null, 2)}</pre>
|
|
1029
|
-
</div>
|
|
1030
|
-
` : ''}
|
|
1031
|
-
</div>
|
|
1032
|
-
</details>
|
|
1033
|
-
` : ''}
|
|
1034
|
-
|
|
1035
|
-
${trace.children && trace.children.length > 0 ? `
|
|
1036
|
-
<div class="trace-children">
|
|
1037
|
-
${renderTraceTree(trace.children, depth + 1)}
|
|
1038
|
-
</div>
|
|
1039
|
-
` : ''}
|
|
1040
|
-
</div>
|
|
1041
|
-
`}).join('');
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
function getTraceIcon(type) {
|
|
1045
|
-
const icons = {
|
|
1046
|
-
'agent_execution': '🤖',
|
|
1047
|
-
'llm_call': '💬',
|
|
1048
|
-
'tool_call': '🔧',
|
|
1049
|
-
'memory_operation': '🧠',
|
|
1050
|
-
'workflow_execution': '🔄',
|
|
1051
|
-
'workflow_step': '📍',
|
|
1052
|
-
'connector_call': '🔌',
|
|
1053
|
-
'vector_search': '🔍',
|
|
1054
|
-
};
|
|
1055
|
-
return icons[type] || '📝';
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
async function clearTraces() {
|
|
1059
|
-
if (!confirm('Clear all local traces?')) return;
|
|
1060
|
-
|
|
1061
|
-
try {
|
|
1062
|
-
await APIClient.delete('/api/traces');
|
|
1063
|
-
loadTraces();
|
|
1064
|
-
document.getElementById('traceDetailsPanel').style.display = 'none';
|
|
1065
|
-
UIComponents.Toast.show('Traces cleared', 'success');
|
|
1066
|
-
} catch (error) {
|
|
1067
|
-
UIComponents.Toast.show('Failed to clear traces', 'error');
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
return {
|
|
1072
|
-
loadTraces,
|
|
1073
|
-
viewThread,
|
|
1074
|
-
viewExecution,
|
|
1075
|
-
clearTraces
|
|
1076
|
-
};
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
// ============================================================================
|
|
1080
|
-
// Main Application - Bootstrap and Initialization
|
|
1081
|
-
// ============================================================================
|
|
1082
|
-
|
|
1083
|
-
Runflow.define('App', ['EventBus', 'StateManager', 'ChatManager', 'ScenarioManager', 'ConnectionMonitor', 'UIComponents', 'TracesManager'],
|
|
1084
|
-
(EventBus, StateManager, ChatManager, ScenarioManager, ConnectionMonitor, UIComponents, TracesManager) => {
|
|
1085
|
-
|
|
1086
|
-
let initialized = false;
|
|
1087
|
-
const dom = {};
|
|
1088
|
-
|
|
1089
|
-
function init() {
|
|
1090
|
-
if (initialized) return;
|
|
1091
|
-
|
|
1092
|
-
console.log('🚀 Runflow Test Interface v2.0 - Initializing...');
|
|
1093
|
-
|
|
1094
|
-
setupDOM();
|
|
1095
|
-
setupEventHandlers();
|
|
1096
|
-
setupKeyboardShortcuts();
|
|
1097
|
-
setupTracesPanel();
|
|
1098
|
-
loadSettings();
|
|
1099
|
-
|
|
1100
|
-
// Start services
|
|
1101
|
-
ConnectionMonitor.start();
|
|
1102
|
-
ScenarioManager.loadScenarios();
|
|
1103
|
-
|
|
1104
|
-
updateUI();
|
|
1105
|
-
|
|
1106
|
-
initialized = true;
|
|
1107
|
-
|
|
1108
|
-
console.log('✅ Application ready');
|
|
1109
|
-
|
|
1110
|
-
// Development helpers
|
|
1111
|
-
if (window.location.hostname === 'localhost') {
|
|
1112
|
-
window.RunflowDebug = {
|
|
1113
|
-
state: StateManager,
|
|
1114
|
-
events: EventBus,
|
|
1115
|
-
modules: Runflow
|
|
1116
|
-
};
|
|
1117
|
-
console.log('Debug tools available at window.RunflowDebug');
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
function setupDOM() {
|
|
1122
|
-
// Cache DOM elements
|
|
1123
|
-
dom.messageForm = document.getElementById('messageForm');
|
|
1124
|
-
dom.messageInput = document.getElementById('messageInput');
|
|
1125
|
-
dom.sendButton = document.getElementById('sendButton');
|
|
1126
|
-
dom.clearBtn = document.getElementById('clearBtn');
|
|
1127
|
-
dom.refreshBtn = document.getElementById('refreshBtn');
|
|
1128
|
-
dom.exportBtn = document.getElementById('exportBtn');
|
|
1129
|
-
dom.clearAllBtn = document.getElementById('clearAllBtn');
|
|
1130
|
-
dom.payloadInput = document.getElementById('payloadInput');
|
|
1131
|
-
dom.customPayloadSection = document.getElementById('customPayloadSection');
|
|
1132
|
-
|
|
1133
|
-
// Update project info
|
|
1134
|
-
const projectName = new URLSearchParams(window.location.search).get('project') || 'Runflow Project';
|
|
1135
|
-
document.getElementById('projectName').textContent = projectName.split('/').pop();
|
|
1136
|
-
document.getElementById('backendUrl').textContent = Runflow.get('Config').api.baseURL.replace('http://', '');
|
|
1137
|
-
document.getElementById('sessionId').textContent = StateManager.get('session.id').split('-').pop();
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
function setupEventHandlers() {
|
|
1141
|
-
// Form submission
|
|
1142
|
-
dom.messageForm.addEventListener('submit', (e) => {
|
|
1143
|
-
e.preventDefault();
|
|
1144
|
-
handleMessageSubmit();
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
// Clear button
|
|
1148
|
-
dom.clearBtn.addEventListener('click', () => {
|
|
1149
|
-
dom.messageInput.value = '';
|
|
1150
|
-
dom.messageInput.focus();
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
// Control buttons
|
|
1154
|
-
dom.refreshBtn.addEventListener('click', () => ConnectionMonitor.checkHealth());
|
|
1155
|
-
dom.exportBtn.addEventListener('click', () => ChatManager.exportChat());
|
|
1156
|
-
dom.clearAllBtn.addEventListener('click', () => {
|
|
1157
|
-
if (confirm('Clear all messages?')) {
|
|
1158
|
-
ChatManager.clearAll();
|
|
1159
|
-
}
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
// Test mode toggle
|
|
1163
|
-
document.querySelectorAll('input[name="testMode"]').forEach(radio => {
|
|
1164
|
-
radio.addEventListener('change', (e) => handleTestModeChange(e.target.value));
|
|
1165
|
-
});
|
|
1166
|
-
|
|
1167
|
-
// Scenario selection
|
|
1168
|
-
document.getElementById('scenarioSelect').addEventListener('change', (e) => {
|
|
1169
|
-
ScenarioManager.loadScenario(e.target.value);
|
|
1170
|
-
});
|
|
1171
|
-
|
|
1172
|
-
// Example buttons
|
|
1173
|
-
document.querySelectorAll('.example-btn').forEach(btn => {
|
|
1174
|
-
btn.addEventListener('click', () => {
|
|
1175
|
-
const message = btn.getAttribute('data-message');
|
|
1176
|
-
dom.messageInput.value = message;
|
|
1177
|
-
dom.messageInput.focus();
|
|
1178
|
-
});
|
|
1179
|
-
});
|
|
1180
|
-
|
|
1181
|
-
// Sidebar tabs
|
|
1182
|
-
document.querySelectorAll('.sidebar-tab').forEach(tab => {
|
|
1183
|
-
tab.addEventListener('click', () => {
|
|
1184
|
-
const tabName = tab.dataset.tab;
|
|
1185
|
-
switchTab(tabName);
|
|
1186
|
-
});
|
|
1187
|
-
});
|
|
1188
|
-
|
|
1189
|
-
// State change listeners
|
|
1190
|
-
StateManager.subscribe('session.messageCount', count => {
|
|
1191
|
-
document.getElementById('messageCount').textContent = count;
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
StateManager.subscribe('session.lastResponseTime', time => {
|
|
1195
|
-
if (time) {
|
|
1196
|
-
document.getElementById('lastResponse').textContent = `${time}ms`;
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
StateManager.subscribe('ui.loading', loading => {
|
|
1201
|
-
dom.sendButton.disabled = loading;
|
|
1202
|
-
const spinner = dom.sendButton.querySelector('.spinner');
|
|
1203
|
-
const text = dom.sendButton.querySelector('span');
|
|
1204
|
-
if (spinner && text) {
|
|
1205
|
-
spinner.style.display = loading ? 'block' : 'none';
|
|
1206
|
-
text.style.display = loading ? 'none' : 'inline';
|
|
1207
|
-
}
|
|
1208
|
-
});
|
|
1209
|
-
|
|
1210
|
-
// Event bus listeners
|
|
1211
|
-
EventBus.on('message:sent', () => {
|
|
1212
|
-
dom.messageInput.value = '';
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
function setupKeyboardShortcuts() {
|
|
1217
|
-
document.addEventListener('keydown', (e) => {
|
|
1218
|
-
// Ctrl/Cmd + Enter to send
|
|
1219
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
1220
|
-
e.preventDefault();
|
|
1221
|
-
dom.messageForm.dispatchEvent(new Event('submit'));
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// Ctrl/Cmd + L to clear chat
|
|
1225
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
|
|
1226
|
-
e.preventDefault();
|
|
1227
|
-
ChatManager.clearAll();
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
// Escape to clear input
|
|
1231
|
-
if (e.key === 'Escape') {
|
|
1232
|
-
dom.messageInput.value = '';
|
|
1233
|
-
dom.messageInput.focus();
|
|
1234
|
-
}
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
function handleMessageSubmit() {
|
|
1239
|
-
const mode = StateManager.get('ui.testMode');
|
|
1240
|
-
const content = dom.messageInput.value.trim();
|
|
1241
|
-
|
|
1242
|
-
if (mode === 'simple') {
|
|
1243
|
-
if (!content) return;
|
|
1244
|
-
ChatManager.sendMessage(content);
|
|
1245
|
-
} else {
|
|
1246
|
-
const payload = dom.payloadInput.value.trim();
|
|
1247
|
-
if (!payload) {
|
|
1248
|
-
UIComponents.Toast.show('Please enter a JSON payload', 'warning');
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
try {
|
|
1253
|
-
JSON.parse(payload);
|
|
1254
|
-
ChatManager.sendMessage(content, 'custom', payload);
|
|
1255
|
-
} catch (error) {
|
|
1256
|
-
UIComponents.Toast.show('Invalid JSON payload', 'error');
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
function handleTestModeChange(mode) {
|
|
1262
|
-
StateManager.set('ui.testMode', mode);
|
|
1263
|
-
dom.customPayloadSection.style.display = mode === 'custom' ? 'block' : 'none';
|
|
1264
|
-
dom.messageInput.placeholder = mode === 'simple'
|
|
1265
|
-
? 'Type your message...'
|
|
1266
|
-
: 'Optional: Add context message for the payload...';
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
function setupTracesPanel() {
|
|
1270
|
-
// Refresh traces button
|
|
1271
|
-
const refreshTracesBtn = document.getElementById('refreshTracesBtn');
|
|
1272
|
-
if (refreshTracesBtn) {
|
|
1273
|
-
refreshTracesBtn.addEventListener('click', () => {
|
|
1274
|
-
TracesManager.loadTraces();
|
|
1275
|
-
UIComponents.Toast.show('Traces refreshed', 'success');
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// Clear traces button
|
|
1280
|
-
const clearTracesBtn = document.getElementById('clearTracesBtn');
|
|
1281
|
-
if (clearTracesBtn) {
|
|
1282
|
-
clearTracesBtn.addEventListener('click', () => {
|
|
1283
|
-
TracesManager.clearTraces();
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// Close details button
|
|
1288
|
-
const closeDetailsBtn = document.getElementById('closeDetailsBtn');
|
|
1289
|
-
if (closeDetailsBtn) {
|
|
1290
|
-
closeDetailsBtn.addEventListener('click', () => {
|
|
1291
|
-
document.getElementById('traceDetailsPanel').style.display = 'none';
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
function switchTab(tabName) {
|
|
1297
|
-
// Update tab buttons
|
|
1298
|
-
document.querySelectorAll('.sidebar-tab').forEach(tab => {
|
|
1299
|
-
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
// Update tab content
|
|
1303
|
-
document.querySelectorAll('.tab-content').forEach(content => {
|
|
1304
|
-
content.classList.toggle('active', content.dataset.tabContent === tabName);
|
|
1305
|
-
});
|
|
1306
|
-
|
|
1307
|
-
// Load traces if switching to traces tab
|
|
1308
|
-
if (tabName === 'traces') {
|
|
1309
|
-
TracesManager.loadTraces();
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
function updateUI() {
|
|
1314
|
-
document.getElementById('messageCount').textContent = StateManager.get('session.messageCount');
|
|
1315
|
-
dom.messageInput.focus();
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
function loadSettings() {
|
|
1319
|
-
try {
|
|
1320
|
-
const settings = localStorage.getItem('runflow-display-settings');
|
|
1321
|
-
if (settings) {
|
|
1322
|
-
const parsed = JSON.parse(settings);
|
|
1323
|
-
if (document.getElementById('showJsonToggle')) {
|
|
1324
|
-
document.getElementById('showJsonToggle').checked = parsed.showJsonToggle !== false;
|
|
1325
|
-
}
|
|
1326
|
-
if (document.getElementById('autoExpandJson')) {
|
|
1327
|
-
document.getElementById('autoExpandJson').checked = parsed.autoExpandJson === true;
|
|
1328
|
-
}
|
|
1329
|
-
if (document.getElementById('showMetadata')) {
|
|
1330
|
-
document.getElementById('showMetadata').checked = parsed.showMetadata !== false;
|
|
1331
|
-
}
|
|
1332
|
-
if (document.getElementById('responsePath') && parsed.responsePath) {
|
|
1333
|
-
document.getElementById('responsePath').value = parsed.responsePath;
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
} catch (error) {
|
|
1337
|
-
console.warn('Failed to load settings:', error);
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
// Setup settings change handlers
|
|
1341
|
-
['showJsonToggle', 'autoExpandJson', 'showMetadata'].forEach(id => {
|
|
1342
|
-
const element = document.getElementById(id);
|
|
1343
|
-
if (element) {
|
|
1344
|
-
element.addEventListener('change', saveSettings);
|
|
1345
|
-
}
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
// Setup response path change handler
|
|
1349
|
-
const responsePathElement = document.getElementById('responsePath');
|
|
1350
|
-
if (responsePathElement) {
|
|
1351
|
-
responsePathElement.addEventListener('input', saveSettings);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
function saveSettings() {
|
|
1356
|
-
try {
|
|
1357
|
-
const settings = {
|
|
1358
|
-
showJsonToggle: document.getElementById('showJsonToggle')?.checked,
|
|
1359
|
-
autoExpandJson: document.getElementById('autoExpandJson')?.checked,
|
|
1360
|
-
showMetadata: document.getElementById('showMetadata')?.checked,
|
|
1361
|
-
responsePath: document.getElementById('responsePath')?.value || 'data.message'
|
|
1362
|
-
};
|
|
1363
|
-
localStorage.setItem('runflow-display-settings', JSON.stringify(settings));
|
|
1364
|
-
} catch (error) {
|
|
1365
|
-
console.warn('Failed to save settings:', error);
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
return {
|
|
1370
|
-
init
|
|
1371
|
-
};
|
|
1372
|
-
});
|
|
1373
|
-
|
|
1374
|
-
// ============================================================================
|
|
1375
|
-
// Application Entry Point
|
|
1376
|
-
// ============================================================================
|
|
1377
|
-
|
|
1378
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
1379
|
-
const App = Runflow.get('App');
|
|
1380
|
-
App.init();
|
|
1381
|
-
});
|