xcomponent-ai 0.3.2 → 0.4.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.
@@ -10,1113 +10,1802 @@
10
10
  /* Reset & Base */
11
11
  * { margin: 0; padding: 0; box-sizing: border-box; }
12
12
 
13
- /* Dark Background */
13
+ :root {
14
+ --bg-primary: #0f0f0f;
15
+ --bg-secondary: #1a1a1a;
16
+ --bg-tertiary: #252525;
17
+ --glass-bg: rgba(255, 255, 255, 0.08);
18
+ --glass-border: rgba(255, 255, 255, 0.15);
19
+ --text-primary: #f0f0f0;
20
+ --text-secondary: rgba(255, 255, 255, 0.7);
21
+ --text-muted: rgba(255, 255, 255, 0.5);
22
+ --accent: #fbbf24;
23
+ --accent-hover: #f59e0b;
24
+ --success: #10b981;
25
+ --error: #ef4444;
26
+ --info: #3b82f6;
27
+ --inter-machine: #10b981;
28
+ --sidebar-width: 300px;
29
+ --header-height: 60px;
30
+ --history-height: 260px;
31
+ }
32
+
14
33
  body {
15
34
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
16
- background: linear-gradient(-45deg, #0a0a0a, #1a1a1a, #2d2d2d, #1f1f1f);
17
- background-size: 400% 400%;
18
- animation: gradientShift 15s ease infinite;
35
+ background: var(--bg-primary);
19
36
  min-height: 100vh;
20
- color: #e0e0e0;
37
+ color: var(--text-primary);
38
+ overflow: hidden;
21
39
  }
22
40
 
23
- @keyframes gradientShift {
24
- 0% { background-position: 0% 50%; }
25
- 50% { background-position: 100% 50%; }
26
- 100% { background-position: 0% 50%; }
41
+ /* Main Layout */
42
+ .app-layout {
43
+ display: grid;
44
+ grid-template-columns: var(--sidebar-width) 1fr;
45
+ grid-template-rows: var(--header-height) 1fr var(--history-height);
46
+ height: 100vh;
47
+ gap: 1px;
48
+ background: var(--glass-border);
27
49
  }
28
50
 
29
- .container {
30
- max-width: 1600px;
31
- margin: 0 auto;
32
- padding: 30px 20px;
33
- }
34
-
35
- /* Glass Morphism Header */
51
+ /* Header */
36
52
  .header {
37
- background: rgba(255, 255, 255, 0.15);
38
- backdrop-filter: blur(20px);
39
- -webkit-backdrop-filter: blur(20px);
40
- border: 1px solid rgba(255, 255, 255, 0.3);
41
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
42
- color: white;
43
- padding: 30px;
44
- border-radius: 20px;
45
- margin-bottom: 30px;
53
+ grid-column: 1 / -1;
54
+ background: var(--bg-secondary);
46
55
  display: flex;
47
- justify-content: space-between;
48
56
  align-items: center;
49
- animation: slideDown 0.5s ease-out;
57
+ justify-content: space-between;
58
+ padding: 0 24px;
59
+ border-bottom: 1px solid var(--glass-border);
50
60
  }
51
61
 
52
- @keyframes slideDown {
53
- from { transform: translateY(-30px); opacity: 0; }
54
- to { transform: translateY(0); opacity: 1; }
62
+ .header-left {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 16px;
55
66
  }
56
67
 
57
- .header h1 {
58
- font-size: 32px;
68
+ .logo {
69
+ font-size: 20px;
59
70
  font-weight: 700;
60
- background: linear-gradient(135deg, #fff 0%, #f0f0f0 100%);
61
- -webkit-background-clip: text;
62
- -webkit-text-fill-color: transparent;
63
- background-clip: text;
64
- letter-spacing: -0.5px;
71
+ color: var(--text-primary);
65
72
  }
66
73
 
67
- .header .status { display: flex; align-items: center; gap: 20px; }
68
-
69
- .status-dot {
70
- width: 12px;
71
- height: 12px;
72
- border-radius: 50%;
73
- position: relative;
74
+ .logo span {
75
+ color: var(--accent);
74
76
  }
75
77
 
76
- .status-dot.connected {
77
- background: #10b981;
78
- box-shadow: 0 0 20px #10b981, 0 0 40px rgba(16, 185, 129, 0.5);
79
- animation: pulse 2s infinite;
78
+ .connection-status {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 8px;
82
+ font-size: 13px;
83
+ color: var(--text-secondary);
80
84
  }
81
85
 
82
- @keyframes pulse {
83
- 0%, 100% { transform: scale(1); opacity: 1; }
84
- 50% { transform: scale(1.1); opacity: 0.8; }
86
+ .status-dot {
87
+ width: 8px;
88
+ height: 8px;
89
+ border-radius: 50%;
90
+ background: var(--error);
85
91
  }
86
92
 
87
- .status-dot.disconnected {
88
- background: #ef4444;
89
- box-shadow: 0 0 10px #ef4444;
93
+ .status-dot.connected {
94
+ background: var(--success);
95
+ box-shadow: 0 0 10px var(--success);
90
96
  }
91
97
 
92
- .header-links { display: flex; gap: 12px; }
93
-
94
- .header-links a {
95
- padding: 10px 20px;
96
- background: rgba(255, 255, 255, 0.2);
97
- border: 1px solid rgba(255, 255, 255, 0.3);
98
- border-radius: 10px;
99
- color: white;
100
- text-decoration: none;
101
- font-size: 14px;
102
- font-weight: 600;
103
- transition: all 0.3s ease;
98
+ .header-center {
99
+ display: flex;
100
+ gap: 8px;
104
101
  }
105
102
 
106
- .header-links a:hover {
107
- background: rgba(255, 255, 255, 0.3);
108
- transform: translateY(-2px);
109
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
110
- }
111
-
112
- /* Stats Cards with Gradients */
113
- .stats-grid {
114
- display: grid;
115
- grid-template-columns: repeat(5, 1fr);
116
- gap: 20px;
117
- margin-bottom: 30px;
118
- animation: fadeIn 0.6s ease-out 0.2s both;
103
+ .view-toggle {
104
+ display: flex;
105
+ background: var(--bg-tertiary);
106
+ border-radius: 8px;
107
+ padding: 4px;
119
108
  }
120
109
 
121
- @keyframes fadeIn {
122
- from { opacity: 0; transform: translateY(20px); }
123
- to { opacity: 1; transform: translateY(0); }
110
+ .view-toggle-btn {
111
+ padding: 8px 16px;
112
+ border: none;
113
+ background: transparent;
114
+ color: var(--text-secondary);
115
+ font-size: 13px;
116
+ font-weight: 500;
117
+ cursor: pointer;
118
+ border-radius: 6px;
119
+ transition: all 0.2s;
124
120
  }
125
121
 
126
- .stat-card {
127
- background: rgba(255, 255, 255, 0.2);
128
- backdrop-filter: blur(20px);
129
- -webkit-backdrop-filter: blur(20px);
130
- border: 1px solid rgba(255, 255, 255, 0.3);
131
- border-radius: 16px;
132
- padding: 25px;
133
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
134
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
135
- position: relative;
136
- overflow: hidden;
122
+ .view-toggle-btn.active {
123
+ background: var(--accent);
124
+ color: #000;
137
125
  }
138
126
 
139
- .stat-card::before {
140
- content: '';
141
- position: absolute;
142
- top: 0;
143
- left: 0;
144
- right: 0;
145
- height: 4px;
146
- background: linear-gradient(90deg, #fbbf24, #f59e0b);
147
- transform: scaleX(0);
148
- transition: transform 0.3s ease;
127
+ .view-toggle-btn:hover:not(.active) {
128
+ color: var(--text-primary);
149
129
  }
150
130
 
151
- .stat-card:hover {
152
- transform: translateY(-5px) scale(1.02);
153
- box-shadow: 0 12px 48px 0 rgba(31, 38, 135, 0.2);
131
+ .header-stats {
132
+ display: flex;
133
+ gap: 24px;
154
134
  }
155
135
 
156
- .stat-card:hover::before {
157
- transform: scaleX(1);
136
+ .stat-item {
137
+ text-align: center;
158
138
  }
159
139
 
160
140
  .stat-value {
161
- font-size: 36px;
162
- font-weight: 800;
163
- background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
164
- -webkit-background-clip: text;
165
- -webkit-text-fill-color: transparent;
166
- background-clip: text;
167
- line-height: 1.2;
141
+ font-size: 20px;
142
+ font-weight: 700;
143
+ color: var(--accent);
168
144
  }
169
145
 
170
146
  .stat-label {
171
- font-size: 12px;
172
- color: rgba(255, 255, 255, 0.9);
173
- margin-top: 8px;
147
+ font-size: 10px;
148
+ color: var(--text-muted);
174
149
  text-transform: uppercase;
175
- letter-spacing: 1px;
176
- font-weight: 600;
150
+ letter-spacing: 0.5px;
177
151
  }
178
-
179
- /* Modern Tabs */
180
- .tabs {
152
+
153
+ /* Sidebar */
154
+ .sidebar {
155
+ background: var(--bg-secondary);
181
156
  display: flex;
182
- background: rgba(255, 255, 255, 0.15);
183
- backdrop-filter: blur(20px);
184
- -webkit-backdrop-filter: blur(20px);
185
- border: 1px solid rgba(255, 255, 255, 0.3);
186
- border-radius: 16px 16px 0 0;
187
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
157
+ flex-direction: column;
188
158
  overflow: hidden;
189
- animation: fadeIn 0.7s ease-out 0.3s both;
190
159
  }
191
160
 
192
- .tab {
193
- flex: 1;
194
- padding: 18px;
195
- text-align: center;
196
- cursor: pointer;
197
- color: rgba(255, 255, 255, 0.7);
161
+ .sidebar-section {
162
+ padding: 16px;
163
+ border-bottom: 1px solid var(--glass-border);
164
+ }
165
+
166
+ .sidebar-section-title {
167
+ font-size: 11px;
198
168
  font-weight: 600;
169
+ color: var(--text-muted);
170
+ text-transform: uppercase;
171
+ letter-spacing: 1px;
172
+ margin-bottom: 12px;
173
+ }
174
+
175
+ .selector {
176
+ width: 100%;
177
+ padding: 10px 12px;
178
+ background: var(--bg-tertiary);
179
+ border: 1px solid var(--glass-border);
180
+ border-radius: 8px;
181
+ color: var(--text-primary);
199
182
  font-size: 14px;
200
- border-bottom: 3px solid transparent;
201
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
202
- position: relative;
183
+ cursor: pointer;
184
+ transition: all 0.2s;
203
185
  }
204
186
 
205
- .tab::after {
206
- content: '';
207
- position: absolute;
208
- bottom: 0;
209
- left: 50%;
210
- width: 0;
211
- height: 3px;
212
- background: linear-gradient(90deg, #fbbf24, #f59e0b);
213
- transform: translateX(-50%);
214
- transition: width 0.3s ease;
187
+ .selector:hover {
188
+ border-color: var(--accent);
215
189
  }
216
190
 
217
- .tab:hover {
218
- background: rgba(255, 255, 255, 0.1);
219
- color: white;
191
+ .selector:focus {
192
+ outline: none;
193
+ border-color: var(--accent);
194
+ box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.2);
220
195
  }
221
196
 
222
- .tab.active {
223
- color: white;
224
- background: rgba(255, 255, 255, 0.15);
197
+ /* Transitions Panel */
198
+ .transitions-panel {
199
+ padding: 16px;
200
+ border-bottom: 1px solid var(--glass-border);
201
+ max-height: 200px;
202
+ overflow-y: auto;
225
203
  }
226
204
 
227
- .tab.active::after {
228
- width: 100%;
205
+ .transition-item {
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: space-between;
209
+ padding: 10px 12px;
210
+ background: var(--bg-tertiary);
211
+ border: 1px solid var(--glass-border);
212
+ border-radius: 8px;
213
+ margin-bottom: 8px;
214
+ cursor: pointer;
215
+ transition: all 0.2s;
229
216
  }
230
217
 
231
- /* Tab Content - Glass Effect */
232
- .tab-content {
233
- display: none;
234
- background: rgba(255, 255, 255, 0.15);
235
- backdrop-filter: blur(20px);
236
- -webkit-backdrop-filter: blur(20px);
237
- border: 1px solid rgba(255, 255, 255, 0.3);
238
- border-top: none;
239
- padding: 30px;
240
- border-radius: 0 0 16px 16px;
241
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
242
- min-height: 600px;
243
- animation: fadeInContent 0.4s ease-out;
218
+ .transition-item:hover {
219
+ border-color: var(--accent);
220
+ background: rgba(251, 191, 36, 0.05);
244
221
  }
245
222
 
246
- @keyframes fadeInContent {
247
- from { opacity: 0; transform: translateY(-10px); }
248
- to { opacity: 1; transform: translateY(0); }
223
+ .transition-item.inter-machine {
224
+ border-left: 3px solid var(--inter-machine);
249
225
  }
250
226
 
251
- .tab-content.active {
252
- display: block;
227
+ .transition-info {
228
+ flex: 1;
253
229
  }
254
-
255
- /* Grid Layouts */
256
- .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
257
- .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
258
-
259
- /* Glass Card */
260
- .card {
261
- background: rgba(255, 255, 255, 0.1);
262
- backdrop-filter: blur(10px);
263
- -webkit-backdrop-filter: blur(10px);
264
- border: 1px solid rgba(255, 255, 255, 0.2);
265
- border-radius: 16px;
266
- padding: 25px;
267
- box-shadow: 0 4px 24px 0 rgba(31, 38, 135, 0.1);
230
+
231
+ .transition-event {
232
+ font-weight: 600;
233
+ font-size: 13px;
234
+ color: var(--text-primary);
268
235
  }
269
236
 
270
- .card h2 {
271
- font-size: 18px;
272
- font-weight: 700;
273
- color: white;
274
- margin-bottom: 20px;
275
- padding-bottom: 15px;
276
- border-bottom: 2px solid rgba(255, 255, 255, 0.2);
277
- background: linear-gradient(135deg, #fff 0%, rgba(255, 255, 255, 0.8) 100%);
278
- -webkit-background-clip: text;
279
- -webkit-text-fill-color: transparent;
280
- background-clip: text;
237
+ .transition-path {
238
+ font-size: 11px;
239
+ color: var(--text-muted);
240
+ margin-top: 2px;
281
241
  }
282
242
 
283
- /* Modern Forms */
284
- .form-group {
285
- margin-bottom: 20px;
243
+ .transition-target {
244
+ font-size: 10px;
245
+ color: var(--inter-machine);
246
+ margin-top: 2px;
286
247
  }
287
248
 
288
- .form-group label {
289
- display: block;
249
+ .transition-send-btn {
250
+ padding: 6px 12px;
251
+ background: var(--accent);
252
+ border: none;
253
+ border-radius: 6px;
254
+ color: #000;
255
+ font-size: 11px;
290
256
  font-weight: 600;
291
- margin-bottom: 8px;
292
- font-size: 14px;
293
- color: rgba(255, 255, 255, 0.9);
257
+ cursor: pointer;
258
+ transition: all 0.2s;
294
259
  }
295
260
 
296
- .form-group input,
297
- .form-group select,
298
- .form-group textarea {
299
- width: 100%;
300
- padding: 12px 16px;
301
- background: rgba(255, 255, 255, 0.15);
302
- border: 1px solid rgba(255, 255, 255, 0.3);
303
- border-radius: 10px;
304
- font-size: 14px;
305
- color: white;
306
- transition: all 0.3s ease;
261
+ .transition-send-btn:hover {
262
+ background: var(--accent-hover);
307
263
  }
308
264
 
309
- .form-group input::placeholder,
310
- .form-group textarea::placeholder {
311
- color: rgba(255, 255, 255, 0.5);
265
+ .transition-send-btn:disabled {
266
+ background: var(--bg-tertiary);
267
+ color: var(--text-muted);
268
+ cursor: not-allowed;
312
269
  }
313
270
 
314
- .form-group input:focus,
315
- .form-group select:focus,
316
- .form-group textarea:focus {
317
- outline: none;
318
- background: rgba(255, 255, 255, 0.2);
319
- border-color: #fbbf24;
320
- box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.2);
271
+ /* Instance List */
272
+ .instance-list-container {
273
+ flex: 1;
274
+ overflow-y: auto;
275
+ padding: 16px;
321
276
  }
322
277
 
323
- .form-help {
278
+ .instance-list-header {
279
+ display: flex;
280
+ justify-content: space-between;
281
+ align-items: center;
282
+ margin-bottom: 12px;
283
+ }
284
+
285
+ .instance-count {
324
286
  font-size: 12px;
325
- color: rgba(255, 255, 255, 0.7);
326
- margin-top: 6px;
327
- font-style: italic;
287
+ color: var(--text-muted);
328
288
  }
329
-
330
- /* Modern Buttons */
331
- .btn {
332
- padding: 12px 28px;
289
+
290
+ .btn-create {
291
+ padding: 6px 12px;
292
+ background: var(--accent);
333
293
  border: none;
334
- border-radius: 12px;
294
+ border-radius: 6px;
295
+ color: #000;
296
+ font-size: 12px;
297
+ font-weight: 600;
335
298
  cursor: pointer;
336
- font-weight: 700;
337
- font-size: 14px;
338
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
339
- box-shadow: 0 4px 15px 0 rgba(0, 0, 0, 0.1);
340
- position: relative;
341
- overflow: hidden;
299
+ transition: all 0.2s;
342
300
  }
343
301
 
344
- .btn::before {
345
- content: '';
346
- position: absolute;
347
- top: 50%;
348
- left: 50%;
349
- width: 0;
350
- height: 0;
351
- border-radius: 50%;
352
- background: rgba(255, 255, 255, 0.3);
353
- transform: translate(-50%, -50%);
354
- transition: width 0.5s, height 0.5s;
302
+ .btn-create:hover {
303
+ background: var(--accent-hover);
304
+ transform: translateY(-1px);
355
305
  }
356
306
 
357
- .btn:hover::before {
358
- width: 300px;
359
- height: 300px;
307
+ .instance-item {
308
+ background: var(--bg-tertiary);
309
+ border: 1px solid var(--glass-border);
310
+ border-radius: 8px;
311
+ padding: 12px;
312
+ margin-bottom: 8px;
313
+ cursor: pointer;
314
+ transition: all 0.2s;
360
315
  }
361
316
 
362
- .btn-primary {
363
- background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
364
- color: #1a1a1a;
317
+ .instance-item:hover {
318
+ border-color: var(--accent);
319
+ background: rgba(251, 191, 36, 0.05);
365
320
  }
366
321
 
367
- .btn-primary:hover {
368
- transform: translateY(-2px);
369
- box-shadow: 0 8px 25px 0 rgba(251, 191, 36, 0.4);
322
+ .instance-item.selected {
323
+ border-color: var(--accent);
324
+ background: rgba(251, 191, 36, 0.1);
325
+ box-shadow: 0 0 0 1px var(--accent);
370
326
  }
371
327
 
372
- .btn-success {
373
- background: linear-gradient(135deg, #10b981 0%, #059669 100%);
374
- color: white;
328
+ .instance-header {
329
+ display: flex;
330
+ justify-content: space-between;
331
+ align-items: center;
332
+ margin-bottom: 8px;
375
333
  }
376
334
 
377
- .btn-success:hover {
378
- transform: translateY(-2px);
379
- box-shadow: 0 8px 25px 0 rgba(16, 185, 129, 0.4);
335
+ .instance-id {
336
+ font-family: 'Monaco', 'Consolas', monospace;
337
+ font-size: 12px;
338
+ color: var(--text-primary);
380
339
  }
381
340
 
382
- .btn-danger {
383
- background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
384
- color: white;
341
+ .instance-state {
342
+ padding: 4px 8px;
343
+ border-radius: 12px;
344
+ font-size: 10px;
345
+ font-weight: 600;
346
+ text-transform: uppercase;
385
347
  }
386
348
 
387
- .btn-danger:hover {
388
- transform: translateY(-2px);
389
- box-shadow: 0 8px 25px 0 rgba(239, 68, 68, 0.4);
349
+ .instance-state.active {
350
+ background: rgba(59, 130, 246, 0.2);
351
+ color: var(--info);
390
352
  }
391
353
 
392
- .btn-sm {
393
- padding: 8px 16px;
394
- font-size: 12px;
395
- }
396
-
397
- /* Instance List - Modern Cards */
398
- .instance-list {
399
- max-height: 500px;
400
- overflow-y: auto;
401
- scrollbar-width: thin;
402
- scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
354
+ .instance-state.terminal {
355
+ background: rgba(16, 185, 129, 0.2);
356
+ color: var(--success);
403
357
  }
404
358
 
405
- .instance-list::-webkit-scrollbar {
406
- width: 8px;
359
+ .instance-state.error {
360
+ background: rgba(239, 68, 68, 0.2);
361
+ color: var(--error);
407
362
  }
408
363
 
409
- .instance-list::-webkit-scrollbar-track {
410
- background: rgba(255, 255, 255, 0.05);
411
- border-radius: 10px;
364
+ .instance-meta {
365
+ font-size: 11px;
366
+ color: var(--text-muted);
412
367
  }
413
368
 
414
- .instance-list::-webkit-scrollbar-thumb {
415
- background: rgba(255, 255, 255, 0.3);
416
- border-radius: 10px;
369
+ .empty-state {
370
+ text-align: center;
371
+ padding: 40px 20px;
372
+ color: var(--text-muted);
417
373
  }
418
374
 
419
- .instance-item {
420
- background: rgba(255, 255, 255, 0.1);
421
- backdrop-filter: blur(10px);
422
- -webkit-backdrop-filter: blur(10px);
423
- border: 1px solid rgba(255, 255, 255, 0.2);
424
- padding: 18px;
375
+ .empty-state-icon {
376
+ font-size: 32px;
425
377
  margin-bottom: 12px;
426
- border-radius: 12px;
427
- cursor: pointer;
428
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
429
- position: relative;
378
+ opacity: 0.5;
379
+ }
380
+
381
+ /* Main Content - Diagram */
382
+ .main-content {
383
+ background: var(--bg-primary);
384
+ display: flex;
385
+ flex-direction: column;
430
386
  overflow: hidden;
431
387
  }
432
388
 
433
- .instance-item::before {
434
- content: '';
435
- position: absolute;
436
- top: 0;
437
- left: 0;
438
- width: 4px;
439
- height: 100%;
440
- background: linear-gradient(180deg, #fbbf24, #f59e0b);
441
- opacity: 0;
442
- transition: opacity 0.3s ease;
389
+ .diagram-header {
390
+ padding: 16px 24px;
391
+ background: var(--bg-secondary);
392
+ border-bottom: 1px solid var(--glass-border);
393
+ display: flex;
394
+ justify-content: space-between;
395
+ align-items: center;
443
396
  }
444
397
 
445
- .instance-item:hover {
446
- background: rgba(255, 255, 255, 0.15);
447
- box-shadow: 0 8px 30px rgba(251, 191, 36, 0.2);
448
- transform: translateY(-3px) translateX(4px);
398
+ .diagram-title {
399
+ font-size: 16px;
400
+ font-weight: 600;
449
401
  }
450
402
 
451
- .instance-item:hover::before {
452
- opacity: 1;
403
+ .diagram-title span {
404
+ color: var(--accent);
453
405
  }
454
406
 
455
- .instance-item.selected {
456
- border-color: #fbbf24;
457
- background: rgba(251, 191, 36, 0.2);
458
- box-shadow: 0 8px 30px rgba(251, 191, 36, 0.3);
407
+ .diagram-legend {
408
+ display: flex;
409
+ gap: 16px;
410
+ font-size: 12px;
459
411
  }
460
412
 
461
- .instance-header {
413
+ .legend-item {
462
414
  display: flex;
463
- justify-content: space-between;
464
415
  align-items: center;
465
- margin-bottom: 12px;
416
+ gap: 6px;
417
+ color: var(--text-secondary);
466
418
  }
467
419
 
468
- .instance-id {
469
- font-weight: 700;
470
- color: white;
471
- font-family: 'Courier New', monospace;
472
- font-size: 13px;
473
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
420
+ .legend-dot {
421
+ width: 12px;
422
+ height: 12px;
423
+ border-radius: 3px;
474
424
  }
475
425
 
476
- .badge {
477
- padding: 6px 14px;
478
- border-radius: 20px;
479
- font-size: 11px;
480
- font-weight: 700;
481
- text-transform: uppercase;
482
- letter-spacing: 0.5px;
483
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
426
+ .legend-line {
427
+ width: 20px;
428
+ height: 3px;
429
+ border-radius: 2px;
484
430
  }
485
431
 
486
- .badge.active {
487
- background: linear-gradient(135deg, #3b82f6, #2563eb);
488
- color: white;
432
+ .legend-dot.entry { background: #fbbf24; }
433
+ .legend-dot.current { background: #3b82f6; }
434
+ .legend-dot.terminal { background: #10b981; }
435
+ .legend-dot.error { background: #ef4444; }
436
+ .legend-line.inter-machine { background: #10b981; }
437
+
438
+ .diagram-container {
439
+ flex: 1;
440
+ padding: 24px;
441
+ overflow: auto;
442
+ display: flex;
443
+ align-items: center;
444
+ justify-content: center;
489
445
  }
490
446
 
491
- .badge.final {
492
- background: linear-gradient(135deg, #10b981, #059669);
493
- color: white;
447
+ .mermaid-wrapper {
448
+ background: #fff;
449
+ border-radius: 12px;
450
+ padding: 24px;
451
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
452
+ max-width: 100%;
453
+ overflow: auto;
494
454
  }
495
455
 
496
- .badge.error {
497
- background: linear-gradient(135deg, #ef4444, #dc2626);
498
- color: white;
456
+ /* Component View */
457
+ .component-view {
458
+ flex: 1;
459
+ padding: 24px;
460
+ overflow: auto;
461
+ position: relative;
499
462
  }
500
463
 
501
- .instance-meta {
502
- font-size: 13px;
503
- color: rgba(255, 255, 255, 0.8);
504
- line-height: 1.6;
464
+ .component-graph {
465
+ position: relative;
466
+ min-height: 600px;
467
+ width: 100%;
505
468
  }
506
-
507
- /* Modern Terminal Blotter */
508
- .blotter {
509
- background: rgba(0, 0, 0, 0.4);
510
- backdrop-filter: blur(10px);
511
- -webkit-backdrop-filter: blur(10px);
512
- border: 1px solid rgba(255, 255, 255, 0.1);
469
+
470
+ .machine-card {
471
+ position: absolute;
472
+ background: var(--bg-secondary);
473
+ border: 2px solid var(--glass-border);
513
474
  border-radius: 12px;
514
- padding: 20px;
515
- max-height: 600px;
516
- overflow-y: auto;
517
- font-family: 'Courier New', 'Consolas', monospace;
518
- box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.3);
475
+ padding: 16px;
476
+ min-width: 180px;
477
+ cursor: pointer;
478
+ transition: all 0.2s;
519
479
  }
520
480
 
521
- .blotter-item {
522
- border-left: 4px solid #fbbf24;
523
- background: rgba(255, 255, 255, 0.05);
524
- padding: 12px 16px;
525
- margin-bottom: 10px;
526
- font-size: 13px;
527
- border-radius: 0 8px 8px 0;
528
- transition: all 0.3s ease;
529
- animation: slideInLeft 0.3s ease-out;
481
+ .machine-card:hover {
482
+ border-color: var(--accent);
483
+ transform: translateY(-2px);
484
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
530
485
  }
531
486
 
532
- @keyframes slideInLeft {
533
- from {
534
- opacity: 0;
535
- transform: translateX(-20px);
536
- }
537
- to {
538
- opacity: 1;
539
- transform: translateX(0);
540
- }
487
+ .machine-card.entry-machine {
488
+ border-color: var(--accent);
541
489
  }
542
490
 
543
- .blotter-item:hover {
544
- background: rgba(255, 255, 255, 0.1);
545
- transform: translateX(4px);
491
+ .machine-card-header {
492
+ display: flex;
493
+ justify-content: space-between;
494
+ align-items: center;
495
+ margin-bottom: 12px;
496
+ }
497
+
498
+ .machine-card-name {
499
+ font-weight: 600;
500
+ font-size: 14px;
501
+ color: var(--text-primary);
546
502
  }
547
503
 
548
- .blotter-item.created {
549
- border-color: #10b981;
550
- box-shadow: 0 0 10px rgba(16, 185, 129, 0.2);
504
+ .machine-card-badge {
505
+ padding: 4px 8px;
506
+ background: var(--accent);
507
+ color: #000;
508
+ font-size: 10px;
509
+ font-weight: 600;
510
+ border-radius: 10px;
551
511
  }
552
512
 
553
- .blotter-item.error {
554
- border-color: #ef4444;
555
- box-shadow: 0 0 10px rgba(239, 68, 68, 0.2);
513
+ .machine-card-stats {
514
+ font-size: 11px;
515
+ color: var(--text-muted);
516
+ line-height: 1.6;
556
517
  }
557
518
 
558
- .blotter-item.cross-component {
559
- border-color: #f59e0b;
560
- box-shadow: 0 0 10px rgba(245, 158, 11, 0.2);
519
+ .machine-card-instances {
520
+ margin-top: 8px;
521
+ padding-top: 8px;
522
+ border-top: 1px solid var(--glass-border);
523
+ font-size: 12px;
524
+ color: var(--text-secondary);
561
525
  }
562
526
 
563
- .blotter-time {
564
- color: rgba(255, 255, 255, 0.5);
565
- font-size: 11px;
527
+ .inter-machine-arrow {
528
+ position: absolute;
529
+ pointer-events: none;
530
+ }
531
+
532
+ .inter-machine-label {
533
+ position: absolute;
534
+ background: var(--bg-tertiary);
535
+ border: 1px solid var(--inter-machine);
536
+ color: var(--inter-machine);
537
+ padding: 4px 8px;
538
+ border-radius: 4px;
539
+ font-size: 10px;
566
540
  font-weight: 600;
541
+ white-space: nowrap;
567
542
  }
568
543
 
569
- .blotter-content {
570
- color: rgba(255, 255, 255, 0.9);
571
- margin-top: 6px;
572
- line-height: 1.5;
544
+ /* History Panel */
545
+ .history-panel {
546
+ grid-column: 1 / -1;
547
+ background: var(--bg-secondary);
548
+ border-top: 1px solid var(--glass-border);
549
+ display: flex;
550
+ flex-direction: column;
551
+ overflow: hidden;
573
552
  }
574
553
 
575
- .blotter-source {
576
- color: #f59e0b;
577
- font-size: 11px;
578
- margin-top: 6px;
554
+ .history-header {
555
+ padding: 12px 24px;
556
+ background: var(--bg-tertiary);
557
+ border-bottom: 1px solid var(--glass-border);
558
+ display: flex;
559
+ justify-content: space-between;
560
+ align-items: center;
561
+ }
562
+
563
+ .history-title {
564
+ font-size: 14px;
579
565
  font-weight: 600;
580
- text-transform: uppercase;
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 8px;
581
569
  }
582
-
583
- /* Mermaid */
584
- .mermaid-container { background: white; border: 1px solid #e1e4e8; border-radius: 6px; padding: 20px; overflow-x: auto; }
585
-
586
- /* Sequence */
587
- .sequence-container { max-height: 600px; overflow-y: auto; }
588
- .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; }
589
- .sequence-time { min-width: 80px; font-size: 11px; color: #586069; font-family: monospace; }
590
- .sequence-arrow { margin: 0 15px; color: #0366d6; }
591
- .sequence-content { flex: 1; }
592
- .sequence-event { font-weight: 600; font-size: 14px; }
593
- .sequence-detail { font-size: 12px; color: #586069; margin-top: 4px; }
594
-
595
- /* Empty State */
596
- .empty-state { text-align: center; padding: 60px 20px; color: #959da5; }
597
- .empty-state-icon { font-size: 48px; margin-bottom: 15px; }
598
-
599
- /* Filter */
600
- .filter-bar { display: flex; gap: 10px; margin-bottom: 20px; padding: 15px; background: #f6f8fa; border-radius: 6px; }
601
- .filter-bar input, .filter-bar select { flex: 1; }
602
-
603
- /* Component Selector */
604
- .component-selector { margin-bottom: 20px; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; }
605
- .component-selector label { display: block; font-weight: 600; margin-bottom: 8px; }
606
- .component-selector select { width: 100%; padding: 10px; border-radius: 4px; border: 1px solid #d1d5da; }
607
- </style>
570
+
571
+ .history-instance-info {
572
+ font-size: 12px;
573
+ color: var(--text-muted);
574
+ }
575
+
576
+ .history-actions {
577
+ display: flex;
578
+ gap: 8px;
579
+ }
580
+
581
+ .history-content {
582
+ flex: 1;
583
+ overflow-x: auto;
584
+ overflow-y: hidden;
585
+ padding: 16px 24px;
586
+ }
587
+
588
+ .history-timeline {
589
+ display: flex;
590
+ align-items: center;
591
+ gap: 0;
592
+ min-width: max-content;
593
+ height: 100%;
594
+ }
595
+
596
+ .history-node {
597
+ display: flex;
598
+ flex-direction: column;
599
+ align-items: center;
600
+ min-width: 120px;
601
+ }
602
+
603
+ .history-state {
604
+ padding: 10px 16px;
605
+ background: var(--bg-tertiary);
606
+ border: 2px solid var(--glass-border);
607
+ border-radius: 8px;
608
+ font-size: 13px;
609
+ font-weight: 600;
610
+ color: var(--text-primary);
611
+ transition: all 0.2s;
612
+ }
613
+
614
+ .history-state.current {
615
+ border-color: var(--info);
616
+ background: rgba(59, 130, 246, 0.2);
617
+ color: var(--info);
618
+ }
619
+
620
+ .history-state.terminal {
621
+ border-color: var(--success);
622
+ background: rgba(16, 185, 129, 0.2);
623
+ color: var(--success);
624
+ }
625
+
626
+ .history-time {
627
+ font-size: 10px;
628
+ color: var(--text-muted);
629
+ margin-top: 8px;
630
+ }
631
+
632
+ .history-arrow {
633
+ display: flex;
634
+ flex-direction: column;
635
+ align-items: center;
636
+ padding: 0 8px;
637
+ }
638
+
639
+ .history-arrow-line {
640
+ width: 40px;
641
+ height: 2px;
642
+ background: var(--accent);
643
+ position: relative;
644
+ }
645
+
646
+ .history-arrow-line::after {
647
+ content: '';
648
+ position: absolute;
649
+ right: -4px;
650
+ top: -4px;
651
+ border: 5px solid transparent;
652
+ border-left-color: var(--accent);
653
+ }
654
+
655
+ .history-event {
656
+ font-size: 10px;
657
+ color: var(--accent);
658
+ margin-top: 4px;
659
+ max-width: 80px;
660
+ text-align: center;
661
+ word-break: break-word;
662
+ }
663
+
664
+ .history-empty {
665
+ display: flex;
666
+ align-items: center;
667
+ justify-content: center;
668
+ height: 100%;
669
+ color: var(--text-muted);
670
+ font-size: 14px;
671
+ }
672
+
673
+ /* Modal */
674
+ .modal-overlay {
675
+ position: fixed;
676
+ top: 0;
677
+ left: 0;
678
+ right: 0;
679
+ bottom: 0;
680
+ background: rgba(0, 0, 0, 0.8);
681
+ display: flex;
682
+ align-items: center;
683
+ justify-content: center;
684
+ z-index: 1000;
685
+ opacity: 0;
686
+ visibility: hidden;
687
+ transition: all 0.3s;
688
+ }
689
+
690
+ .modal-overlay.active {
691
+ opacity: 1;
692
+ visibility: visible;
693
+ }
694
+
695
+ .modal {
696
+ background: var(--bg-secondary);
697
+ border: 1px solid var(--glass-border);
698
+ border-radius: 16px;
699
+ padding: 24px;
700
+ width: 90%;
701
+ max-width: 500px;
702
+ transform: translateY(20px);
703
+ transition: transform 0.3s;
704
+ }
705
+
706
+ .modal-overlay.active .modal {
707
+ transform: translateY(0);
708
+ }
709
+
710
+ .modal-header {
711
+ display: flex;
712
+ justify-content: space-between;
713
+ align-items: center;
714
+ margin-bottom: 20px;
715
+ }
716
+
717
+ .modal-title {
718
+ font-size: 18px;
719
+ font-weight: 600;
720
+ }
721
+
722
+ .modal-close {
723
+ background: none;
724
+ border: none;
725
+ color: var(--text-muted);
726
+ font-size: 24px;
727
+ cursor: pointer;
728
+ padding: 4px;
729
+ line-height: 1;
730
+ }
731
+
732
+ .modal-close:hover {
733
+ color: var(--text-primary);
734
+ }
735
+
736
+ .form-group {
737
+ margin-bottom: 16px;
738
+ }
739
+
740
+ .form-group label {
741
+ display: block;
742
+ font-size: 13px;
743
+ font-weight: 500;
744
+ color: var(--text-secondary);
745
+ margin-bottom: 6px;
746
+ }
747
+
748
+ .form-group input,
749
+ .form-group textarea {
750
+ width: 100%;
751
+ padding: 10px 12px;
752
+ background: var(--bg-tertiary);
753
+ border: 1px solid var(--glass-border);
754
+ border-radius: 8px;
755
+ color: var(--text-primary);
756
+ font-size: 14px;
757
+ font-family: inherit;
758
+ }
759
+
760
+ .form-group input:focus,
761
+ .form-group textarea:focus {
762
+ outline: none;
763
+ border-color: var(--accent);
764
+ }
765
+
766
+ .form-group textarea {
767
+ resize: vertical;
768
+ min-height: 100px;
769
+ font-family: 'Monaco', 'Consolas', monospace;
770
+ }
771
+
772
+ .form-help {
773
+ font-size: 11px;
774
+ color: var(--text-muted);
775
+ margin-top: 4px;
776
+ }
777
+
778
+ .modal-actions {
779
+ display: flex;
780
+ gap: 12px;
781
+ justify-content: flex-end;
782
+ margin-top: 24px;
783
+ }
784
+
785
+ .btn {
786
+ padding: 10px 20px;
787
+ border: none;
788
+ border-radius: 8px;
789
+ font-size: 14px;
790
+ font-weight: 600;
791
+ cursor: pointer;
792
+ transition: all 0.2s;
793
+ }
794
+
795
+ .btn-primary {
796
+ background: var(--accent);
797
+ color: #000;
798
+ }
799
+
800
+ .btn-primary:hover {
801
+ background: var(--accent-hover);
802
+ }
803
+
804
+ .btn-secondary {
805
+ background: var(--bg-tertiary);
806
+ color: var(--text-primary);
807
+ border: 1px solid var(--glass-border);
808
+ }
809
+
810
+ .btn-secondary:hover {
811
+ background: var(--bg-primary);
812
+ }
813
+
814
+ /* Scrollbar */
815
+ ::-webkit-scrollbar {
816
+ width: 8px;
817
+ height: 8px;
818
+ }
819
+
820
+ ::-webkit-scrollbar-track {
821
+ background: var(--bg-primary);
822
+ }
823
+
824
+ ::-webkit-scrollbar-thumb {
825
+ background: var(--glass-border);
826
+ border-radius: 4px;
827
+ }
828
+
829
+ ::-webkit-scrollbar-thumb:hover {
830
+ background: var(--text-muted);
831
+ }
832
+
833
+ /* Toast notifications */
834
+ .toast-container {
835
+ position: fixed;
836
+ bottom: 24px;
837
+ right: 24px;
838
+ z-index: 2000;
839
+ display: flex;
840
+ flex-direction: column;
841
+ gap: 8px;
842
+ }
843
+
844
+ .toast {
845
+ padding: 12px 16px;
846
+ background: var(--bg-tertiary);
847
+ border: 1px solid var(--glass-border);
848
+ border-radius: 8px;
849
+ color: var(--text-primary);
850
+ font-size: 13px;
851
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
852
+ animation: slideIn 0.3s ease;
853
+ }
854
+
855
+ .toast.success {
856
+ border-color: var(--success);
857
+ }
858
+
859
+ .toast.error {
860
+ border-color: var(--error);
861
+ }
862
+
863
+ @keyframes slideIn {
864
+ from {
865
+ transform: translateX(100%);
866
+ opacity: 0;
867
+ }
868
+ to {
869
+ transform: translateX(0);
870
+ opacity: 1;
871
+ }
872
+ }
873
+
874
+ .hidden {
875
+ display: none !important;
876
+ }
877
+ </style>
608
878
  </head>
609
879
  <body>
610
- <div class="container">
611
- <div class="header">
612
- <div>
613
- <h1>📊 xcomponent-ai Dashboard</h1>
614
- <div class="status">
880
+ <div class="app-layout">
881
+ <!-- Header -->
882
+ <header class="header">
883
+ <div class="header-left">
884
+ <div class="logo">xcomponent<span>-ai</span></div>
885
+ <div class="connection-status">
615
886
  <span class="status-dot" id="ws-status"></span>
616
- <span id="component-name">Loading...</span>
887
+ <span id="connection-text">Connecting...</span>
617
888
  </div>
618
889
  </div>
619
- <div class="header-links">
620
- <a href="/api-docs" target="_blank">📖 API Docs</a>
621
- <a href="#" onclick="exportState()">💾 Export</a>
890
+ <div class="header-center">
891
+ <div class="view-toggle">
892
+ <button class="view-toggle-btn active" id="btn-view-machine" onclick="setView('machine')">
893
+ Machine View
894
+ </button>
895
+ <button class="view-toggle-btn" id="btn-view-component" onclick="setView('component')">
896
+ Component View
897
+ </button>
898
+ </div>
622
899
  </div>
623
- </div>
624
-
625
- <div class="stats-grid">
626
- <div class="stat-card"><div class="stat-value" id="stat-total">0</div><div class="stat-label">Total Instances</div></div>
627
- <div class="stat-card"><div class="stat-value" id="stat-active">0</div><div class="stat-label">Active</div></div>
628
- <div class="stat-card"><div class="stat-value" id="stat-final">0</div><div class="stat-label">Final</div></div>
629
- <div class="stat-card"><div class="stat-value" id="stat-error">0</div><div class="stat-label">Error</div></div>
630
- <div class="stat-card"><div class="stat-value" id="stat-events">0</div><div class="stat-label">Events</div></div>
631
- </div>
632
-
633
- <div class="component-selector">
634
- <label for="component-select">Component:</label>
635
- <select id="component-select" onchange="selectComponent()">
636
- <option value="">Select a component...</option>
637
- </select>
638
- </div>
639
-
640
- <div class="tabs">
641
- <div class="tab active" onclick="switchTab('diagram')">🎨 FSM Diagram</div>
642
- <div class="tab" onclick="switchTab('overview')">📋 Instances</div>
643
- <div class="tab" onclick="switchTab('blotter')">📊 Event Blotter</div>
644
- <div class="tab" onclick="switchTab('traceability')">🔍 Traceability</div>
645
- </div>
646
-
647
- <div class="tab-content" id="tab-overview">
648
- <div class="filter-bar">
649
- <input type="text" id="filter-instance" placeholder="Filter by instance ID..." oninput="filterInstances()">
650
- <select id="filter-machine" onchange="filterInstances()">
651
- <option value="">All Machines</option>
900
+ <div class="header-stats">
901
+ <div class="stat-item">
902
+ <div class="stat-value" id="stat-instances">0</div>
903
+ <div class="stat-label">Instances</div>
904
+ </div>
905
+ <div class="stat-item">
906
+ <div class="stat-value" id="stat-active">0</div>
907
+ <div class="stat-label">Active</div>
908
+ </div>
909
+ <div class="stat-item">
910
+ <div class="stat-value" id="stat-terminal">0</div>
911
+ <div class="stat-label">Terminal</div>
912
+ </div>
913
+ </div>
914
+ </header>
915
+
916
+ <!-- Sidebar -->
917
+ <aside class="sidebar">
918
+ <div class="sidebar-section">
919
+ <div class="sidebar-section-title">Component</div>
920
+ <select class="selector" id="component-select" onchange="selectComponent()">
921
+ <option value="">Loading...</option>
652
922
  </select>
653
- <select id="filter-state" onchange="filterInstances()">
654
- <option value="">All States</option>
923
+ </div>
924
+
925
+ <div class="sidebar-section" id="machine-selector-section">
926
+ <div class="sidebar-section-title">State Machine</div>
927
+ <select class="selector" id="machine-select" onchange="selectMachine()">
928
+ <option value="">Select a machine...</option>
655
929
  </select>
656
930
  </div>
657
- <div class="instance-list" id="instance-list">
658
- <div class="empty-state">
659
- <div class="empty-state-icon">📭</div>
660
- <div>No instances yet. Go to the "FSM Diagram" tab to create one!</div>
931
+
932
+ <!-- Transitions Panel -->
933
+ <div class="transitions-panel" id="transitions-panel">
934
+ <div class="sidebar-section-title">Available Transitions</div>
935
+ <div id="transitions-list">
936
+ <div class="empty-state" style="padding: 20px 0;">
937
+ <div style="font-size: 12px;">Select an instance to see transitions</div>
938
+ </div>
661
939
  </div>
662
940
  </div>
663
- </div>
664
-
665
- <div class="tab-content active" id="tab-diagram">
666
- <div class="grid-2">
667
- <div>
668
- <div class="form-group">
669
- <label for="diagram-machine">State Machine:</label>
670
- <select id="diagram-machine" onchange="renderDiagram()">
671
- <option value="">Select a state machine...</option>
672
- </select>
941
+
942
+ <div class="instance-list-container">
943
+ <div class="instance-list-header">
944
+ <div class="sidebar-section-title" style="margin: 0;">Instances</div>
945
+ <button class="btn-create" id="btn-create-instance" onclick="openCreateModal()">+ New</button>
946
+ </div>
947
+ <div class="instance-count" id="instance-count">0 instances</div>
948
+ <div id="instance-list">
949
+ <div class="empty-state">
950
+ <div class="empty-state-icon">&#128230;</div>
951
+ <div>Select a machine to see instances</div>
952
+ </div>
953
+ </div>
954
+ </div>
955
+ </aside>
956
+
957
+ <!-- Main Content -->
958
+ <main class="main-content">
959
+ <!-- Machine View -->
960
+ <div id="machine-view">
961
+ <div class="diagram-header">
962
+ <div class="diagram-title" id="diagram-title">
963
+ Select a state machine to view its diagram
964
+ </div>
965
+ <div class="diagram-legend">
966
+ <div class="legend-item">
967
+ <div class="legend-dot entry"></div>
968
+ <span>Entry</span>
969
+ </div>
970
+ <div class="legend-item">
971
+ <div class="legend-dot current"></div>
972
+ <span>Current</span>
973
+ </div>
974
+ <div class="legend-item">
975
+ <div class="legend-dot terminal"></div>
976
+ <span>Terminal</span>
977
+ </div>
978
+ <div class="legend-item">
979
+ <div class="legend-dot error"></div>
980
+ <span>Error</span>
981
+ </div>
982
+ <div class="legend-item">
983
+ <div class="legend-line inter-machine"></div>
984
+ <span>Inter-machine</span>
985
+ </div>
673
986
  </div>
674
- <div class="mermaid-container" id="diagram-container">
987
+ </div>
988
+ <div class="diagram-container">
989
+ <div class="mermaid-wrapper" id="diagram-container">
675
990
  <div class="empty-state">
676
- <div class="empty-state-icon">🎨</div>
991
+ <div class="empty-state-icon">&#128202;</div>
677
992
  <div>Select a state machine to view its diagram</div>
678
993
  </div>
679
994
  </div>
680
995
  </div>
996
+ </div>
681
997
 
682
- <div>
683
- <div class="card">
684
- <h2>Create New Instance</h2>
685
- <div id="diagram-create-form">
686
- <div class="empty-state" style="padding: 40px 20px;">
687
- <div class="empty-state-icon">👈</div>
688
- <div>Select a state machine to create an instance</div>
689
- </div>
998
+ <!-- Component View -->
999
+ <div id="component-view" class="hidden">
1000
+ <div class="diagram-header">
1001
+ <div class="diagram-title" id="component-title">
1002
+ Component Overview
1003
+ </div>
1004
+ <div class="diagram-legend">
1005
+ <div class="legend-item">
1006
+ <div class="legend-dot entry"></div>
1007
+ <span>Entry Machine</span>
1008
+ </div>
1009
+ <div class="legend-item">
1010
+ <div class="legend-line inter-machine"></div>
1011
+ <span>Inter-machine Transition</span>
690
1012
  </div>
691
1013
  </div>
692
-
693
- <div class="card" style="margin-top: 20px;" id="diagram-instances-card">
694
- <h2>Active Instances</h2>
695
- <div id="diagram-instances-list">
696
- <div class="empty-state" style="padding: 20px;">
697
- <div class="empty-state-icon">📭</div>
698
- <div>No instances for this state machine</div>
699
- </div>
1014
+ </div>
1015
+ <div class="component-view">
1016
+ <div class="component-graph" id="component-graph">
1017
+ <div class="empty-state">
1018
+ <div class="empty-state-icon">&#127959;</div>
1019
+ <div>Select a component to view its machines</div>
700
1020
  </div>
701
1021
  </div>
702
1022
  </div>
703
1023
  </div>
704
- </div>
705
-
706
- <div class="tab-content" id="tab-blotter">
707
- <div class="filter-bar">
708
- <input type="text" id="blotter-filter" placeholder="Filter events..." oninput="filterBlotter()">
709
- <select id="blotter-type" onchange="filterBlotter()">
710
- <option value="">All Events</option>
711
- <option value="state-change">State Changes</option>
712
- <option value="created">Created</option>
713
- <option value="error">Errors</option>
714
- <option value="cross-component">Cross-Component</option>
715
- </select>
716
- <button class="btn btn-sm btn-danger" onclick="clearBlotter()">Clear</button>
1024
+ </main>
1025
+
1026
+ <!-- History Panel -->
1027
+ <section class="history-panel">
1028
+ <div class="history-header">
1029
+ <div class="history-title">
1030
+ Transition History
1031
+ <span class="history-instance-info" id="history-instance-info"></span>
1032
+ </div>
1033
+ <div class="history-actions" id="history-actions"></div>
1034
+ </div>
1035
+ <div class="history-content">
1036
+ <div class="history-timeline" id="history-timeline">
1037
+ <div class="history-empty">Select an instance to view its transition history</div>
1038
+ </div>
717
1039
  </div>
718
- <div class="blotter" id="event-blotter">
719
- <div style="color: #8b949e; text-align: center; padding: 40px;">Waiting for events...</div>
1040
+ </section>
1041
+ </div>
1042
+
1043
+ <!-- Create Instance Modal -->
1044
+ <div class="modal-overlay" id="create-modal">
1045
+ <div class="modal">
1046
+ <div class="modal-header">
1047
+ <div class="modal-title">Create New Instance</div>
1048
+ <button class="modal-close" onclick="closeCreateModal()">&times;</button>
1049
+ </div>
1050
+ <div id="create-form-content">
1051
+ <div class="form-group">
1052
+ <label>Context (JSON)</label>
1053
+ <textarea id="create-context" placeholder='{"property": "value"}'></textarea>
1054
+ <div class="form-help">Enter a JSON object with context properties</div>
1055
+ </div>
1056
+ </div>
1057
+ <div class="modal-actions">
1058
+ <button class="btn btn-secondary" onclick="closeCreateModal()">Cancel</button>
1059
+ <button class="btn btn-primary" onclick="createInstance()">Create</button>
720
1060
  </div>
721
1061
  </div>
722
-
723
- <div class="tab-content" id="tab-traceability">
1062
+ </div>
1063
+
1064
+ <!-- Send Event Modal -->
1065
+ <div class="modal-overlay" id="event-modal">
1066
+ <div class="modal">
1067
+ <div class="modal-header">
1068
+ <div class="modal-title">Send Event</div>
1069
+ <button class="modal-close" onclick="closeEventModal()">&times;</button>
1070
+ </div>
724
1071
  <div class="form-group">
725
- <label for="trace-instance">Instance:</label>
726
- <select id="trace-instance" onchange="loadTrace()">
727
- <option value="">Select an instance...</option>
728
- </select>
1072
+ <label>Event Type</label>
1073
+ <input type="text" id="event-type" placeholder="event_name">
729
1074
  </div>
730
- <div class="sequence-container" id="trace-container">
731
- <div class="empty-state">
732
- <div class="empty-state-icon">🔍</div>
733
- <div>Select an instance to view its history</div>
734
- </div>
1075
+ <div class="form-group">
1076
+ <label>Payload (JSON)</label>
1077
+ <textarea id="event-payload" placeholder='{"key": "value"}'></textarea>
1078
+ <div class="form-help">Optional JSON payload for the event</div>
1079
+ </div>
1080
+ <div class="modal-actions">
1081
+ <button class="btn btn-secondary" onclick="closeEventModal()">Cancel</button>
1082
+ <button class="btn btn-primary" onclick="sendEvent()">Send</button>
735
1083
  </div>
736
1084
  </div>
737
-
738
1085
  </div>
739
-
1086
+
1087
+ <!-- Toast Container -->
1088
+ <div class="toast-container" id="toast-container"></div>
1089
+
740
1090
  <script>
1091
+ // State
741
1092
  const socket = io();
742
1093
  let componentsData = [];
743
1094
  let selectedComponentName = null;
1095
+ let selectedMachineName = null;
1096
+ let selectedInstanceId = null;
744
1097
  let instances = [];
745
- let events = [];
746
- let selectedInstance = null;
747
-
748
- // Get current component
749
- function getCurrentComponent() {
750
- if (!selectedComponentName || componentsData.length === 0) {
751
- return componentsData[0] || null;
752
- }
753
- return componentsData.find(c => c.name === selectedComponentName) || componentsData[0];
754
- }
1098
+ let allInstances = [];
1099
+ let terminalStates = new Set();
1100
+ let currentTransitions = [];
1101
+ let currentView = 'machine'; // 'machine' or 'component'
1102
+
1103
+ // Initialize Mermaid
1104
+ mermaid.initialize({
1105
+ startOnLoad: false,
1106
+ theme: 'default',
1107
+ securityLevel: 'loose'
1108
+ });
755
1109
 
756
- // WebSocket connection
1110
+ // WebSocket Events
757
1111
  socket.on('connect', () => {
758
1112
  document.getElementById('ws-status').className = 'status-dot connected';
1113
+ document.getElementById('connection-text').textContent = 'Connected';
759
1114
  });
760
1115
 
761
1116
  socket.on('disconnect', () => {
762
- document.getElementById('ws-status').className = 'status-dot disconnected';
1117
+ document.getElementById('ws-status').className = 'status-dot';
1118
+ document.getElementById('connection-text').textContent = 'Disconnected';
763
1119
  });
764
1120
 
765
1121
  socket.on('components_data', (data) => {
766
1122
  componentsData = data.components || [];
767
1123
  if (componentsData.length > 0) {
768
1124
  selectedComponentName = componentsData[0].name;
769
- document.getElementById('component-name').textContent = `${componentsData.length} component(s) loaded`;
770
- populateSelectors();
1125
+ populateComponentSelector();
1126
+ populateMachineSelector();
1127
+ if (currentView === 'component') {
1128
+ renderComponentView();
1129
+ }
771
1130
  }
772
1131
  });
773
-
774
- socket.on('instance_created', (data) => {
775
- addEvent({type: 'created', timestamp: Date.now(), message: `Instance ${data.instanceId} created (${data.machineName})`, instanceId: data.instanceId});
1132
+
1133
+ socket.on('instance_created', () => {
776
1134
  loadInstances();
1135
+ showToast('Instance created', 'success');
777
1136
  });
778
-
1137
+
779
1138
  socket.on('state_change', (data) => {
780
- addEvent({type: 'state-change', timestamp: Date.now(), message: `${data.instanceId}: ${data.previousState} → ${data.newState} (${data.event.type})`, instanceId: data.instanceId, source: data.componentName});
781
1139
  loadInstances();
782
- if (selectedInstance === data.instanceId) loadTrace();
1140
+ if (selectedInstanceId === data.instanceId) {
1141
+ loadInstanceHistory(selectedInstanceId);
1142
+ renderDiagram();
1143
+ loadTransitionsForInstance(selectedInstanceId);
1144
+ }
783
1145
  });
784
-
1146
+
785
1147
  socket.on('instance_error', (data) => {
786
- addEvent({type: 'error', timestamp: Date.now(), message: `ERROR in ${data.instanceId}: ${data.error}`, instanceId: data.instanceId});
787
1148
  loadInstances();
1149
+ showToast(`Error: ${data.error}`, 'error');
788
1150
  });
789
-
790
- // Tab Switching
791
- function switchTab(tabName) {
792
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
793
- document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
794
-
795
- // Find the tab element by matching the onclick content
796
- document.querySelectorAll('.tab').forEach(tab => {
797
- if (tab.getAttribute('onclick') === `switchTab('${tabName}')`) {
798
- tab.classList.add('active');
799
- }
800
- });
801
1151
 
802
- document.getElementById('tab-' + tabName).classList.add('active');
803
- }
804
-
805
- // Load Instances
806
- async function loadInstances() {
807
- const res = await fetch('/api/instances');
808
- const data = await res.json();
809
- instances = data.instances || [];
810
- updateStats();
811
- renderInstances();
812
- updateTraceSelector();
1152
+ socket.on('instance_disposed', () => {
1153
+ loadInstances();
1154
+ });
813
1155
 
814
- // Update diagram instances list if a machine is selected
815
- const selectedMachine = document.getElementById('diagram-machine')?.value;
816
- if (selectedMachine) {
817
- updateDiagramInstancesList(selectedMachine);
1156
+ // View Toggle
1157
+ function setView(view) {
1158
+ currentView = view;
1159
+ document.getElementById('btn-view-machine').classList.toggle('active', view === 'machine');
1160
+ document.getElementById('btn-view-component').classList.toggle('active', view === 'component');
1161
+ document.getElementById('machine-view').classList.toggle('hidden', view !== 'machine');
1162
+ document.getElementById('component-view').classList.toggle('hidden', view !== 'component');
1163
+ document.getElementById('machine-selector-section').classList.toggle('hidden', view === 'component');
1164
+
1165
+ if (view === 'component') {
1166
+ renderComponentView();
1167
+ } else if (selectedMachineName) {
1168
+ renderDiagram();
818
1169
  }
819
1170
  }
820
-
821
- function updateStats() {
822
- document.getElementById('stat-total').textContent = instances.length;
823
- document.getElementById('stat-active').textContent = instances.filter(i => i.status === 'active').length;
824
- document.getElementById('stat-final').textContent = instances.filter(i => i.status === 'final').length;
825
- document.getElementById('stat-error').textContent = instances.filter(i => i.status === 'error').length;
826
- document.getElementById('stat-events').textContent = events.length;
827
- }
828
-
829
- function renderInstances() {
830
- const container = document.getElementById('instance-list');
831
- if (instances.length === 0) {
832
- container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📭</div><div>No instances yet. Go to the "FSM Diagram" tab to create one!</div></div>';
1171
+
1172
+ // Component/Machine Selection
1173
+ function getCurrentComponent() {
1174
+ return componentsData.find(c => c.name === selectedComponentName) || componentsData[0];
1175
+ }
1176
+
1177
+ function getCurrentMachine() {
1178
+ const component = getCurrentComponent();
1179
+ if (!component || !selectedMachineName) return null;
1180
+ return component.stateMachines.find(m => m.name === selectedMachineName);
1181
+ }
1182
+
1183
+ function populateComponentSelector() {
1184
+ const select = document.getElementById('component-select');
1185
+ select.innerHTML = componentsData.map(c =>
1186
+ `<option value="${c.name}" ${c.name === selectedComponentName ? 'selected' : ''}>${c.name}</option>`
1187
+ ).join('');
1188
+ }
1189
+
1190
+ function populateMachineSelector() {
1191
+ const component = getCurrentComponent();
1192
+ const select = document.getElementById('machine-select');
1193
+
1194
+ if (!component || !component.stateMachines) {
1195
+ select.innerHTML = '<option value="">No machines available</option>';
833
1196
  return;
834
1197
  }
835
-
836
- container.innerHTML = instances.map(inst => `
837
- <div class="instance-item ${selectedInstance === inst.id ? 'selected' : ''}" onclick="selectInstance('${inst.id}')">
838
- <div class="instance-header">
839
- <span class="instance-id">${inst.id.substring(0, 8)}</span>
840
- <span class="badge ${inst.status}">${inst.currentState}</span>
841
- </div>
842
- <div class="instance-meta">
843
- Machine: ${inst.machineName}<br>
844
- Status: ${inst.status}
845
- </div>
846
- </div>
847
- `).join('');
1198
+
1199
+ select.innerHTML = '<option value="">Select a machine...</option>' +
1200
+ component.stateMachines.map(m =>
1201
+ `<option value="${m.name}">${m.name}${component.entryMachine === m.name ? ' (entry)' : ''}</option>`
1202
+ ).join('');
848
1203
  }
849
-
850
- function selectInstance(id) {
851
- selectedInstance = id;
852
- renderInstances();
853
- loadTrace();
854
- }
855
-
856
- // Filter Instances
857
- function filterInstances() {
858
- const idFilter = document.getElementById('filter-instance').value.toLowerCase();
859
- const machineFilter = document.getElementById('filter-machine').value;
860
- const stateFilter = document.getElementById('filter-state').value;
861
-
862
- document.querySelectorAll('.instance-item').forEach(item => {
863
- const id = item.querySelector('.instance-id').textContent.toLowerCase();
864
- const machine = item.querySelector('.instance-meta').textContent.includes(machineFilter);
865
- const state = item.querySelector('.badge').textContent.includes(stateFilter);
866
-
867
- const show = id.includes(idFilter) && (!machineFilter || machine) && (!stateFilter || state);
868
- item.style.display = show ? '' : 'none';
869
- });
1204
+
1205
+ function selectComponent() {
1206
+ selectedComponentName = document.getElementById('component-select').value;
1207
+ selectedMachineName = null;
1208
+ selectedInstanceId = null;
1209
+ populateMachineSelector();
1210
+ clearDiagram();
1211
+ clearHistory();
1212
+ clearInstanceList();
1213
+ clearTransitions();
1214
+ loadInstances();
1215
+
1216
+ if (currentView === 'component') {
1217
+ renderComponentView();
1218
+ }
870
1219
  }
871
-
872
- // Event Blotter
873
- function addEvent(event) {
874
- events.unshift(event);
875
- if (events.length > 200) events = events.slice(0, 200);
876
- renderBlotter();
877
- updateStats();
1220
+
1221
+ function selectMachine() {
1222
+ selectedMachineName = document.getElementById('machine-select').value;
1223
+ selectedInstanceId = null;
1224
+
1225
+ if (selectedMachineName) {
1226
+ renderDiagram();
1227
+ loadInstances();
1228
+ } else {
1229
+ clearDiagram();
1230
+ clearInstanceList();
1231
+ }
1232
+ clearHistory();
1233
+ clearTransitions();
878
1234
  }
879
-
880
- function renderBlotter() {
881
- const container = document.getElementById('event-blotter');
882
- if (events.length === 0) {
883
- container.innerHTML = '<div style="color: #8b949e; text-align: center; padding: 40px;">Waiting for events...</div>';
1235
+
1236
+ // Component View - Show all machines with inter-machine connections
1237
+ function renderComponentView() {
1238
+ const component = getCurrentComponent();
1239
+ if (!component) {
1240
+ document.getElementById('component-graph').innerHTML = `
1241
+ <div class="empty-state">
1242
+ <div class="empty-state-icon">&#127959;</div>
1243
+ <div>No component loaded</div>
1244
+ </div>`;
884
1245
  return;
885
1246
  }
886
-
887
- container.innerHTML = events.map(evt => `
888
- <div class="blotter-item ${evt.type}">
889
- <div class="blotter-time">${new Date(evt.timestamp).toLocaleTimeString()}</div>
890
- <div class="blotter-content">${evt.message}</div>
891
- ${evt.source ? `<div class="blotter-source">Source: ${evt.source}</div>` : ''}
892
- </div>
893
- `).join('');
894
- }
895
-
896
- function filterBlotter() {
897
- const filter = document.getElementById('blotter-filter').value.toLowerCase();
898
- const typeFilter = document.getElementById('blotter-type').value;
899
-
900
- document.querySelectorAll('.blotter-item').forEach(item => {
901
- const text = item.textContent.toLowerCase();
902
- const type = typeFilter ? item.classList.contains(typeFilter) : true;
903
- item.style.display = (text.includes(filter) && type) ? '' : 'none';
1247
+
1248
+ document.getElementById('component-title').innerHTML = `<span>${component.name}</span> - Component Overview`;
1249
+
1250
+ const machines = component.stateMachines || [];
1251
+ const container = document.getElementById('component-graph');
1252
+
1253
+ // Collect inter-machine transitions
1254
+ const interMachineLinks = [];
1255
+ machines.forEach(machine => {
1256
+ (machine.transitions || []).forEach(transition => {
1257
+ if (transition.type === 'inter_machine' && transition.targetMachine) {
1258
+ interMachineLinks.push({
1259
+ from: machine.name,
1260
+ to: transition.targetMachine,
1261
+ event: transition.event,
1262
+ fromState: transition.from
1263
+ });
1264
+ }
1265
+ });
1266
+ });
1267
+
1268
+ // Layout machines in a grid
1269
+ const cols = Math.ceil(Math.sqrt(machines.length));
1270
+ const cardWidth = 200;
1271
+ const cardHeight = 140;
1272
+ const spacingX = 120;
1273
+ const spacingY = 100;
1274
+
1275
+ // Calculate positions
1276
+ const positions = {};
1277
+ machines.forEach((machine, idx) => {
1278
+ const row = Math.floor(idx / cols);
1279
+ const col = idx % cols;
1280
+ positions[machine.name] = {
1281
+ x: col * (cardWidth + spacingX) + 40,
1282
+ y: row * (cardHeight + spacingY) + 40
1283
+ };
1284
+ });
1285
+
1286
+ // Build SVG for arrows
1287
+ const svgWidth = cols * (cardWidth + spacingX) + 100;
1288
+ const svgHeight = Math.ceil(machines.length / cols) * (cardHeight + spacingY) + 100;
1289
+
1290
+ let svgHtml = `<svg width="${svgWidth}" height="${svgHeight}" style="position: absolute; top: 0; left: 0; pointer-events: none; overflow: visible;">
1291
+ <defs>
1292
+ <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
1293
+ <polygon points="0 0, 10 3, 0 6" fill="#10b981" />
1294
+ </marker>
1295
+ </defs>`;
1296
+
1297
+ // Draw inter-machine arrows
1298
+ interMachineLinks.forEach((link, idx) => {
1299
+ const fromPos = positions[link.from];
1300
+ const toPos = positions[link.to];
1301
+ if (fromPos && toPos) {
1302
+ const x1 = fromPos.x + cardWidth / 2;
1303
+ const y1 = fromPos.y + cardHeight / 2;
1304
+ const x2 = toPos.x + cardWidth / 2;
1305
+ const y2 = toPos.y + cardHeight / 2;
1306
+
1307
+ // Calculate curve control point
1308
+ const midX = (x1 + x2) / 2;
1309
+ const midY = (y1 + y2) / 2;
1310
+ const offset = 30;
1311
+
1312
+ svgHtml += `
1313
+ <path d="M ${x1} ${y1} Q ${midX} ${midY - offset} ${x2} ${y2}"
1314
+ fill="none" stroke="#10b981" stroke-width="3"
1315
+ marker-end="url(#arrowhead)" />
1316
+ <text x="${midX}" y="${midY - offset - 8}" fill="#10b981" font-size="11"
1317
+ text-anchor="middle" font-weight="600">${link.event}</text>`;
1318
+ }
904
1319
  });
1320
+
1321
+ svgHtml += '</svg>';
1322
+
1323
+ // Build machine cards
1324
+ let cardsHtml = svgHtml;
1325
+ machines.forEach(machine => {
1326
+ const pos = positions[machine.name];
1327
+ const isEntry = component.entryMachine === machine.name;
1328
+ const machineInstances = allInstances.filter(i => i.machineName === machine.name);
1329
+
1330
+ cardsHtml += `
1331
+ <div class="machine-card ${isEntry ? 'entry-machine' : ''}"
1332
+ style="left: ${pos.x}px; top: ${pos.y}px; width: ${cardWidth}px;"
1333
+ onclick="selectMachineFromComponent('${machine.name}')">
1334
+ <div class="machine-card-header">
1335
+ <span class="machine-card-name">${machine.name}</span>
1336
+ ${isEntry ? '<span class="machine-card-badge">Entry</span>' : ''}
1337
+ </div>
1338
+ <div class="machine-card-stats">
1339
+ ${machine.states?.length || 0} states<br>
1340
+ ${machine.transitions?.length || 0} transitions
1341
+ </div>
1342
+ <div class="machine-card-instances">
1343
+ <strong>${machineInstances.length}</strong> instance${machineInstances.length !== 1 ? 's' : ''}
1344
+ </div>
1345
+ </div>`;
1346
+ });
1347
+
1348
+ container.innerHTML = cardsHtml;
1349
+ container.style.minHeight = `${svgHeight}px`;
905
1350
  }
906
-
907
- function clearBlotter() {
908
- events = [];
909
- renderBlotter();
910
- updateStats();
1351
+
1352
+ function selectMachineFromComponent(machineName) {
1353
+ selectedMachineName = machineName;
1354
+ document.getElementById('machine-select').value = machineName;
1355
+ setView('machine');
1356
+ renderDiagram();
1357
+ loadInstances();
911
1358
  }
912
-
913
- // Diagram
1359
+
1360
+ // Diagram Rendering
914
1361
  async function renderDiagram() {
915
- const machineName = document.getElementById('diagram-machine').value;
916
1362
  const component = getCurrentComponent();
917
- if (!machineName || !component) {
918
- document.getElementById('diagram-create-form').innerHTML = '<div class="empty-state" style="padding: 40px 20px;"><div class="empty-state-icon">👈</div><div>Select a state machine to create an instance</div></div>';
919
- document.getElementById('diagram-instances-list').innerHTML = '<div class="empty-state" style="padding: 20px;"><div class="empty-state-icon">📭</div><div>No instances for this state machine</div></div>';
920
- return;
1363
+ const machine = getCurrentMachine();
1364
+
1365
+ if (!component || !machine) return;
1366
+
1367
+ const container = document.getElementById('diagram-container');
1368
+ document.getElementById('diagram-title').innerHTML =
1369
+ `<span>${component.name}</span> / ${machine.name}`;
1370
+
1371
+ // Get current state if an instance is selected
1372
+ let currentState = null;
1373
+ if (selectedInstanceId) {
1374
+ const instance = instances.find(i => i.id === selectedInstanceId);
1375
+ if (instance) {
1376
+ currentState = instance.currentState;
1377
+ }
921
1378
  }
922
1379
 
923
- const machine = component.stateMachines.find(m => m.name === machineName);
924
- if (!machine) return;
1380
+ // Fetch diagram with current state highlighting
1381
+ const url = `/api/components/${component.name}/diagrams/${machine.name}` +
1382
+ (currentState ? `?currentState=${encodeURIComponent(currentState)}` : '');
925
1383
 
926
- // Fetch Mermaid diagram from API
927
- const res = await fetch(`/api/components/${component.name}/diagrams/${machineName}`);
1384
+ const res = await fetch(url);
928
1385
  const data = await res.json();
929
1386
 
930
1387
  if (data.diagram) {
931
- try {
932
- // Use Mermaid v10 API
933
- const container = document.getElementById('diagram-container');
934
- container.innerHTML = '<div class="mermaid">' + data.diagram + '</div>';
1388
+ terminalStates = new Set(data.terminalStates || []);
1389
+ currentTransitions = data.transitions || [];
935
1390
 
936
- // Re-render mermaid diagrams
1391
+ container.innerHTML = `<div class="mermaid">${data.diagram}</div>`;
1392
+
1393
+ try {
937
1394
  await mermaid.run({
938
1395
  querySelector: '#diagram-container .mermaid'
939
1396
  });
940
1397
  } catch (error) {
941
- console.error('Mermaid rendering error:', error);
942
- const container = document.getElementById('diagram-container');
943
- container.innerHTML = `
944
- <div style="color: #ef4444; padding: 20px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border: 1px solid #ef4444;">
945
- <strong>Diagram Rendering Error:</strong> ${error.message || 'Unknown error'}<br><br>
946
- <details style="margin-top: 10px;">
947
- <summary style="cursor: pointer;">Show diagram source</summary>
948
- <pre style="margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.3); border-radius: 4px; overflow-x: auto;">${data.diagram}</pre>
949
- </details>
950
- </div>
951
- `;
1398
+ console.error('Mermaid error:', error);
1399
+ container.innerHTML = `<div class="empty-state">
1400
+ <div class="empty-state-icon">&#9888;</div>
1401
+ <div>Error rendering diagram</div>
1402
+ </div>`;
952
1403
  }
953
1404
  }
1405
+ }
954
1406
 
955
- // Populate create instance form for this machine
956
- updateDiagramCreateForm(machine);
1407
+ function clearDiagram() {
1408
+ document.getElementById('diagram-container').innerHTML = `
1409
+ <div class="empty-state">
1410
+ <div class="empty-state-icon">&#128202;</div>
1411
+ <div>Select a state machine to view its diagram</div>
1412
+ </div>`;
1413
+ document.getElementById('diagram-title').textContent =
1414
+ 'Select a state machine to view its diagram';
1415
+ }
1416
+
1417
+ // Transitions Panel
1418
+ function loadTransitionsForInstance(instanceId) {
1419
+ const instance = instances.find(i => i.id === instanceId);
1420
+ if (!instance) {
1421
+ clearTransitions();
1422
+ return;
1423
+ }
957
1424
 
958
- // Show instances for this machine
959
- updateDiagramInstancesList(machineName);
1425
+ // Filter transitions that start from current state
1426
+ const availableTransitions = currentTransitions.filter(t => t.from === instance.currentState);
1427
+ renderTransitions(availableTransitions, instance);
960
1428
  }
961
1429
 
962
- function updateDiagramCreateForm(machine) {
963
- let html = '';
1430
+ function renderTransitions(transitions, instance) {
1431
+ const container = document.getElementById('transitions-list');
964
1432
 
965
- if (!machine.contextSchema) {
966
- html = `
967
- <div class="form-group">
968
- <label>Context (JSON)</label>
969
- <textarea id="diagram-context-json" rows="6" placeholder='{"property": "value"}'></textarea>
970
- <div class="form-help">Enter a JSON object with the context properties for this instance</div>
1433
+ if (!transitions || transitions.length === 0) {
1434
+ container.innerHTML = `
1435
+ <div class="empty-state" style="padding: 20px 0;">
1436
+ <div style="font-size: 12px;">No transitions available from current state</div>
1437
+ </div>`;
1438
+ return;
1439
+ }
1440
+
1441
+ container.innerHTML = transitions.map(t => `
1442
+ <div class="transition-item ${t.type === 'inter_machine' ? 'inter-machine' : ''}"
1443
+ onclick="sendTransitionEvent('${instance.id}', '${t.event}')">
1444
+ <div class="transition-info">
1445
+ <div class="transition-event">${t.event}</div>
1446
+ <div class="transition-path">${t.from} → ${t.to}</div>
1447
+ ${t.targetMachine ? `<div class="transition-target">Creates instance in: ${t.targetMachine}</div>` : ''}
971
1448
  </div>
972
- <button class="btn btn-primary" onclick="createInstanceFromDiagram()">Create Instance</button>
973
- `;
1449
+ <button class="transition-send-btn" onclick="event.stopPropagation(); sendTransitionEvent('${instance.id}', '${t.event}')">
1450
+ Send
1451
+ </button>
1452
+ </div>
1453
+ `).join('');
1454
+ }
1455
+
1456
+ function clearTransitions() {
1457
+ document.getElementById('transitions-list').innerHTML = `
1458
+ <div class="empty-state" style="padding: 20px 0;">
1459
+ <div style="font-size: 12px;">Select an instance to see transitions</div>
1460
+ </div>`;
1461
+ }
1462
+
1463
+ async function sendTransitionEvent(instanceId, eventType) {
1464
+ const component = getCurrentComponent();
1465
+ if (!component) return;
1466
+
1467
+ try {
1468
+ await fetch(`/api/components/${component.name}/instances/${instanceId}/events`, {
1469
+ method: 'POST',
1470
+ headers: { 'Content-Type': 'application/json' },
1471
+ body: JSON.stringify({ type: eventType, payload: {} })
1472
+ });
1473
+ showToast(`Event "${eventType}" sent`, 'success');
1474
+ } catch (error) {
1475
+ showToast('Failed to send event', 'error');
1476
+ }
1477
+ }
1478
+
1479
+ // Instance Management
1480
+ async function loadInstances() {
1481
+ const res = await fetch('/api/instances');
1482
+ const data = await res.json();
1483
+ allInstances = data.instances || [];
1484
+
1485
+ if (selectedMachineName) {
1486
+ instances = allInstances.filter(i => i.machineName === selectedMachineName);
974
1487
  } else {
975
- html = '<div style="background: rgba(251, 191, 36, 0.1); border-left: 3px solid #fbbf24; padding: 12px; margin-bottom: 20px; border-radius: 4px; color: rgba(255,255,255,0.9);"><strong>📝 Context Properties:</strong> Fill in the fields below</div>';
976
-
977
- for (const [key, field] of Object.entries(machine.contextSchema)) {
978
- html += `<div class="form-group">`;
979
- html += `<label for="diagram_ctx_${key}">${field.label || key}${field.required ? ' *' : ''}</label>`;
980
- if (field.type === 'select') {
981
- html += `<select id="diagram_ctx_${key}">`;
982
- field.options.forEach(opt => html += `<option value="${opt.value}">${opt.label}</option>`);
983
- html += `</select>`;
984
- } else {
985
- html += `<input type="${field.type || 'text'}" id="diagram_ctx_${key}" placeholder="${field.placeholder || ''}" ${field.required ? 'required' : ''}>`;
986
- }
987
- if (field.description) html += `<div class="form-help">${field.description}</div>`;
988
- html += `</div>`;
989
- }
990
- html += `<button class="btn btn-primary" onclick="createInstanceFromDiagram()">Create Instance</button>`;
1488
+ instances = [];
991
1489
  }
992
1490
 
993
- document.getElementById('diagram-create-form').innerHTML = html;
1491
+ renderInstanceList();
1492
+ updateStats();
1493
+
1494
+ if (currentView === 'component') {
1495
+ renderComponentView();
1496
+ }
994
1497
  }
995
1498
 
996
- function updateDiagramInstancesList(machineName) {
997
- const machineInstances = instances.filter(i => i.machineName === machineName);
1499
+ function renderInstanceList() {
1500
+ const container = document.getElementById('instance-list');
1501
+ const countEl = document.getElementById('instance-count');
998
1502
 
999
- if (machineInstances.length === 0) {
1000
- document.getElementById('diagram-instances-list').innerHTML = '<div class="empty-state" style="padding: 20px;"><div class="empty-state-icon">📭</div><div>No instances yet. Create one above!</div></div>';
1503
+ countEl.textContent = `${instances.length} instance${instances.length !== 1 ? 's' : ''}`;
1504
+
1505
+ if (instances.length === 0) {
1506
+ container.innerHTML = `
1507
+ <div class="empty-state">
1508
+ <div class="empty-state-icon">&#128230;</div>
1509
+ <div>No instances yet</div>
1510
+ </div>`;
1001
1511
  return;
1002
1512
  }
1003
1513
 
1004
- const html = machineInstances.map(inst => `
1005
- <div class="instance-item" onclick="selectInstance('${inst.id}'); switchTab('overview');" style="margin-bottom: 10px;">
1006
- <div class="instance-header">
1007
- <span class="instance-id">${inst.id.substring(0, 8)}</span>
1008
- <span class="badge ${inst.status}">${inst.currentState}</span>
1514
+ container.innerHTML = instances.map(inst => {
1515
+ const isTerminal = terminalStates.has(inst.currentState);
1516
+ const stateClass = inst.status === 'error' ? 'error' :
1517
+ (isTerminal ? 'terminal' : 'active');
1518
+
1519
+ return `
1520
+ <div class="instance-item ${selectedInstanceId === inst.id ? 'selected' : ''}"
1521
+ onclick="selectInstance('${inst.id}')">
1522
+ <div class="instance-header">
1523
+ <span class="instance-id">${inst.id.substring(0, 8)}</span>
1524
+ <span class="instance-state ${stateClass}">${inst.currentState}</span>
1525
+ </div>
1526
+ <div class="instance-meta">
1527
+ ${inst.status} &bull; ${new Date(inst.updatedAt).toLocaleTimeString()}
1528
+ </div>
1529
+ </div>`;
1530
+ }).join('');
1531
+ }
1532
+
1533
+ function clearInstanceList() {
1534
+ document.getElementById('instance-list').innerHTML = `
1535
+ <div class="empty-state">
1536
+ <div class="empty-state-icon">&#128230;</div>
1537
+ <div>Select a machine to see instances</div>
1538
+ </div>`;
1539
+ document.getElementById('instance-count').textContent = '0 instances';
1540
+ }
1541
+
1542
+ async function selectInstance(id) {
1543
+ selectedInstanceId = id;
1544
+ renderInstanceList();
1545
+ renderDiagram();
1546
+ loadInstanceHistory(id);
1547
+ loadTransitionsForInstance(id);
1548
+ }
1549
+
1550
+ function updateStats() {
1551
+ const total = instances.length;
1552
+ const terminalCount = instances.filter(i => terminalStates.has(i.currentState)).length;
1553
+ const activeCount = instances.filter(i => i.status === 'active' && !terminalStates.has(i.currentState)).length;
1554
+
1555
+ document.getElementById('stat-instances').textContent = total;
1556
+ document.getElementById('stat-active').textContent = activeCount;
1557
+ document.getElementById('stat-terminal').textContent = terminalCount;
1558
+ }
1559
+
1560
+ // Instance History (Traceability)
1561
+ async function loadInstanceHistory(instanceId) {
1562
+ const component = getCurrentComponent();
1563
+ if (!component) return;
1564
+
1565
+ const infoEl = document.getElementById('history-instance-info');
1566
+ infoEl.textContent = `- ${instanceId.substring(0, 8)}`;
1567
+
1568
+ try {
1569
+ const res = await fetch(`/api/instances/${instanceId}/history`);
1570
+ const data = await res.json();
1571
+
1572
+ if (data.history && data.history.length > 0) {
1573
+ renderHistory(data.history);
1574
+ } else {
1575
+ renderHistoryFromInstance(instanceId);
1576
+ }
1577
+ } catch (error) {
1578
+ renderHistoryFromInstance(instanceId);
1579
+ }
1580
+ }
1581
+
1582
+ function renderHistory(history) {
1583
+ const container = document.getElementById('history-timeline');
1584
+
1585
+ if (!history || history.length === 0) {
1586
+ container.innerHTML = '<div class="history-empty">No history available</div>';
1587
+ return;
1588
+ }
1589
+
1590
+ let html = '';
1591
+
1592
+ // Add initial state
1593
+ const firstEvent = history[0];
1594
+ if (firstEvent.stateBefore) {
1595
+ html += `
1596
+ <div class="history-node">
1597
+ <div class="history-state">${firstEvent.stateBefore}</div>
1598
+ <div class="history-time">Initial</div>
1599
+ </div>`;
1600
+ }
1601
+
1602
+ history.forEach((event, index) => {
1603
+ const isLast = index === history.length - 1;
1604
+ const isTerminal = terminalStates.has(event.stateAfter);
1605
+
1606
+ html += `
1607
+ <div class="history-arrow">
1608
+ <div class="history-arrow-line"></div>
1609
+ <div class="history-event">${event.event?.type || 'transition'}</div>
1009
1610
  </div>
1611
+ <div class="history-node">
1612
+ <div class="history-state ${isLast ? (isTerminal ? 'terminal' : 'current') : ''}">
1613
+ ${event.stateAfter}
1614
+ </div>
1615
+ <div class="history-time">${new Date(event.persistedAt).toLocaleTimeString()}</div>
1616
+ </div>`;
1617
+ });
1618
+
1619
+ container.innerHTML = html;
1620
+ }
1621
+
1622
+ function renderHistoryFromInstance(instanceId) {
1623
+ const instance = instances.find(i => i.id === instanceId);
1624
+ if (!instance) {
1625
+ document.getElementById('history-timeline').innerHTML =
1626
+ '<div class="history-empty">Instance not found</div>';
1627
+ return;
1628
+ }
1629
+
1630
+ const machine = getCurrentMachine();
1631
+ const isTerminal = terminalStates.has(instance.currentState);
1632
+
1633
+ document.getElementById('history-timeline').innerHTML = `
1634
+ <div class="history-node">
1635
+ <div class="history-state">${machine?.initialState || 'initial'}</div>
1636
+ <div class="history-time">Created</div>
1010
1637
  </div>
1011
- `).join('');
1638
+ <div class="history-arrow">
1639
+ <div class="history-arrow-line"></div>
1640
+ <div class="history-event">...</div>
1641
+ </div>
1642
+ <div class="history-node">
1643
+ <div class="history-state ${isTerminal ? 'terminal' : 'current'}">${instance.currentState}</div>
1644
+ <div class="history-time">${new Date(instance.updatedAt).toLocaleTimeString()}</div>
1645
+ </div>`;
1646
+ }
1647
+
1648
+ function clearHistory() {
1649
+ document.getElementById('history-timeline').innerHTML =
1650
+ '<div class="history-empty">Select an instance to view its transition history</div>';
1651
+ document.getElementById('history-instance-info').textContent = '';
1652
+ document.getElementById('history-actions').innerHTML = '';
1653
+ }
1654
+
1655
+ // Create Instance Modal
1656
+ function openCreateModal() {
1657
+ if (!selectedMachineName) {
1658
+ showToast('Please select a machine first', 'error');
1659
+ return;
1660
+ }
1661
+
1662
+ const machine = getCurrentMachine();
1663
+ if (machine?.contextSchema) {
1664
+ renderSchemaForm(machine.contextSchema);
1665
+ } else {
1666
+ document.getElementById('create-form-content').innerHTML = `
1667
+ <div class="form-group">
1668
+ <label>Context (JSON)</label>
1669
+ <textarea id="create-context" placeholder='{"property": "value"}'></textarea>
1670
+ <div class="form-help">Enter a JSON object with context properties</div>
1671
+ </div>`;
1672
+ }
1012
1673
 
1013
- document.getElementById('diagram-instances-list').innerHTML = html;
1674
+ document.getElementById('create-modal').classList.add('active');
1014
1675
  }
1015
1676
 
1016
- async function createInstanceFromDiagram() {
1017
- const machineName = document.getElementById('diagram-machine').value;
1677
+ function renderSchemaForm(schema) {
1678
+ let html = '';
1679
+ for (const [key, field] of Object.entries(schema)) {
1680
+ html += `<div class="form-group">
1681
+ <label>${field.label || key}${field.required ? ' *' : ''}</label>`;
1682
+
1683
+ if (field.type === 'select') {
1684
+ html += `<select class="selector" id="ctx_${key}">
1685
+ ${field.options.map(o => `<option value="${o.value}">${o.label}</option>`).join('')}
1686
+ </select>`;
1687
+ } else {
1688
+ html += `<input type="${field.type || 'text'}" id="ctx_${key}"
1689
+ placeholder="${field.placeholder || ''}" ${field.required ? 'required' : ''}>`;
1690
+ }
1691
+
1692
+ if (field.description) {
1693
+ html += `<div class="form-help">${field.description}</div>`;
1694
+ }
1695
+ html += '</div>';
1696
+ }
1697
+ document.getElementById('create-form-content').innerHTML = html;
1698
+ }
1699
+
1700
+ function closeCreateModal() {
1701
+ document.getElementById('create-modal').classList.remove('active');
1702
+ }
1703
+
1704
+ async function createInstance() {
1018
1705
  const component = getCurrentComponent();
1019
- if (!machineName || !component) return alert('Please select a state machine');
1706
+ const machine = getCurrentMachine();
1707
+ if (!component || !machine) return;
1020
1708
 
1021
- const machine = component.stateMachines.find(m => m.name === machineName);
1022
1709
  let context = {};
1023
1710
 
1024
- if (machine?.contextSchema) {
1711
+ if (machine.contextSchema) {
1025
1712
  for (const key of Object.keys(machine.contextSchema)) {
1026
- const input = document.getElementById('diagram_ctx_' + key);
1713
+ const input = document.getElementById('ctx_' + key);
1027
1714
  if (input && input.value) {
1028
1715
  context[key] = input.type === 'number' ? parseFloat(input.value) : input.value;
1029
1716
  }
1030
1717
  }
1031
1718
  } else {
1032
- const json = document.getElementById('diagram-context-json')?.value;
1033
- if (json) {
1719
+ const jsonInput = document.getElementById('create-context');
1720
+ if (jsonInput && jsonInput.value) {
1034
1721
  try {
1035
- context = JSON.parse(json);
1722
+ context = JSON.parse(jsonInput.value);
1036
1723
  } catch (e) {
1037
- return alert('Invalid JSON: ' + e.message);
1724
+ showToast('Invalid JSON: ' + e.message, 'error');
1725
+ return;
1038
1726
  }
1039
1727
  }
1040
1728
  }
1041
1729
 
1042
- await fetch(`/api/components/${component.name}/instances`, {
1043
- method: 'POST',
1044
- headers: {'Content-Type': 'application/json'},
1045
- body: JSON.stringify({machineName, context})
1046
- });
1047
-
1048
- // Clear form
1049
- if (machine?.contextSchema) {
1050
- for (const key of Object.keys(machine.contextSchema)) {
1051
- const input = document.getElementById('diagram_ctx_' + key);
1052
- if (input) input.value = '';
1053
- }
1054
- } else {
1055
- const textarea = document.getElementById('diagram-context-json');
1056
- if (textarea) textarea.value = '';
1730
+ try {
1731
+ await fetch(`/api/components/${component.name}/instances`, {
1732
+ method: 'POST',
1733
+ headers: { 'Content-Type': 'application/json' },
1734
+ body: JSON.stringify({ machineName: machine.name, context })
1735
+ });
1736
+ closeCreateModal();
1737
+ } catch (error) {
1738
+ showToast('Failed to create instance', 'error');
1057
1739
  }
1740
+ }
1058
1741
 
1059
- // Reload instances to update the list
1060
- loadInstances();
1742
+ // Send Event Modal
1743
+ let eventTargetInstanceId = null;
1744
+
1745
+ function openEventModal(instanceId) {
1746
+ eventTargetInstanceId = instanceId;
1747
+ document.getElementById('event-type').value = '';
1748
+ document.getElementById('event-payload').value = '';
1749
+ document.getElementById('event-modal').classList.add('active');
1750
+ }
1751
+
1752
+ function closeEventModal() {
1753
+ document.getElementById('event-modal').classList.remove('active');
1754
+ eventTargetInstanceId = null;
1061
1755
  }
1062
-
1063
- // Traceability
1064
- async function loadTrace() {
1065
- if (!selectedInstance) return;
1066
-
1067
- const container = document.getElementById('trace-container');
1068
- // In real version, fetch from /api/instances/:id/history
1069
- container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔍</div><div>Traceability history would appear here</div></div>';
1070
- }
1071
-
1072
- function updateTraceSelector() {
1073
- const select = document.getElementById('trace-instance');
1074
- select.innerHTML = '<option value="">Select an instance...</option>' +
1075
- instances.map(i => `<option value="${i.id}">${i.id.substring(0, 8)} - ${i.machineName}</option>`).join('');
1076
- }
1077
-
1078
-
1079
- function populateSelectors() {
1080
- if (!componentsData || componentsData.length === 0) return;
1081
-
1082
- // Populate component selector with all loaded components
1083
- const componentSelect = document.getElementById('component-select');
1084
- componentSelect.innerHTML = componentsData.map(c =>
1085
- `<option value="${c.name}" ${c.name === selectedComponentName ? 'selected' : ''}>${c.name}</option>`
1086
- ).join('');
1087
1756
 
1088
- // Populate state machine selectors for current component
1757
+ async function sendEvent() {
1758
+ if (!eventTargetInstanceId) return;
1759
+
1089
1760
  const component = getCurrentComponent();
1090
- if (component && component.stateMachines) {
1091
- const machines = component.stateMachines.map(m => `<option value="${m.name}">${m.name}</option>`).join('');
1092
- document.getElementById('filter-machine').innerHTML = '<option value="">All Machines</option>' + machines;
1093
- document.getElementById('diagram-machine').innerHTML = '<option value="">Select...</option>' + machines;
1761
+ if (!component) return;
1762
+
1763
+ const eventType = document.getElementById('event-type').value;
1764
+ if (!eventType) {
1765
+ showToast('Event type is required', 'error');
1766
+ return;
1767
+ }
1768
+
1769
+ let payload = {};
1770
+ const payloadInput = document.getElementById('event-payload').value;
1771
+ if (payloadInput) {
1772
+ try {
1773
+ payload = JSON.parse(payloadInput);
1774
+ } catch (e) {
1775
+ showToast('Invalid JSON payload', 'error');
1776
+ return;
1777
+ }
1778
+ }
1779
+
1780
+ try {
1781
+ await fetch(`/api/components/${component.name}/instances/${eventTargetInstanceId}/events`, {
1782
+ method: 'POST',
1783
+ headers: { 'Content-Type': 'application/json' },
1784
+ body: JSON.stringify({ type: eventType, payload })
1785
+ });
1786
+ closeEventModal();
1787
+ showToast(`Event "${eventType}" sent`, 'success');
1788
+ } catch (error) {
1789
+ showToast('Failed to send event', 'error');
1094
1790
  }
1095
1791
  }
1096
1792
 
1097
- function selectComponent() {
1098
- const selected = document.getElementById('component-select').value;
1099
- selectedComponentName = selected;
1100
- console.log('Component selected:', selected);
1101
- // Refresh selectors for the new component
1102
- populateSelectors();
1103
- // Clear diagram create form when switching components
1104
- document.getElementById('diagram-create-form').innerHTML = '<div class="empty-state" style="padding: 40px 20px;"><div class="empty-state-icon">👈</div><div>Select a state machine to create an instance</div></div>';
1105
- }
1106
-
1107
- function exportState() {
1108
- const state = {componentData, instances, events, timestamp: Date.now()};
1109
- const blob = new Blob([JSON.stringify(state, null, 2)], {type: 'application/json'});
1110
- const url = URL.createObjectURL(blob);
1111
- const a = document.createElement('a');
1112
- a.href = url;
1113
- a.download = `xcomponent-state-${Date.now()}.json`;
1114
- a.click();
1115
- }
1116
-
1793
+ // Toast Notifications
1794
+ function showToast(message, type = 'info') {
1795
+ const container = document.getElementById('toast-container');
1796
+ const toast = document.createElement('div');
1797
+ toast.className = `toast ${type}`;
1798
+ toast.textContent = message;
1799
+ container.appendChild(toast);
1800
+
1801
+ setTimeout(() => {
1802
+ toast.style.opacity = '0';
1803
+ setTimeout(() => toast.remove(), 300);
1804
+ }, 3000);
1805
+ }
1806
+
1117
1807
  // Initialize
1118
1808
  loadInstances();
1119
- mermaid.initialize({startOnLoad: true, theme: 'default'});
1120
1809
  </script>
1121
1810
  </body>
1122
1811
  </html>