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 +4 -3
- package/README.md +3 -3
- package/dist/cli.js +1 -1
- package/dist/swagger-spec.js +1 -1
- package/package.json +2 -1
- package/public/dashboard.html +512 -0
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
package/dist/swagger-spec.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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>
|