xcomponent-ai 0.2.0 → 0.2.2

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/QUICKSTART.md CHANGED
@@ -49,7 +49,8 @@ xcomponent-ai serve examples/trading.yaml
49
49
  - Settlement (3 states, 3 transitions)
50
50
 
51
51
  🌐 API Server: http://localhost:3000
52
- 📊 Dashboard: http://localhost:3000/dashboard
52
+ 📊 Dashboard: http://localhost:3000/dashboard.html
53
+ 📚 API Docs: http://localhost:3000/api-docs
53
54
  📡 WebSocket: ws://localhost:3000
54
55
 
55
56
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -58,7 +59,7 @@ Press Ctrl+C to stop
58
59
 
59
60
  ### 3. Visualize in the Dashboard
60
61
 
61
- Open your browser to **http://localhost:3000/dashboard**
62
+ Open your browser to **http://localhost:3000/dashboard.html**
62
63
 
63
64
  You'll see:
64
65
  - 📊 **All active instances** (real-time table)
@@ -126,7 +127,7 @@ Context: { orderId: "ORD-001", amount: 1000, symbol: "AAPL" }
126
127
 
127
128
  **Option C: Via Web Dashboard**
128
129
 
129
- 1. Open http://localhost:3000/dashboard
130
+ 1. Open http://localhost:3000/dashboard.html
130
131
  2. Click **"Create Instance"** button
131
132
  3. Select machine: `OrderEntry`
132
133
  4. Enter context: `{ "orderId": "ORD-001", "amount": 1000 }`
package/README.md CHANGED
@@ -28,7 +28,7 @@ npm install -g xcomponent-ai
28
28
  # Start runtime with dashboard
29
29
  xcomponent-ai serve examples/trading.yaml
30
30
 
31
- # Open browser → http://localhost:3000/dashboard
31
+ # Open browser → http://localhost:3000/dashboard.html
32
32
  # Create instances, send events, visualize FSM in real-time!
33
33
  ```
34
34
 
@@ -177,7 +177,7 @@ console.log(result.data.yaml);
177
177
  # Start API server
178
178
  npm run api
179
179
 
180
- # Visit dashboard: http://localhost:3000/dashboard
180
+ # Visit dashboard: http://localhost:3000/dashboard.html
181
181
  # WebSocket: ws://localhost:3000
182
182
  ```
183
183
 
@@ -368,7 +368,7 @@ socket.on('instance_error', (error) => { ... });
368
368
 
369
369
  ### Dashboard
370
370
 
371
- Visit `http://localhost:3000/dashboard` for:
371
+ Visit `http://localhost:3000/dashboard.html` for:
372
372
  - Active instances table
373
373
  - Real-time event stream
374
374
  - State visualizations
package/dist/cli.js CHANGED
@@ -61,7 +61,7 @@ const program = new commander_1.Command();
61
61
  program
62
62
  .name('xcomponent-ai')
63
63
  .description('Agentic FSM tool for fintech workflows')
64
- .version('0.2.0');
64
+ .version('0.2.2');
65
65
  /**
66
66
  * Initialize new project
67
67
  */
