@runflow-ai/cli 0.2.11 → 0.2.12

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/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
- });