@@ -9,7 +9,7 @@ function generateSwaggerSpec(component, port) {
9
9
  openapi: '3.0.0',
10
10
  info: {
11
11
  title: 'xcomponent-ai REST API',
12
- version: '0.2.0',
12
+ version: '0.2.2',
13
13
  description: `REST API for ${component.name} FSM runtime.
14
14
 
15
15
  Manage state machine instances, send events, and monitor execution in real-time.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xcomponent-ai",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "LLM-first framework for AI agents (Claude, GPT) to build apps with sanctuarized business logic. Event-driven FSM runtime with multi-instance state machines, cross-component communication, event sourcing, and production-ready persistence (PostgreSQL, MongoDB)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "files": [
14
14
  "dist/",
15
+ "public/",
15
16
  "examples/",
16
17
  "README.md",
17
18
  "QUICKSTART.md",
@@ -0,0 +1,512 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>xcomponent-ai Dashboard</title>
7
+ <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
9
+ <style>
10
+ /* Reset & Base */
11
+ * { margin: 0; padding: 0; box-sizing: border-box; }
12
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f6f8fa; color: #24292e; }
13
+ .container { max-width: 1600px; margin: 0 auto; padding: 20px; }
14
+
15
+ /* Header */
16
+ .header { background: #24292e; color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
17
+ .header h1 { font-size: 24px; }
18
+ .header .status { display: flex; align-items: center; gap: 15px; }
19
+ .status-dot { width: 10px; height: 10px; border-radius: 50%; }
20
+ .status-dot.connected { background: #28a745; box-shadow: 0 0 8px #28a745; }
21
+ .status-dot.disconnected { background: #d73a49; }
22
+ .header-links { display: flex; gap: 10px; }
23
+ .header-links a { padding: 8px 16px; background: rgba(255,255,255,0.1); border-radius: 4px; color: white; text-decoration: none; font-size: 14px; }
24
+ .header-links a:hover { background: rgba(255,255,255,0.2); }
25
+
26
+ /* Stats Cards */
27
+ .stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 15px; margin-bottom: 20px; }
28
+ .stat-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
29
+ .stat-value { font-size: 32px; font-weight: 700; color: #0366d6; }
30
+ .stat-label { font-size: 12px; color: #586069; margin-top: 5px; text-transform: uppercase; letter-spacing: 0.5px; }
31
+
32
+ /* Tabs */
33
+ .tabs { display: flex; background: white; border-radius: 8px 8px 0 0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; }
34
+ .tab { flex: 1; padding: 15px; text-align: center; cursor: pointer; color: #586069; font-weight: 600; border-bottom: 3px solid transparent; transition: all 0.3s; }
35
+ .tab:hover { background: #f6f8fa; }
36
+ .tab.active { color: #0366d6; border-bottom-color: #0366d6; background: #f6f8fa; }
37
+
38
+ /* Tab Content */
39
+ .tab-content { display: none; background: white; padding: 20px; border-radius: 0 0 8px 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); min-height: 600px; }
40
+ .tab-content.active { display: block; }
41
+
42
+ /* Grid Layouts */
43
+ .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
44
+ .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
45
+
46
+ /* Card */
47
+ .card { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 20px; }
48
+ .card h2 { font-size: 16px; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #0366d6; }
49
+
50
+ /* Form */
51
+ .form-group { margin-bottom: 15px; }
52
+ .form-group label { display: block; font-weight: 600; margin-bottom: 5px; font-size: 14px; }
53
+ .form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px; border: 1px solid #d1d5da; border-radius: 4px; font-size: 14px; }
54
+ .form-group input:focus, .form-group select:focus { outline: none; border-color: #0366d6; box-shadow: 0 0 0 3px rgba(3,102,214,0.1); }
55
+ .form-help { font-size: 12px; color: #586069; margin-top: 4px; }
56
+
57
+ /* Button */
58
+ .btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; font-size: 14px; transition: all 0.2s; }
59
+ .btn-primary { background: #0366d6; color: white; }
60
+ .btn-primary:hover { background: #0256c7; }
61
+ .btn-success { background: #28a745; color: white; }
62
+ .btn-success:hover { background: #22863a; }
63
+ .btn-danger { background: #d73a49; color: white; }
64
+ .btn-danger:hover { background: #cb2431; }
65
+ .btn-sm { padding: 6px 12px; font-size: 12px; }
66
+
67
+ /* Instance List */
68
+ .instance-list { max-height: 500px; overflow-y: auto; }
69
+ .instance-item { background: white; border: 1px solid #e1e4e8; padding: 15px; margin-bottom: 10px; border-radius: 6px; cursor: pointer; transition: all 0.2s; }
70
+ .instance-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); transform: translateY(-1px); }
71
+ .instance-item.selected { border-color: #0366d6; background: #f1f8ff; }
72
+ .instance-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
73
+ .instance-id { font-weight: 700; color: #0366d6; font-family: monospace; font-size: 13px; }
74
+ .badge { padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: 700; text-transform: uppercase; }
75
+ .badge.active { background: #dbedff; color: #0366d6; }
76
+ .badge.final { background: #dcffe4; color: #28a745; }
77
+ .badge.error { background: #ffdce0; color: #d73a49; }
78
+ .instance-meta { font-size: 13px; color: #586069; line-height: 1.6; }
79
+
80
+ /* Blotter */
81
+ .blotter { background: #24292e; border-radius: 6px; padding: 15px; max-height: 600px; overflow-y: auto; }
82
+ .blotter-item { border-left: 3px solid #0366d6; background: #2d333b; padding: 10px 12px; margin-bottom: 8px; font-family: monospace; font-size: 12px; border-radius: 0 4px 4px 0; }
83
+ .blotter-item.created { border-color: #28a745; }
84
+ .blotter-item.error { border-color: #d73a49; }
85
+ .blotter-item.cross-component { border-color: #f66a0a; }
86
+ .blotter-time { color: #8b949e; font-size: 11px; }
87
+ .blotter-content { color: #c9d1d9; margin-top: 4px; }
88
+ .blotter-source { color: #f66a0a; font-size: 11px; margin-top: 4px; }
89
+
90
+ /* Mermaid */
91
+ .mermaid-container { background: white; border: 1px solid #e1e4e8; border-radius: 6px; padding: 20px; overflow-x: auto; }
92
+
93
+ /* Sequence */
94
+ .sequence-container { max-height: 600px; overflow-y: auto; }
95
+ .sequence-item { display: flex; align-items: center; padding: 12px; margin-bottom: 8px; border-left: 3px solid #0366d6; background: #f6f8fa; border-radius: 0 4px 4px 0; }
96
+ .sequence-time { min-width: 80px; font-size: 11px; color: #586069; font-family: monospace; }
97
+ .sequence-arrow { margin: 0 15px; color: #0366d6; }
98
+ .sequence-content { flex: 1; }
99
+ .sequence-event { font-weight: 600; font-size: 14px; }
100
+ .sequence-detail { font-size: 12px; color: #586069; margin-top: 4px; }
101
+
102
+ /* Empty State */
103
+ .empty-state { text-align: center; padding: 60px 20px; color: #959da5; }
104
+ .empty-state-icon { font-size: 48px; margin-bottom: 15px; }
105
+
106
+ /* Filter */
107
+ .filter-bar { display: flex; gap: 10px; margin-bottom: 20px; padding: 15px; background: #f6f8fa; border-radius: 6px; }
108
+ .filter-bar input, .filter-bar select { flex: 1; }
109
+
110
+ /* Component Selector */
111
+ .component-selector { margin-bottom: 20px; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; }
112
+ .component-selector label { display: block; font-weight: 600; margin-bottom: 8px; }
113
+ .component-selector select { width: 100%; padding: 10px; border-radius: 4px; border: 1px solid #d1d5da; }
114
+ </style>
115
+ </head>
116
+ <body>
117
+ <div class="container">
118
+ <div class="header">
119
+ <div>
120
+ <h1>📊 xcomponent-ai Dashboard</h1>
121
+ <div class="status">
122
+ <span class="status-dot" id="ws-status"></span>
123
+ <span id="component-name">Loading...</span>
124
+ </div>
125
+ </div>
126
+ <div class="header-links">
127
+ <a href="/api-docs" target="_blank">📖 API Docs</a>
128
+ <a href="#" onclick="exportState()">💾 Export</a>
129
+ </div>
130
+ </div>
131
+
132
+ <div class="stats-grid">
133
+ <div class="stat-card"><div class="stat-value" id="stat-total">0</div><div class="stat-label">Total Instances</div></div>
134
+ <div class="stat-card"><div class="stat-value" id="stat-active">0</div><div class="stat-label">Active</div></div>
135
+ <div class="stat-card"><div class="stat-value" id="stat-final">0</div><div class="stat-label">Final</div></div>
136
+ <div class="stat-card"><div class="stat-value" id="stat-error">0</div><div class="stat-label">Error</div></div>
137
+ <div class="stat-card"><div class="stat-value" id="stat-events">0</div><div class="stat-label">Events</div></div>
138
+ </div>
139
+
140
+ <div class="component-selector">
141
+ <label for="component-select">Component:</label>
142
+ <select id="component-select" onchange="selectComponent()">
143
+ <option value="">Select a component...</option>
144
+ </select>
145
+ </div>
146
+
147
+ <div class="tabs">
148
+ <div class="tab active" onclick="switchTab('overview')">Overview</div>
149
+ <div class="tab" onclick="switchTab('diagram')">FSM Diagram</div>
150
+ <div class="tab" onclick="switchTab('blotter')">Event Blotter</div>
151
+ <div class="tab" onclick="switchTab('traceability')">Traceability</div>
152
+ <div class="tab" onclick="switchTab('create')">Create Instance</div>
153
+ </div>
154
+
155
+ <div class="tab-content active" id="tab-overview">
156
+ <div class="filter-bar">
157
+ <input type="text" id="filter-instance" placeholder="Filter by instance ID..." oninput="filterInstances()">
158
+ <select id="filter-machine" onchange="filterInstances()">
159
+ <option value="">All Machines</option>
160
+ </select>
161
+ <select id="filter-state" onchange="filterInstances()">
162
+ <option value="">All States</option>
163
+ </select>
164
+ </div>
165
+ <div class="instance-list" id="instance-list">
166
+ <div class="empty-state">
167
+ <div class="empty-state-icon">📭</div>
168
+ <div>No instances yet. Create one in the "Create Instance" tab.</div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ <div class="tab-content" id="tab-diagram">
174
+ <div class="form-group">
175
+ <label for="diagram-machine">State Machine:</label>
176
+ <select id="diagram-machine" onchange="renderDiagram()">
177
+ <option value="">Select a state machine...</option>
178
+ </select>
179
+ </div>
180
+ <div class="mermaid-container" id="diagram-container">
181
+ <div class="empty-state">
182
+ <div class="empty-state-icon">🎨</div>
183
+ <div>Select a state machine to view its diagram</div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <div class="tab-content" id="tab-blotter">
189
+ <div class="filter-bar">
190
+ <input type="text" id="blotter-filter" placeholder="Filter events..." oninput="filterBlotter()">
191
+ <select id="blotter-type" onchange="filterBlotter()">
192
+ <option value="">All Events</option>
193
+ <option value="state-change">State Changes</option>
194
+ <option value="created">Created</option>
195
+ <option value="error">Errors</option>
196
+ <option value="cross-component">Cross-Component</option>
197
+ </select>
198
+ <button class="btn btn-sm btn-danger" onclick="clearBlotter()">Clear</button>
199
+ </div>
200
+ <div class="blotter" id="event-blotter">
201
+ <div style="color: #8b949e; text-align: center; padding: 40px;">Waiting for events...</div>
202
+ </div>
203
+ </div>
204
+
205
+ <div class="tab-content" id="tab-traceability">
206
+ <div class="form-group">
207
+ <label for="trace-instance">Instance:</label>
208
+ <select id="trace-instance" onchange="loadTrace()">
209
+ <option value="">Select an instance...</option>
210
+ </select>
211
+ </div>
212
+ <div class="sequence-container" id="trace-container">
213
+ <div class="empty-state">
214
+ <div class="empty-state-icon">🔍</div>
215
+ <div>Select an instance to view its history</div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <div class="tab-content" id="tab-create">
221
+ <div class="card">
222
+ <h2>Create New Instance</h2>
223
+ <div class="form-group">
224
+ <label for="create-machine">State Machine *</label>
225
+ <select id="create-machine" onchange="updateCreateForm()">
226
+ <option value="">Select a state machine...</option>
227
+ </select>
228
+ </div>
229
+ <div id="create-form-fields"></div>
230
+ <button class="btn btn-primary" onclick="createInstance()">Create Instance</button>
231
+ </div>
232
+ </div>
233
+ </div>
234
+
235
+ <script>
236
+ const socket = io();
237
+ let componentData = null;
238
+ let instances = [];
239
+ let events = [];
240
+ let selectedInstance = null;
241
+
242
+ // WebSocket connection
243
+ socket.on('connect', () => {
244
+ document.getElementById('ws-status').className = 'status-dot connected';
245
+ });
246
+
247
+ socket.on('disconnect', () => {
248
+ document.getElementById('ws-status').className = 'status-dot disconnected';
249
+ });
250
+
251
+ socket.on('component_data', (data) => {
252
+ componentData = data.component;
253
+ document.getElementById('component-name').textContent = componentData.name;
254
+ populateSelectors();
255
+ });
256
+
257
+ socket.on('instance_created', (data) => {
258
+ addEvent({type: 'created', timestamp: Date.now(), message: `Instance ${data.instanceId} created (${data.machineName})`, instanceId: data.instanceId});
259
+ loadInstances();
260
+ });
261
+
262
+ socket.on('state_change', (data) => {
263
+ addEvent({type: 'state-change', timestamp: Date.now(), message: `${data.instanceId}: ${data.previousState} → ${data.newState} (${data.event.type})`, instanceId: data.instanceId, source: data.componentName});
264
+ loadInstances();
265
+ if (selectedInstance === data.instanceId) loadTrace();
266
+ });
267
+
268
+ socket.on('instance_error', (data) => {
269
+ addEvent({type: 'error', timestamp: Date.now(), message: `ERROR in ${data.instanceId}: ${data.error}`, instanceId: data.instanceId});
270
+ loadInstances();
271
+ });
272
+
273
+ // Tab Switching
274
+ function switchTab(tabName) {
275
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
276
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
277
+ event.target.classList.add('active');
278
+ document.getElementById('tab-' + tabName).classList.add('active');
279
+ }
280
+
281
+ // Load Instances
282
+ async function loadInstances() {
283
+ const res = await fetch('/api/instances');
284
+ const data = await res.json();
285
+ instances = data.instances || [];
286
+ updateStats();
287
+ renderInstances();
288
+ updateTraceSelector();
289
+ }
290
+
291
+ function updateStats() {
292
+ document.getElementById('stat-total').textContent = instances.length;
293
+ document.getElementById('stat-active').textContent = instances.filter(i => i.status === 'active').length;
294
+ document.getElementById('stat-final').textContent = instances.filter(i => i.status === 'final').length;
295
+ document.getElementById('stat-error').textContent = instances.filter(i => i.status === 'error').length;
296
+ document.getElementById('stat-events').textContent = events.length;
297
+ }
298
+
299
+ function renderInstances() {
300
+ const container = document.getElementById('instance-list');
301
+ if (instances.length === 0) {
302
+ container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📭</div><div>No instances yet. Create one in the "Create Instance" tab.</div></div>';
303
+ return;
304
+ }
305
+
306
+ container.innerHTML = instances.map(inst => `
307
+ <div class="instance-item ${selectedInstance === inst.id ? 'selected' : ''}" onclick="selectInstance('${inst.id}')">
308
+ <div class="instance-header">
309
+ <span class="instance-id">${inst.id.substring(0, 8)}</span>
310
+ <span class="badge ${inst.status}">${inst.currentState}</span>
311
+ </div>
312
+ <div class="instance-meta">
313
+ Machine: ${inst.machineName}<br>
314
+ Status: ${inst.status}
315
+ </div>
316
+ </div>
317
+ `).join('');
318
+ }
319
+
320
+ function selectInstance(id) {
321
+ selectedInstance = id;
322
+ renderInstances();
323
+ loadTrace();
324
+ }
325
+
326
+ // Filter Instances
327
+ function filterInstances() {
328
+ const idFilter = document.getElementById('filter-instance').value.toLowerCase();
329
+ const machineFilter = document.getElementById('filter-machine').value;
330
+ const stateFilter = document.getElementById('filter-state').value;
331
+
332
+ document.querySelectorAll('.instance-item').forEach(item => {
333
+ const id = item.querySelector('.instance-id').textContent.toLowerCase();
334
+ const machine = item.querySelector('.instance-meta').textContent.includes(machineFilter);
335
+ const state = item.querySelector('.badge').textContent.includes(stateFilter);
336
+
337
+ const show = id.includes(idFilter) && (!machineFilter || machine) && (!stateFilter || state);
338
+ item.style.display = show ? '' : 'none';
339
+ });
340
+ }
341
+
342
+ // Event Blotter
343
+ function addEvent(event) {
344
+ events.unshift(event);
345
+ if (events.length > 200) events = events.slice(0, 200);
346
+ renderBlotter();
347
+ updateStats();
348
+ }
349
+
350
+ function renderBlotter() {
351
+ const container = document.getElementById('event-blotter');
352
+ if (events.length === 0) {
353
+ container.innerHTML = '<div style="color: #8b949e; text-align: center; padding: 40px;">Waiting for events...</div>';
354
+ return;
355
+ }
356
+
357
+ container.innerHTML = events.map(evt => `
358
+ <div class="blotter-item ${evt.type}">
359
+ <div class="blotter-time">${new Date(evt.timestamp).toLocaleTimeString()}</div>
360
+ <div class="blotter-content">${evt.message}</div>
361
+ ${evt.source ? `<div class="blotter-source">Source: ${evt.source}</div>` : ''}
362
+ </div>
363
+ `).join('');
364
+ }
365
+
366
+ function filterBlotter() {
367
+ const filter = document.getElementById('blotter-filter').value.toLowerCase();
368
+ const typeFilter = document.getElementById('blotter-type').value;
369
+
370
+ document.querySelectorAll('.blotter-item').forEach(item => {
371
+ const text = item.textContent.toLowerCase();
372
+ const type = typeFilter ? item.classList.contains(typeFilter) : true;
373
+ item.style.display = (text.includes(filter) && type) ? '' : 'none';
374
+ });
375
+ }
376
+
377
+ function clearBlotter() {
378
+ events = [];
379
+ renderBlotter();
380
+ updateStats();
381
+ }
382
+
383
+ // Diagram
384
+ function renderDiagram() {
385
+ const machineName = document.getElementById('diagram-machine').value;
386
+ if (!machineName || !componentData) return;
387
+
388
+ const machine = componentData.stateMachines.find(m => m.name === machineName);
389
+ if (!machine) return;
390
+
391
+ // Generate Mermaid diagram
392
+ let mermaid = 'stateDiagram-v2\\n';
393
+ mermaid += ` [*] --> ${machine.initialState}\\n`;
394
+
395
+ machine.transitions.forEach(t => {
396
+ mermaid += ` ${t.from} --> ${t.to}: ${t.event}\\n`;
397
+ });
398
+
399
+ machine.states.filter(s => s.type === 'final' || s.type === 'error').forEach(s => {
400
+ mermaid += ` ${s.name} --> [*]\\n`;
401
+ });
402
+
403
+ document.getElementById('diagram-container').innerHTML = `<div class="mermaid">${mermaid}</div>`;
404
+ mermaid.init();
405
+ }
406
+
407
+ // Traceability
408
+ async function loadTrace() {
409
+ if (!selectedInstance) return;
410
+
411
+ const container = document.getElementById('trace-container');
412
+ // In real version, fetch from /api/instances/:id/history
413
+ container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔍</div><div>Traceability history would appear here</div></div>';
414
+ }
415
+
416
+ function updateTraceSelector() {
417
+ const select = document.getElementById('trace-instance');
418
+ select.innerHTML = '<option value="">Select an instance...</option>' +
419
+ instances.map(i => `<option value="${i.id}">${i.id.substring(0, 8)} - ${i.machineName}</option>`).join('');
420
+ }
421
+
422
+ // Create Instance
423
+ function updateCreateForm() {
424
+ const machineName = document.getElementById('create-machine').value;
425
+ if (!machineName || !componentData) return;
426
+
427
+ const machine = componentData.stateMachines.find(m => m.name === machineName);
428
+ if (!machine || !machine.contextSchema) {
429
+ document.getElementById('create-form-fields').innerHTML = '<div class="form-group"><label>Context (JSON)</label><textarea id="context-json" rows="4"></textarea></div>';
430
+ return;
431
+ }
432
+
433
+ // Generate form from schema
434
+ let html = '';
435
+ for (const [key, field] of Object.entries(machine.contextSchema)) {
436
+ html += `<div class="form-group">`;
437
+ html += `<label for="ctx_${key}">${field.label || key}${field.required ? ' *' : ''}</label>`;
438
+ if (field.type === 'select') {
439
+ html += `<select id="ctx_${key}">`;
440
+ field.options.forEach(opt => html += `<option value="${opt.value}">${opt.label}</option>`);
441
+ html += `</select>`;
442
+ } else {
443
+ html += `<input type="${field.type || 'text'}" id="ctx_${key}" placeholder="${field.placeholder || ''}" ${field.required ? 'required' : ''}>`;
444
+ }
445
+ if (field.description) html += `<div class="form-help">${field.description}</div>`;
446
+ html += `</div>`;
447
+ }
448
+ document.getElementById('create-form-fields').innerHTML = html;
449
+ }
450
+
451
+ async function createInstance() {
452
+ const machineName = document.getElementById('create-machine').value;
453
+ if (!machineName) return alert('Please select a state machine');
454
+
455
+ const machine = componentData?.stateMachines.find(m => m.name === machineName);
456
+ let context = {};
457
+
458
+ if (machine?.contextSchema) {
459
+ for (const key of Object.keys(machine.contextSchema)) {
460
+ const input = document.getElementById('ctx_' + key);
461
+ if (input && input.value) {
462
+ context[key] = input.type === 'number' ? parseFloat(input.value) : input.value;
463
+ }
464
+ }
465
+ } else {
466
+ const json = document.getElementById('context-json')?.value;
467
+ if (json) context = JSON.parse(json);
468
+ }
469
+
470
+ await fetch('/api/instances', {
471
+ method: 'POST',
472
+ headers: {'Content-Type': 'application/json'},
473
+ body: JSON.stringify({machineName, context})
474
+ });
475
+
476
+ // Clear form
477
+ if (machine?.contextSchema) {
478
+ for (const key of Object.keys(machine.contextSchema)) {
479
+ const input = document.getElementById('ctx_' + key);
480
+ if (input) input.value = '';
481
+ }
482
+ }
483
+
484
+ switchTab('overview');
485
+ loadInstances();
486
+ }
487
+
488
+ function populateSelectors() {
489
+ if (!componentData || !componentData.stateMachines) return;
490
+
491
+ const machines = componentData.stateMachines.map(m => `<option value="${m.name}">${m.name}</option>`).join('');
492
+ document.getElementById('filter-machine').innerHTML = '<option value="">All Machines</option>' + machines;
493
+ document.getElementById('diagram-machine').innerHTML = '<option value="">Select...</option>' + machines;
494
+ document.getElementById('create-machine').innerHTML = '<option value="">Select...</option>' + machines;
495
+ }
496
+
497
+ function exportState() {
498
+ const state = {componentData, instances, events, timestamp: Date.now()};
499
+ const blob = new Blob([JSON.stringify(state, null, 2)], {type: 'application/json'});
500
+ const url = URL.createObjectURL(blob);
501
+ const a = document.createElement('a');
502
+ a.href = url;
503
+ a.download = `xcomponent-state-${Date.now()}.json`;
504
+ a.click();
505
+ }
506
+
507
+ // Initialize
508
+ loadInstances();
509
+ mermaid.initialize({startOnLoad: true, theme: 'default'});
510
+ </script>
511
+ </body>
512
+ </html>