chansung commited on
Commit
3d77df7
·
verified ·
1 Parent(s): cb98e5d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1349 -19
index.html CHANGED
@@ -1,19 +1,1349 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>dstack Configuration Editor</title>
7
+
8
+ <!-- Tailwind CSS CDN -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <!-- Inter Font -->
12
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
13
+
14
+ <!-- JS-YAML CDN -->
15
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js-yaml.min.js"></script>
16
+
17
+ <style>
18
+ /* Modern styling with animations and gradients */
19
+ html {
20
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
21
+ scroll-behavior: smooth;
22
+ }
23
+ @supports (font-variation-settings: normal) {
24
+ html { font-family: 'Inter var', system-ui, -apple-system, sans-serif; }
25
+ }
26
+
27
+ body {
28
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
29
+ background-attachment: fixed;
30
+ color: #1e293b;
31
+ min-height: 100vh;
32
+ position: relative;
33
+ }
34
+
35
+ /* Enhanced animated background with multiple layers */
36
+ body::after {
37
+ content: '';
38
+ position: fixed;
39
+ top: 0;
40
+ left: 0;
41
+ width: 100%;
42
+ height: 100%;
43
+ background:
44
+ radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
45
+ radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
46
+ radial-gradient(circle at 40% 40%, rgba(120, 200, 255, 0.2) 0%, transparent 50%);
47
+ pointer-events: none;
48
+ z-index: -1;
49
+ animation: float 20s ease-in-out infinite;
50
+ }
51
+
52
+ /* Animated background pattern */
53
+ body::before {
54
+ content: '';
55
+ position: fixed;
56
+ top: 0;
57
+ left: 0;
58
+ width: 100%;
59
+ height: 100%;
60
+ background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
61
+ pointer-events: none;
62
+ z-index: -1;
63
+ }
64
+
65
+ /* Beautiful custom scrollbar */
66
+ ::-webkit-scrollbar {
67
+ width: 12px;
68
+ height: 12px;
69
+ }
70
+ ::-webkit-scrollbar-track {
71
+ background: rgba(148, 163, 184, 0.1);
72
+ border-radius: 10px;
73
+ }
74
+ ::-webkit-scrollbar-thumb {
75
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
76
+ border-radius: 10px;
77
+ border: 2px solid transparent;
78
+ background-clip: content-box;
79
+ }
80
+ ::-webkit-scrollbar-thumb:hover {
81
+ background: linear-gradient(135deg, #4f46e5, #7c3aed);
82
+ background-clip: content-box;
83
+ }
84
+
85
+ /* Enhanced Glassmorphism effects */
86
+ .glass {
87
+ background: linear-gradient(135deg,
88
+ rgba(255, 255, 255, 0.1) 0%,
89
+ rgba(255, 255, 255, 0.05) 100%);
90
+ backdrop-filter: blur(20px) saturate(180%);
91
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
92
+ border: 1px solid rgba(255, 255, 255, 0.3);
93
+ box-shadow:
94
+ 0 8px 32px rgba(31, 38, 135, 0.37),
95
+ inset 0 1px 0 rgba(255, 255, 255, 0.5),
96
+ inset 0 -1px 0 rgba(255, 255, 255, 0.1);
97
+ position: relative;
98
+ overflow: hidden;
99
+ }
100
+
101
+ .glass::before {
102
+ content: '';
103
+ position: absolute;
104
+ top: 0;
105
+ left: -100%;
106
+ width: 100%;
107
+ height: 100%;
108
+ background: linear-gradient(90deg,
109
+ transparent,
110
+ rgba(255, 255, 255, 0.1),
111
+ transparent);
112
+ animation: glass-shine 3s infinite;
113
+ pointer-events: none;
114
+ }
115
+
116
+ @keyframes glass-shine {
117
+ 0% { left: -100%; }
118
+ 50% { left: 100%; }
119
+ 100% { left: 100%; }
120
+ }
121
+
122
+ .glass-dark {
123
+ background: linear-gradient(135deg,
124
+ rgba(15, 23, 42, 0.7) 0%,
125
+ rgba(15, 23, 42, 0.5) 100%);
126
+ backdrop-filter: blur(25px) saturate(200%);
127
+ -webkit-backdrop-filter: blur(25px) saturate(200%);
128
+ border: 1px solid rgba(255, 255, 255, 0.2);
129
+ box-shadow:
130
+ 0 8px 32px rgba(0, 0, 0, 0.3),
131
+ inset 0 1px 0 rgba(255, 255, 255, 0.3),
132
+ inset 0 -1px 0 rgba(255, 255, 255, 0.1);
133
+ position: relative;
134
+ overflow: hidden;
135
+ }
136
+
137
+ .glass-dark::before {
138
+ content: '';
139
+ position: absolute;
140
+ top: 0;
141
+ left: -100%;
142
+ width: 100%;
143
+ height: 100%;
144
+ background: linear-gradient(90deg,
145
+ transparent,
146
+ rgba(255, 255, 255, 0.05),
147
+ transparent);
148
+ animation: glass-shine 4s infinite;
149
+ pointer-events: none;
150
+ }
151
+
152
+ /* Smooth animations */
153
+ * {
154
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
155
+ }
156
+
157
+ /* Glowing effects */
158
+ .glow {
159
+ box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
160
+ }
161
+
162
+ .glow:hover {
163
+ box-shadow: 0 0 30px rgba(99, 102, 241, 0.5);
164
+ transform: translateY(-2px);
165
+ }
166
+
167
+ /* Modern button styles */
168
+ .btn-primary {
169
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
170
+ border: none;
171
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
172
+ }
173
+
174
+ .btn-primary:hover {
175
+ background: linear-gradient(135deg, #4f46e5, #7c3aed);
176
+ box-shadow: 0 6px 20px rgba(99, 102, 241, 0.6);
177
+ transform: translateY(-1px);
178
+ }
179
+
180
+ .btn-secondary {
181
+ background: rgba(255, 255, 255, 0.2);
182
+ backdrop-filter: blur(10px);
183
+ border: 1px solid rgba(255, 255, 255, 0.3);
184
+ color: white;
185
+ }
186
+
187
+ .btn-secondary:hover {
188
+ background: rgba(255, 255, 255, 0.3);
189
+ transform: translateY(-1px);
190
+ }
191
+
192
+ /* Enhanced form input with better readability */
193
+ .form-input {
194
+ background: linear-gradient(135deg,
195
+ rgba(255, 255, 255, 0.15) 0%,
196
+ rgba(255, 255, 255, 0.1) 100%);
197
+ border: 2px solid rgba(255, 255, 255, 0.4);
198
+ backdrop-filter: blur(10px) saturate(180%);
199
+ -webkit-backdrop-filter: blur(10px) saturate(180%);
200
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
201
+ color: white;
202
+ font-weight: 500;
203
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
204
+ box-shadow:
205
+ 0 4px 16px rgba(31, 38, 135, 0.2),
206
+ inset 0 1px 0 rgba(255, 255, 255, 0.4);
207
+ position: relative;
208
+ }
209
+
210
+ .form-input:focus {
211
+ background: linear-gradient(135deg,
212
+ rgba(255, 255, 255, 0.25) 0%,
213
+ rgba(255, 255, 255, 0.18) 100%);
214
+ border-color: rgba(99, 102, 241, 0.9);
215
+ box-shadow:
216
+ 0 0 0 4px rgba(99, 102, 241, 0.2),
217
+ 0 8px 32px rgba(99, 102, 241, 0.3),
218
+ inset 0 1px 0 rgba(255, 255, 255, 0.5);
219
+ transform: translateY(-2px) scale(1.02);
220
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
221
+ }
222
+
223
+ /* Enhanced dropdown with better readability */
224
+ .form-select {
225
+ background: linear-gradient(135deg,
226
+ rgba(255, 255, 255, 0.15) 0%,
227
+ rgba(255, 255, 255, 0.1) 100%);
228
+ border: 2px solid rgba(255, 255, 255, 0.4);
229
+ backdrop-filter: blur(10px) saturate(180%);
230
+ -webkit-backdrop-filter: blur(10px) saturate(180%);
231
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
232
+ color: white;
233
+ font-weight: 500;
234
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
235
+ box-shadow:
236
+ 0 4px 16px rgba(31, 38, 135, 0.2),
237
+ inset 0 1px 0 rgba(255, 255, 255, 0.4);
238
+ }
239
+
240
+ .form-select:focus {
241
+ background: linear-gradient(135deg,
242
+ rgba(255, 255, 255, 0.25) 0%,
243
+ rgba(255, 255, 255, 0.18) 100%);
244
+ border-color: rgba(99, 102, 241, 0.9);
245
+ box-shadow:
246
+ 0 0 0 4px rgba(99, 102, 241, 0.2),
247
+ 0 8px 32px rgba(99, 102, 241, 0.3),
248
+ inset 0 1px 0 rgba(255, 255, 255, 0.5);
249
+ transform: translateY(-2px) scale(1.02);
250
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
251
+ outline: none;
252
+ }
253
+
254
+ .form-select option {
255
+ background: rgba(15, 23, 42, 0.95);
256
+ color: white;
257
+ padding: 8px 12px;
258
+ }
259
+
260
+ .form-select option:checked {
261
+ background: rgba(99, 102, 241, 0.8);
262
+ }
263
+
264
+ .form-select option:hover {
265
+ background: rgba(99, 102, 241, 0.6);
266
+ }
267
+
268
+ /* Remove default backgrounds from grouped components */
269
+ fieldset {
270
+ background-color: transparent !important;
271
+ background: none !important;
272
+ }
273
+
274
+ /* Override glass background for fieldsets - they should be transparent containers */
275
+ fieldset.glass {
276
+ background: transparent !important;
277
+ background-color: transparent !important;
278
+ backdrop-filter: none !important;
279
+ -webkit-backdrop-filter: none !important;
280
+ box-shadow: none !important;
281
+ border: 1px solid rgba(255, 255, 255, 0.2) !important;
282
+ }
283
+
284
+ /* Keep fieldsets completely transparent even on hover */
285
+ fieldset.glass:hover {
286
+ background: transparent !important;
287
+ backdrop-filter: none !important;
288
+ -webkit-backdrop-filter: none !important;
289
+ }
290
+
291
+ /* Ensure legends have proper glass backgrounds */
292
+ fieldset legend {
293
+ background-color: transparent;
294
+ }
295
+
296
+ /* Global text readability improvements */
297
+ body, * {
298
+ -webkit-font-smoothing: antialiased;
299
+ -moz-osx-font-smoothing: grayscale;
300
+ }
301
+
302
+ /* Better text contrast for all white text */
303
+ .text-white {
304
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
305
+ }
306
+
307
+ /* Enhanced placeholder text */
308
+ ::placeholder {
309
+ color: rgba(255, 255, 255, 0.6);
310
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
311
+ }
312
+
313
+ /* Smooth collapsible animations */
314
+ .fieldset-content {
315
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
316
+ overflow: hidden;
317
+ transform-origin: top;
318
+ }
319
+
320
+ .fieldset-content.collapsed {
321
+ max-height: 0;
322
+ opacity: 0;
323
+ transform: scaleY(0);
324
+ margin-top: 0;
325
+ padding-top: 0;
326
+ padding-bottom: 0;
327
+ }
328
+
329
+ .fieldset-content.expanded {
330
+ max-height: 1000px;
331
+ opacity: 1;
332
+ transform: scaleY(1);
333
+ }
334
+
335
+ /* Arrow rotation animation */
336
+ .collapse-arrow {
337
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
338
+ }
339
+
340
+ .collapse-arrow.collapsed {
341
+ transform: rotate(-90deg);
342
+ }
343
+
344
+ .collapse-arrow.expanded {
345
+ transform: rotate(0deg);
346
+ }
347
+
348
+ /* Fieldset entrance animation */
349
+ @keyframes slideInUp {
350
+ from {
351
+ opacity: 0;
352
+ transform: translateY(20px) scale(0.95);
353
+ }
354
+ to {
355
+ opacity: 1;
356
+ transform: translateY(0) scale(1);
357
+ }
358
+ }
359
+
360
+ .fieldset-enter {
361
+ animation: slideInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
362
+ }
363
+
364
+ /* Legend hover animation */
365
+ .legend-hover {
366
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
367
+ }
368
+
369
+ /* Preset buttons styling */
370
+ .glass-preset {
371
+ background: linear-gradient(135deg,
372
+ rgba(255, 255, 255, 0.1) 0%,
373
+ rgba(255, 255, 255, 0.05) 100%);
374
+ backdrop-filter: blur(10px) saturate(180%);
375
+ -webkit-backdrop-filter: blur(10px) saturate(180%);
376
+ box-shadow:
377
+ 0 4px 16px rgba(31, 38, 135, 0.2),
378
+ inset 0 1px 0 rgba(255, 255, 255, 0.3);
379
+ }
380
+
381
+ .glass-preset:hover {
382
+ background: linear-gradient(135deg,
383
+ rgba(255, 255, 255, 0.15) 0%,
384
+ rgba(255, 255, 255, 0.08) 100%);
385
+ box-shadow:
386
+ 0 6px 20px rgba(31, 38, 135, 0.3),
387
+ 0 0 0 2px rgba(255, 255, 255, 0.2),
388
+ inset 0 1px 0 rgba(255, 255, 255, 0.4);
389
+ transform: translateY(-2px) scale(1.05);
390
+ }
391
+
392
+ .glass-preset:active {
393
+ transform: translateY(0) scale(1.02);
394
+ box-shadow:
395
+ 0 2px 8px rgba(31, 38, 135, 0.4),
396
+ inset 0 1px 0 rgba(255, 255, 255, 0.5);
397
+ }
398
+
399
+ /* Summary preview styling */
400
+ .summary-preview {
401
+ opacity: 0;
402
+ max-height: 0;
403
+ overflow: hidden;
404
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
405
+ font-size: 0.75rem;
406
+ color: rgba(255, 255, 255, 0.7);
407
+ font-weight: 400;
408
+ margin-top: 0;
409
+ }
410
+
411
+ .summary-preview.show {
412
+ opacity: 1;
413
+ max-height: 100px;
414
+ margin-top: 0.5rem;
415
+ }
416
+
417
+ /* Animated status indicator */
418
+ .status-indicator {
419
+ position: relative;
420
+ overflow: hidden;
421
+ }
422
+
423
+ .status-indicator::before {
424
+ content: '';
425
+ position: absolute;
426
+ top: 0;
427
+ left: -100%;
428
+ width: 100%;
429
+ height: 100%;
430
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
431
+ animation: shimmer 2s infinite;
432
+ }
433
+
434
+ @keyframes shimmer {
435
+ 0% { left: -100%; }
436
+ 100% { left: 100%; }
437
+ }
438
+
439
+ /* Floating animation */
440
+ @keyframes float {
441
+ 0%, 100% { transform: translateY(0px); }
442
+ 50% { transform: translateY(-10px); }
443
+ }
444
+
445
+ .float {
446
+ animation: float 6s ease-in-out infinite;
447
+ }
448
+
449
+ /* Gradient text */
450
+ .gradient-text {
451
+ background: linear-gradient(135deg, #6366f1, #8b5cf6, #ec4899);
452
+ -webkit-background-clip: text;
453
+ -webkit-text-fill-color: transparent;
454
+ background-clip: text;
455
+ }
456
+
457
+ /* Enhanced card hover effects with glass morphism */
458
+ .card {
459
+ transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
460
+ position: relative;
461
+ }
462
+
463
+ .card:hover {
464
+ transform: translateY(-12px) scale(1.03);
465
+ box-shadow:
466
+ 0 32px 64px rgba(31, 38, 135, 0.4),
467
+ 0 16px 32px rgba(99, 102, 241, 0.2),
468
+ inset 0 2px 0 rgba(255, 255, 255, 0.6);
469
+ }
470
+
471
+ .card:hover .glass {
472
+ background: linear-gradient(135deg,
473
+ rgba(255, 255, 255, 0.15) 0%,
474
+ rgba(255, 255, 255, 0.08) 100%);
475
+ border-color: rgba(255, 255, 255, 0.4);
476
+ }
477
+
478
+ .card:hover .glass-dark {
479
+ background: linear-gradient(135deg,
480
+ rgba(15, 23, 42, 0.6) 0%,
481
+ rgba(15, 23, 42, 0.4) 100%);
482
+ border-color: rgba(255, 255, 255, 0.3);
483
+ }
484
+ </style>
485
+ </head>
486
+ <body class="h-full antialiased">
487
+
488
+ <div id="app" class="flex flex-col h-full">
489
+ <!-- Header Section -->
490
+ <header class="glass sticky top-0 z-20 border-b border-white/20">
491
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
492
+ <div class="flex items-center justify-between h-20">
493
+ <div class="flex items-center space-x-4">
494
+ <div class="relative">
495
+ <svg class="h-10 w-10 text-white drop-shadow-lg float" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
496
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3V6a3 3 0 013-3h13.5a3 3 0 013 3v5.25a3 3 0 01-3 3m-13.5 0V9a1.5 1.5 0 011.5-1.5h13.5A1.5 1.5 0 0121 9v5.25m0 0H8.25a2.25 2.25 0 00-2.25 2.25V18a3 3 0 003 3h6a3 3 0 003-3v-1.5a2.25 2.25 0 00-2.25-2.25H12" />
497
+ </svg>
498
+ <div class="absolute -top-1 -right-1 w-4 h-4 bg-gradient-to-r from-pink-500 to-violet-500 rounded-full animate-pulse"></div>
499
+ </div>
500
+ <div>
501
+ <h1 class="text-2xl font-bold text-white drop-shadow-lg gradient-text">dstack</h1>
502
+ <p class="text-sm text-white/80 font-medium">Configuration Editor</p>
503
+ </div>
504
+ </div>
505
+ <div class="flex items-center space-x-3">
506
+ <div class="relative">
507
+ <select id="templateSelector" class="form-select px-4 py-2.5 pr-10 text-sm font-semibold rounded-xl transition-all cursor-pointer appearance-none">
508
+ <option value="">Select Template</option>
509
+ <option value="task">Task</option>
510
+ <option value="service">Service</option>
511
+ <option value="dev-environment">Dev Environment</option>
512
+ </select>
513
+ <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
514
+ <svg class="h-5 w-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
515
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
516
+ </svg>
517
+ </div>
518
+ </div>
519
+ <button id="clearBtn" class="btn-primary px-6 py-2.5 text-sm font-semibold text-white rounded-xl transition-all">
520
+ Clear All
521
+ </button>
522
+ </div>
523
+ </div>
524
+ </div>
525
+ </header>
526
+
527
+ <!-- Preset Configuration Buttons -->
528
+ <section class="border-b border-white/10 bg-gradient-to-r from-white/5 to-white/2 backdrop-blur-sm">
529
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
530
+ <div class="flex items-center space-x-2 mb-3">
531
+ <svg class="w-5 h-5 text-white/70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
532
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
533
+ </svg>
534
+ <h3 class="text-sm font-semibold text-white/80" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">Quick Start Templates</h3>
535
+ </div>
536
+ <div class="flex flex-wrap gap-3">
537
+ <button id="preset-llm-training" class="preset-btn glass-preset px-4 py-2.5 text-sm font-medium text-white rounded-xl transition-all hover:scale-105 border border-white/20">
538
+ <div class="flex items-center space-x-2">
539
+ <span class="text-lg">🤖</span>
540
+ <span>LLM Training</span>
541
+ </div>
542
+ </button>
543
+ <button id="preset-model-serving" class="preset-btn glass-preset px-4 py-2.5 text-sm font-medium text-white rounded-xl transition-all hover:scale-105 border border-white/20">
544
+ <div class="flex items-center space-x-2">
545
+ <span class="text-lg">🚀</span>
546
+ <span>Model Serving</span>
547
+ </div>
548
+ </button>
549
+ <button id="preset-jupyter-dev" class="preset-btn glass-preset px-4 py-2.5 text-sm font-medium text-white rounded-xl transition-all hover:scale-105 border border-white/20">
550
+ <div class="flex items-center space-x-2">
551
+ <span class="text-lg">📊</span>
552
+ <span>Jupyter Dev</span>
553
+ </div>
554
+ </button>
555
+ <button id="preset-data-processing" class="preset-btn glass-preset px-4 py-2.5 text-sm font-medium text-white rounded-xl transition-all hover:scale-105 border border-white/20">
556
+ <div class="flex items-center space-x-2">
557
+ <span class="text-lg">⚡</span>
558
+ <span>Data Processing</span>
559
+ </div>
560
+ </button>
561
+ <button id="preset-web-app" class="preset-btn glass-preset px-4 py-2.5 text-sm font-medium text-white rounded-xl transition-all hover:scale-105 border border-white/20">
562
+ <div class="flex items-center space-x-2">
563
+ <span class="text-lg">🌐</span>
564
+ <span>Web App</span>
565
+ </div>
566
+ </button>
567
+ <button id="preset-gpu-cluster" class="preset-btn glass-preset px-4 py-2.5 text-sm font-medium text-white rounded-xl transition-all hover:scale-105 border border-white/20">
568
+ <div class="flex items-center space-x-2">
569
+ <span class="text-lg">🔥</span>
570
+ <span>GPU Cluster</span>
571
+ </div>
572
+ </button>
573
+ </div>
574
+ </div>
575
+ </section>
576
+
577
+ <!-- Main Content: Side-by-side editors -->
578
+ <main class="flex-grow p-6 sm:p-8 lg:p-10 grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-7xl mx-auto w-full">
579
+
580
+ <!-- Left Side: Form-based UI Editor -->
581
+ <div class="flex flex-col glass rounded-2xl shadow-2xl ring-1 ring-white/20 overflow-hidden card glow">
582
+ <div class="flex-shrink-0 bg-gradient-to-r from-violet-500/20 to-purple-500/20 border-b border-white/20 px-8 py-6">
583
+ <div class="flex items-center justify-between">
584
+ <div class="flex items-center space-x-3">
585
+ <div class="w-3 h-3 bg-gradient-to-r from-green-400 to-blue-500 rounded-full animate-pulse"></div>
586
+ <h2 class="text-xl font-bold text-white drop-shadow-lg">Visual Editor</h2>
587
+ </div>
588
+ <div class="flex items-center space-x-2">
589
+ <div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
590
+ <span class="text-xs text-white/70 font-medium">Live Preview</span>
591
+ </div>
592
+ </div>
593
+ <p class="text-sm text-white/80 mt-2 font-medium">Craft your dstack configuration with style</p>
594
+ </div>
595
+ <div class="flex-grow bg-gradient-to-b from-white/5 to-white/1 overflow-hidden">
596
+ <div id="form-container" class="h-full p-6 space-y-4 overflow-y-auto">
597
+ <!-- Dynamic form content will be injected here -->
598
+ </div>
599
+ <div id="empty-state" class="flex flex-col items-center justify-center h-64 text-center p-8" style="display: none;">
600
+ <div class="w-16 h-16 bg-gradient-to-r from-violet-500/20 to-purple-500/20 rounded-2xl flex items-center justify-center mb-4">
601
+ <svg class="w-8 h-8 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
602
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
603
+ </svg>
604
+ </div>
605
+ <h3 class="text-lg font-semibold text-white/80 mb-2">Ready to Build</h3>
606
+ <p class="text-sm text-white/60 max-w-md">Select a template above or start editing the YAML to create your dstack configuration</p>
607
+ </div>
608
+ </div>
609
+ </div>
610
+
611
+ <!-- Right Side: Raw YAML Text Editor -->
612
+ <div class="flex flex-col glass-dark rounded-2xl shadow-2xl overflow-hidden card glow">
613
+ <div class="flex-shrink-0 bg-gradient-to-r from-slate-900/50 to-slate-800/50 border-b border-white/10 px-8 py-6 flex justify-between items-center">
614
+ <div class="flex items-center space-x-3">
615
+ <div class="w-3 h-3 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full animate-pulse"></div>
616
+ <div>
617
+ <h2 class="text-xl font-bold text-white drop-shadow-lg">dstack.yml</h2>
618
+ <p class="text-sm text-white/70 mt-1 font-medium">Raw YAML configuration</p>
619
+ </div>
620
+ </div>
621
+ <!-- Error/Success indicator -->
622
+ <div id="status-indicator" class="status-indicator px-4 py-2 text-xs font-bold uppercase tracking-wider rounded-full transition-all border border-white/20">&nbsp;</div>
623
+ </div>
624
+ <div class="flex-grow relative">
625
+ <textarea id="yaml-editor" class="w-full h-full p-8 bg-transparent text-slate-100 font-mono text-sm leading-7 resize-none focus:outline-none absolute inset-0 placeholder-slate-400" placeholder="# dstack configuration
626
+ type: task
627
+ name: my-awesome-task
628
+ python: '3.11'
629
+ commands:
630
+ - pip install requirements.txt
631
+ - python main.py"></textarea>
632
+ </div>
633
+ </div>
634
+ </main>
635
+ </div>
636
+
637
+ <script>
638
+ document.addEventListener('DOMContentLoaded', () => {
639
+ // --- DOM Element References ---
640
+ const formContainer = document.getElementById('form-container');
641
+ const yamlEditor = document.getElementById('yaml-editor');
642
+ const statusIndicator = document.getElementById('status-indicator');
643
+ const templateSelector = document.getElementById('templateSelector');
644
+ const clearBtn = document.getElementById('clearBtn');
645
+
646
+ // --- dstack Templates ---
647
+ const dstackTemplates = {
648
+ task: `type: task
649
+ name: my-task
650
+ python: "3.11"
651
+ commands:
652
+ - pip install -r requirements.txt
653
+ - python train.py
654
+ resources:
655
+ gpu: 24GB
656
+ memory: 32GB
657
+ ports:
658
+ - 8080
659
+ env:
660
+ - MODEL_NAME=llama2
661
+ - BATCH_SIZE=32`,
662
+
663
+ service: `type: service
664
+ name: my-service
665
+ image: ghcr.io/huggingface/text-generation-inference:latest
666
+ env:
667
+ - MODEL_ID=microsoft/DialoGPT-medium
668
+ commands:
669
+ - text-generation-launcher --port 8000 --trust-remote-code
670
+ port: 8000
671
+ resources:
672
+ gpu: 80GB
673
+ memory: 64GB
674
+ model:
675
+ type: chat
676
+ name: microsoft/DialoGPT-medium
677
+ format: tgi
678
+ replicas: 1..4
679
+ rate_limits:
680
+ - prefix: /api/
681
+ rps: 10
682
+ burst: 20`,
683
+
684
+ 'dev-environment': `type: dev-environment
685
+ name: my-dev-env
686
+ python: "3.11"
687
+ ide: vscode
688
+ commands:
689
+ - pip install -r requirements.txt
690
+ - pip install jupyter
691
+ resources:
692
+ gpu: 24GB
693
+ memory: 32GB
694
+ cpu: 8
695
+ disk: 100GB
696
+ ports:
697
+ - 8888
698
+ env:
699
+ - JUPYTER_TOKEN=my-secret-token`
700
+ };
701
+
702
+ // --- Preset Button Event Handlers ---
703
+ const loadPresetConfig = (presetType) => {
704
+ if (presetConfigs[presetType]) {
705
+ if (yamlEditor.value.trim() === '' || confirm('This will replace the current content. Are you sure?')) {
706
+ yamlEditor.value = presetConfigs[presetType];
707
+ updateFormFromYaml();
708
+ }
709
+ }
710
+ };
711
+
712
+ // --- Mock Preset Configurations ---
713
+ const presetConfigs = {
714
+ 'llm-training': `type: task
715
+ name: llm-training
716
+ python: "3.11"
717
+ commands:
718
+ - pip install torch transformers datasets accelerate
719
+ - python -m torch.distributed.launch --nproc_per_node=4 train.py
720
+ - python evaluate.py --checkpoint ./best_model
721
+ resources:
722
+ gpu: 80GB:4
723
+ memory: 256GB
724
+ cpu: 32
725
+ disk: 1TB
726
+ env:
727
+ - CUDA_VISIBLE_DEVICES=0,1,2,3
728
+ - TRANSFORMERS_CACHE=/opt/cache
729
+ - WANDB_PROJECT=llm-training
730
+ - MODEL_NAME=meta-llama/Llama-2-70b-hf
731
+ - BATCH_SIZE=8
732
+ - LEARNING_RATE=1e-5`,
733
+
734
+ 'model-serving': `type: service
735
+ name: llm-api-server
736
+ image: ghcr.io/huggingface/text-generation-inference:latest
737
+ env:
738
+ - MODEL_ID=microsoft/DialoGPT-large
739
+ - MAX_CONCURRENT_REQUESTS=128
740
+ - MAX_INPUT_LENGTH=1024
741
+ - MAX_TOTAL_TOKENS=2048
742
+ commands:
743
+ - text-generation-launcher --port 8000 --trust-remote-code --quantize bitsandbytes
744
+ port: 8000
745
+ resources:
746
+ gpu: 40GB
747
+ memory: 64GB
748
+ cpu: 16
749
+ model:
750
+ type: chat
751
+ name: microsoft/DialoGPT-large
752
+ format: tgi
753
+ replicas: 2..8
754
+ rate_limits:
755
+ - prefix: /api/v1/chat
756
+ rps: 50
757
+ burst: 100
758
+ - prefix: /health
759
+ rps: 200`,
760
+
761
+ 'jupyter-dev': `type: dev-environment
762
+ name: ml-jupyter-workspace
763
+ python: "3.11"
764
+ ide: jupyter
765
+ commands:
766
+ - pip install jupyter jupyterlab pandas numpy matplotlib seaborn scikit-learn
767
+ - pip install torch torchvision transformers datasets
768
+ - jupyter lab --ip=0.0.0.0 --port=8888 --no-browser --allow-root
769
+ resources:
770
+ gpu: 24GB
771
+ memory: 64GB
772
+ cpu: 16
773
+ disk: 500GB
774
+ ports:
775
+ - 8888
776
+ - 6006
777
+ env:
778
+ - JUPYTER_TOKEN=secure-token-123
779
+ - NVIDIA_VISIBLE_DEVICES=all
780
+ - PYTHONPATH=/workspace`,
781
+
782
+ 'data-processing': `type: task
783
+ name: data-pipeline
784
+ python: "3.11"
785
+ commands:
786
+ - pip install pandas polars dask distributed apache-beam
787
+ - python extract_data.py --source s3://my-bucket/raw/
788
+ - python transform_data.py --workers 16
789
+ - python load_data.py --target s3://my-bucket/processed/
790
+ resources:
791
+ memory: 128GB
792
+ cpu: 32
793
+ disk: 2TB
794
+ env:
795
+ - AWS_ACCESS_KEY_ID=your-access-key
796
+ - AWS_SECRET_ACCESS_KEY=your-secret-key
797
+ - DASK_WORKERS=16
798
+ - CHUNK_SIZE=10000`,
799
+
800
+ 'web-app': `type: service
801
+ name: streamlit-demo
802
+ python: "3.11"
803
+ commands:
804
+ - pip install streamlit plotly pandas numpy
805
+ - streamlit run app.py --server.port 8501 --server.address 0.0.0.0
806
+ port: 8501
807
+ resources:
808
+ memory: 8GB
809
+ cpu: 4
810
+ replicas: 1..3
811
+ rate_limits:
812
+ - rps: 100
813
+ burst: 200
814
+ env:
815
+ - STREAMLIT_THEME_BASE=dark
816
+ - STREAMLIT_SERVER_ENABLE_CORS=true`,
817
+
818
+ 'gpu-cluster': `type: task
819
+ name: distributed-training
820
+ python: "3.11"
821
+ commands:
822
+ - pip install torch torchvision transformers accelerate deepspeed
823
+ - accelerate launch --multi_gpu --num_processes 8 train_distributed.py
824
+ - python consolidate_checkpoints.py
825
+ resources:
826
+ gpu: 40GB:8
827
+ memory: 512GB
828
+ cpu: 64
829
+ disk: 5TB
830
+ env:
831
+ - MASTER_ADDR=localhost
832
+ - MASTER_PORT=29500
833
+ - WORLD_SIZE=8
834
+ - NCCL_DEBUG=INFO
835
+ - CUDA_LAUNCH_BLOCKING=1
836
+ - MODEL_PARALLEL_SIZE=4
837
+ - DATA_PARALLEL_SIZE=2`
838
+ };
839
+
840
+ // --- Initial State & Data ---
841
+ let currentData = {}; // Holds the parsed YAML as a JS object
842
+ let isUpdating = false; // Prevents infinite update loops
843
+
844
+ // --- Utility Functions ---
845
+ /**
846
+ * Sets a value in a nested object based on a path string.
847
+ */
848
+ const setNestedValue = (obj, path, value) => {
849
+ const keys = path.split('.');
850
+ let current = obj;
851
+ for (let i = 0; i < keys.length - 1; i++) {
852
+ const key = keys[i];
853
+ const nextKey = keys[i + 1];
854
+ if (!isNaN(parseInt(nextKey, 10)) && !Array.isArray(current[key])) {
855
+ current[key] = [];
856
+ } else if (isNaN(parseInt(nextKey, 10)) && typeof current[key] !== 'object') {
857
+ current[key] = {};
858
+ }
859
+ current = current[key];
860
+ }
861
+ current[keys[keys.length - 1]] = value;
862
+ };
863
+
864
+ /**
865
+ * Deletes a key or an array element from a nested object.
866
+ */
867
+ const deleteNestedValue = (obj, path) => {
868
+ const keys = path.split('.');
869
+ let current = obj;
870
+ for (let i = 0; i < keys.length - 1; i++) {
871
+ current = current[keys[i]];
872
+ }
873
+ const finalKey = keys[keys.length - 1];
874
+ if (Array.isArray(current)) {
875
+ current.splice(parseInt(finalKey, 10), 1);
876
+ } else {
877
+ delete current[finalKey];
878
+ }
879
+ };
880
+
881
+ // --- Core Rendering Functions ---
882
+
883
+ /**
884
+ * Generates an input field based on the value type and dstack context.
885
+ */
886
+ const createInputField = (key, value, path) => {
887
+ const type = typeof value;
888
+ const id = `field-${path}`;
889
+ let inputHtml = '';
890
+
891
+ // Common classes for text-based inputs with compact styling
892
+ const inputClasses = "form-input mt-1 block w-full rounded-lg border-white/20 shadow-lg focus:border-violet-400 focus:ring-violet-400 text-sm font-medium placeholder-white/60 py-2.5";
893
+ const selectClasses = "form-select mt-1 block w-full rounded-lg border-white/20 shadow-lg focus:border-violet-400 focus:ring-violet-400 text-sm font-medium py-2.5";
894
+
895
+ // Get field icon SVG based on key
896
+ const getFieldIcon = (key) => {
897
+ const iconMap = {
898
+ 'type': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path></svg>',
899
+ 'name': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>',
900
+ 'python': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>',
901
+ 'image': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>',
902
+ 'commands': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>',
903
+ 'ports': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path></svg>',
904
+ 'env': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>',
905
+ 'resources': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path></svg>',
906
+ 'gpu': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path></svg>',
907
+ 'memory': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>',
908
+ 'cpu': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path></svg>',
909
+ 'disk': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"></path></svg>',
910
+ 'replicas': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>',
911
+ 'rate_limits': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
912
+ 'rps': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>',
913
+ 'burst': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>',
914
+ 'model': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path></svg>',
915
+ 'format': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>',
916
+ 'prefix': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>',
917
+ 'ide': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>',
918
+ 'value': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>'
919
+ };
920
+ return iconMap[key] || '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>';
921
+ };
922
+
923
+ const fieldIcon = getFieldIcon(key);
924
+ const fieldLabel = String(key).replace(/_/g, ' ');
925
+
926
+ // dstack-specific field handling
927
+ if (key === 'type' && typeof value === 'string') {
928
+ inputHtml = `
929
+ <div class="relative">
930
+ <select data-path="${path}" id="${id}" class="${selectClasses} pl-12 appearance-none cursor-pointer">
931
+ <option value="task" ${value === 'task' ? 'selected' : ''}>Task</option>
932
+ <option value="service" ${value === 'service' ? 'selected' : ''}>Service</option>
933
+ <option value="dev-environment" ${value === 'dev-environment' ? 'selected' : ''}>Dev Environment</option>
934
+ </select>
935
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/60">
936
+ ${fieldIcon}
937
+ </div>
938
+ <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
939
+ <svg class="h-5 w-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
940
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
941
+ </svg>
942
+ </div>
943
+ </div>`;
944
+ } else if (key === 'python' && typeof value === 'string') {
945
+ inputHtml = `
946
+ <div class="relative">
947
+ <select data-path="${path}" id="${id}" class="${selectClasses} pl-12 appearance-none cursor-pointer">
948
+ <option value="3.8" ${value === '3.8' ? 'selected' : ''}>Python 3.8</option>
949
+ <option value="3.9" ${value === '3.9' ? 'selected' : ''}>Python 3.9</option>
950
+ <option value="3.10" ${value === '3.10' ? 'selected' : ''}>Python 3.10</option>
951
+ <option value="3.11" ${value === '3.11' ? 'selected' : ''}>Python 3.11</option>
952
+ <option value="3.12" ${value === '3.12' ? 'selected' : ''}>Python 3.12</option>
953
+ </select>
954
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/60">
955
+ ${fieldIcon}
956
+ </div>
957
+ <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
958
+ <svg class="h-5 w-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
959
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
960
+ </svg>
961
+ </div>
962
+ </div>`;
963
+ } else if (key === 'ide' && typeof value === 'string') {
964
+ inputHtml = `
965
+ <div class="relative">
966
+ <select data-path="${path}" id="${id}" class="${selectClasses} pl-12 appearance-none cursor-pointer">
967
+ <option value="vscode" ${value === 'vscode' ? 'selected' : ''}>VS Code</option>
968
+ <option value="jupyter" ${value === 'jupyter' ? 'selected' : ''}>Jupyter</option>
969
+ <option value="ssh" ${value === 'ssh' ? 'selected' : ''}>SSH</option>
970
+ </select>
971
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/60">
972
+ ${fieldIcon}
973
+ </div>
974
+ <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
975
+ <svg class="h-5 w-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
976
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
977
+ </svg>
978
+ </div>
979
+ </div>`;
980
+ } else if (type === 'boolean') {
981
+ inputHtml = `
982
+ <div class="relative">
983
+ <div class="flex items-center mt-3 p-4 glass rounded-xl border border-white/10 hover:border-white/20 transition-all group">
984
+ <div class="flex items-center">
985
+ <input type="checkbox" data-path="${path}" id="${id}" ${value ? 'checked' : ''} class="h-5 w-5 rounded-lg border-white/30 text-violet-600 focus:ring-violet-500 focus:ring-offset-2 transition-all">
986
+ <div class="ml-4 flex items-center space-x-3">
987
+ <div class="text-white/60">${fieldIcon}</div>
988
+ <label for="${id}" class="text-sm font-semibold transition-colors ${value ? 'text-green-400' : 'text-white/70'}">${value ? 'Enabled' : 'Disabled'}</label>
989
+ </div>
990
+ </div>
991
+ <div class="ml-auto">
992
+ <div class="w-6 h-6 rounded-full flex items-center justify-center ${value ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}">
993
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
994
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${value ? 'M5 13l4 4L19 7' : 'M6 18L18 6M6 6l12 12'}"></path>
995
+ </svg>
996
+ </div>
997
+ </div>
998
+ </div>
999
+ </div>`;
1000
+ } else if (type === 'number') {
1001
+ inputHtml = `
1002
+ <div class="relative">
1003
+ <input type="number" data-path="${path}" id="${id}" value="${value}" class="${inputClasses} pl-12" placeholder="Enter number...">
1004
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/60">
1005
+ ${fieldIcon}
1006
+ </div>
1007
+ </div>`;
1008
+ } else { // string or other
1009
+ let placeholder = `Enter ${fieldLabel.toLowerCase()}...`;
1010
+ if (key === 'name') placeholder = 'my-awesome-project';
1011
+ else if (key === 'image') placeholder = 'ghcr.io/huggingface/transformers';
1012
+ else if (key === 'value') placeholder = 'Enter value...';
1013
+
1014
+ inputHtml = `
1015
+ <div class="relative">
1016
+ <input type="text" data-path="${path}" id="${id}" value="${value}" class="${inputClasses} pl-12" placeholder="${placeholder}">
1017
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/60">
1018
+ ${fieldIcon}
1019
+ </div>
1020
+ </div>`;
1021
+ }
1022
+
1023
+ // Add compact help text for key fields only
1024
+ let helpText = '';
1025
+ if (key === 'gpu' || key === 'memory' || key === 'cpu') {
1026
+ helpText = '<div class="mt-1 p-2 bg-gradient-to-r from-blue-500/15 to-purple-500/15 rounded-lg border border-white/10"><p class="text-xs text-white/80 font-medium">e.g. 24GB, 8, 24GB..80GB</p></div>';
1027
+ } else if (key === 'replicas') {
1028
+ helpText = '<div class="mt-1 p-2 bg-gradient-to-r from-green-500/15 to-blue-500/15 rounded-lg border border-white/10"><p class="text-xs text-white/80 font-medium">e.g. 1, 1..4, 2..10</p></div>';
1029
+ }
1030
+
1031
+ return `
1032
+ <div class="mb-4 group">
1033
+ <label for="${id}" class="flex items-center space-x-2 text-sm font-bold text-white capitalize mb-2 group-hover:text-white transition-colors">
1034
+ <div class="text-white/80" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">${fieldIcon}</div>
1035
+ <span style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7); font-weight: 600;">${fieldLabel}</span>
1036
+ </label>
1037
+ ${inputHtml}
1038
+ ${helpText}
1039
+ </div>`;
1040
+ };
1041
+
1042
+ /**
1043
+ * Recursively builds the form UI from a JavaScript object.
1044
+ */
1045
+ const renderForm = (data, parentElement, parentPath = '') => {
1046
+ if (Array.isArray(data)) {
1047
+ // --- Render Array ---
1048
+ const arrayContainer = document.createElement('div');
1049
+ data.forEach((item, index) => {
1050
+ const itemPath = parentPath ? `${parentPath}.${index}` : String(index);
1051
+ const fieldset = document.createElement('fieldset');
1052
+ fieldset.className = "glass border border-white/30 rounded-xl p-4 mb-4 relative group hover:glow transition-all duration-500 fieldset-enter";
1053
+ const summaryText = generateSummary(item, itemPath);
1054
+ fieldset.innerHTML = `
1055
+ <legend class="text-sm font-bold px-3 py-1.5 text-white bg-gradient-to-r from-violet-500/90 to-purple-500/90 rounded-lg shadow-lg backdrop-blur-md border border-white/30 cursor-pointer hover:from-violet-500 hover:to-purple-500 transition-all" style="text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);" onclick="toggleFieldset(this)">
1056
+ <div class="flex items-center justify-between w-full">
1057
+ <div class="flex items-center space-x-2">
1058
+ <span>Item ${index + 1}</span>
1059
+ <svg class="w-4 h-4 collapse-arrow expanded" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1060
+ <path d="M19 9l-7 7-7-7"></path>
1061
+ </svg>
1062
+ </div>
1063
+ </div>
1064
+ <div class="summary-preview" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">${summaryText}</div>
1065
+ </legend>
1066
+ <button data-path="${itemPath}" class="remove-btn absolute top-2 right-2 p-1.5 text-white/60 hover:text-red-400 hover:bg-red-500/20 rounded-lg transition-all hover:scale-110">
1067
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
1068
+ </button>
1069
+ <div class="fieldset-content expanded mt-3">
1070
+ </div>
1071
+ `;
1072
+
1073
+ // Handle primitive values in arrays (like strings in commands array)
1074
+ const contentDiv = fieldset.querySelector('.fieldset-content');
1075
+ if (typeof item !== 'object' || item === null) {
1076
+ contentDiv.innerHTML = createInputField('value', item, itemPath);
1077
+ } else {
1078
+ renderForm(item, contentDiv, itemPath);
1079
+ }
1080
+ arrayContainer.appendChild(fieldset);
1081
+ });
1082
+
1083
+ // Smart default for array items based on parent context
1084
+ let addButtonText = 'Add Item';
1085
+ let newItemTemplate = "new_item";
1086
+
1087
+ if (parentPath === 'commands') {
1088
+ addButtonText = 'Add Command';
1089
+ newItemTemplate = 'python script.py';
1090
+ } else if (parentPath === 'env') {
1091
+ addButtonText = 'Add Environment Variable';
1092
+ newItemTemplate = 'KEY=value';
1093
+ } else if (parentPath === 'ports') {
1094
+ addButtonText = 'Add Port';
1095
+ newItemTemplate = 8080;
1096
+ } else if (parentPath === 'rate_limits') {
1097
+ addButtonText = 'Add Rate Limit';
1098
+ newItemTemplate = { prefix: "/api/", rps: 10 };
1099
+ }
1100
+
1101
+ arrayContainer.innerHTML += `<button data-path="${parentPath}" data-template='${JSON.stringify(newItemTemplate)}' class="add-btn mt-3 w-full flex items-center justify-center px-4 py-3 border-2 border-dashed border-white/30 text-sm font-bold rounded-xl text-white bg-gradient-to-r from-violet-500/20 to-purple-500/20 hover:from-violet-500/30 hover:to-purple-500/30 transition-all hover:scale-105 hover:shadow-lg backdrop-blur-sm">
1102
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-white/80" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" /></svg>${addButtonText}</button>`;
1103
+ parentElement.appendChild(arrayContainer);
1104
+ } else if (typeof data === 'object' && data !== null) {
1105
+ // --- Render Object ---
1106
+ Object.entries(data).forEach(([key, value]) => {
1107
+ const currentPath = parentPath ? `${parentPath}.${key}` : key;
1108
+ if (typeof value === 'object' && value !== null) {
1109
+ const fieldset = document.createElement('fieldset');
1110
+ fieldset.className = "glass border border-white/30 rounded-xl p-4 mb-4 group hover:glow transition-all duration-500 fieldset-enter";
1111
+ const summaryText = generateSummary(value, currentPath);
1112
+ fieldset.innerHTML = `
1113
+ <legend class="text-sm font-bold px-3 py-1.5 text-white bg-gradient-to-r from-blue-500/90 to-cyan-500/90 rounded-lg shadow-lg backdrop-blur-md border border-white/30 capitalize cursor-pointer hover:from-blue-500 hover:to-cyan-500 transition-all" style="text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);" onclick="toggleFieldset(this)">
1114
+ <div class="flex items-center justify-between w-full">
1115
+ <div class="flex items-center space-x-2">
1116
+ <span>${key.replace(/_/g, ' ')}</span>
1117
+ <svg class="w-4 h-4 collapse-arrow expanded" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1118
+ <path d="M19 9l-7 7-7-7"></path>
1119
+ </svg>
1120
+ </div>
1121
+ </div>
1122
+ <div class="summary-preview" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">${summaryText}</div>
1123
+ </legend>
1124
+ <div class="fieldset-content expanded mt-3">
1125
+ </div>
1126
+ `;
1127
+ const contentDiv = fieldset.querySelector('.fieldset-content');
1128
+ renderForm(value, contentDiv, currentPath);
1129
+ parentElement.appendChild(fieldset);
1130
+ } else {
1131
+ parentElement.innerHTML += createInputField(key, value, currentPath);
1132
+ }
1133
+ });
1134
+ }
1135
+ };
1136
+
1137
+ // --- Update & Sync Functions ---
1138
+ const updateYamlEditor = () => {
1139
+ if (isUpdating) return;
1140
+ isUpdating = true;
1141
+ try {
1142
+ const yamlString = jsyaml.dump(currentData, { indent: 2 });
1143
+ yamlEditor.value = yamlString;
1144
+ statusIndicator.textContent = 'Synced';
1145
+ statusIndicator.className = 'status-indicator px-4 py-2 text-xs font-bold uppercase tracking-wider rounded-full transition-all border border-white/20 bg-gradient-to-r from-green-500/20 to-emerald-500/20 text-green-300';
1146
+ yamlEditor.classList.remove('border-red-500');
1147
+ } catch (e) {
1148
+ console.error("Error dumping YAML:", e);
1149
+ }
1150
+ setTimeout(() => { isUpdating = false; }, 50);
1151
+ };
1152
+
1153
+ const updateFormFromYaml = () => {
1154
+ if (isUpdating) return;
1155
+ isUpdating = true;
1156
+ try {
1157
+ const newData = jsyaml.load(yamlEditor.value);
1158
+ if (JSON.stringify(newData) !== JSON.stringify(currentData)) {
1159
+ currentData = newData || {};
1160
+ formContainer.innerHTML = ''; // Clear previous form
1161
+
1162
+ // Hide/show empty state based on content
1163
+ const emptyState = document.getElementById('empty-state');
1164
+ if (Object.keys(currentData).length === 0 || yamlEditor.value.trim() === '') {
1165
+ emptyState.style.display = 'flex';
1166
+ formContainer.style.display = 'none';
1167
+ } else {
1168
+ emptyState.style.display = 'none';
1169
+ formContainer.style.display = 'block';
1170
+ renderForm(currentData, formContainer);
1171
+ }
1172
+ }
1173
+ statusIndicator.textContent = 'Valid';
1174
+ statusIndicator.className = 'status-indicator px-4 py-2 text-xs font-bold uppercase tracking-wider rounded-full transition-all border border-white/20 bg-gradient-to-r from-green-500/20 to-emerald-500/20 text-green-300';
1175
+ yamlEditor.classList.remove('border-red-500');
1176
+ } catch (e) {
1177
+ statusIndicator.textContent = 'Error';
1178
+ statusIndicator.className = 'status-indicator px-4 py-2 text-xs font-bold uppercase tracking-wider rounded-full transition-all border border-white/20 bg-gradient-to-r from-red-500/20 to-pink-500/20 text-red-300';
1179
+ yamlEditor.classList.add('border-red-500');
1180
+ console.warn("Invalid YAML:", e.message);
1181
+ }
1182
+ setTimeout(() => { isUpdating = false; }, 50);
1183
+ };
1184
+
1185
+ const updateDataFromForm = (path, value) => {
1186
+ setNestedValue(currentData, path, value);
1187
+ updateYamlEditor();
1188
+ };
1189
+
1190
+ // --- Event Listeners ---
1191
+ yamlEditor.addEventListener('input', updateFormFromYaml);
1192
+
1193
+ formContainer.addEventListener('input', (e) => {
1194
+ if (e.target.dataset.path) {
1195
+ const path = e.target.dataset.path;
1196
+ let value = e.target.value;
1197
+ if (e.target.type === 'checkbox') {
1198
+ value = e.target.checked;
1199
+ // Update label for checkbox
1200
+ const label = e.target.nextElementSibling;
1201
+ if (label) {
1202
+ label.textContent = value ? 'Enabled' : 'Disabled';
1203
+ }
1204
+ } else if (e.target.type === 'number') {
1205
+ value = parseFloat(value) || 0;
1206
+ }
1207
+ updateDataFromForm(path, value);
1208
+ }
1209
+ });
1210
+
1211
+ formContainer.addEventListener('click', (e) => {
1212
+ const button = e.target.closest('button');
1213
+ if (!button) return;
1214
+
1215
+ const path = button.dataset.path;
1216
+ if (button.classList.contains('remove-btn')) {
1217
+ if (confirm('Are you sure you want to remove this item?')) {
1218
+ deleteNestedValue(currentData, path);
1219
+ formContainer.innerHTML = '';
1220
+ renderForm(currentData, formContainer);
1221
+ updateYamlEditor();
1222
+ }
1223
+ } else if (button.classList.contains('add-btn')) {
1224
+ let newItem;
1225
+ try {
1226
+ newItem = JSON.parse(button.dataset.template);
1227
+ } catch (e) {
1228
+ newItem = button.dataset.template;
1229
+ }
1230
+
1231
+ const currentArray = path ? currentData[path] : currentData;
1232
+ const newIndex = Array.isArray(currentArray) ? currentArray.length : 0;
1233
+ setNestedValue(currentData, `${path ? path + '.' : ''}${newIndex}`, newItem);
1234
+ formContainer.innerHTML = '';
1235
+ renderForm(currentData, formContainer);
1236
+ updateYamlEditor();
1237
+ }
1238
+ });
1239
+
1240
+ templateSelector.addEventListener('change', (e) => {
1241
+ if (e.target.value && dstackTemplates[e.target.value]) {
1242
+ if (yamlEditor.value.trim() === '' || confirm('This will replace the current content. Are you sure?')) {
1243
+ yamlEditor.value = dstackTemplates[e.target.value];
1244
+ updateFormFromYaml();
1245
+ }
1246
+ e.target.value = ''; // Reset selector
1247
+ }
1248
+ });
1249
+
1250
+ clearBtn.addEventListener('click', () => {
1251
+ if (confirm('Are you sure you want to clear everything?')) {
1252
+ yamlEditor.value = '';
1253
+ updateFormFromYaml();
1254
+ }
1255
+ });
1256
+
1257
+ // --- Generate Summary for Collapsed Sections ---
1258
+ const generateSummary = (data, path) => {
1259
+ if (Array.isArray(data)) {
1260
+ if (data.length === 0) return "Empty";
1261
+
1262
+ // Handle different array types
1263
+ if (path.includes('commands')) {
1264
+ return `${data.length} command${data.length > 1 ? 's' : ''}: ${data.slice(0, 2).join(', ')}${data.length > 2 ? '...' : ''}`;
1265
+ } else if (path.includes('env')) {
1266
+ return `${data.length} variable${data.length > 1 ? 's' : ''}: ${data.slice(0, 2).join(', ')}${data.length > 2 ? '...' : ''}`;
1267
+ } else if (path.includes('ports')) {
1268
+ return `Port${data.length > 1 ? 's' : ''}: ${data.slice(0, 3).join(', ')}${data.length > 3 ? '...' : ''}`;
1269
+ } else {
1270
+ return `${data.length} item${data.length > 1 ? 's' : ''}`;
1271
+ }
1272
+ } else if (typeof data === 'object' && data !== null) {
1273
+ const keys = Object.keys(data);
1274
+ if (keys.length === 0) return "Empty";
1275
+
1276
+ // Create meaningful summaries for common object types
1277
+ const summaryParts = [];
1278
+
1279
+ if (data.gpu) summaryParts.push(`GPU: ${data.gpu}`);
1280
+ if (data.memory) summaryParts.push(`Memory: ${data.memory}`);
1281
+ if (data.cpu) summaryParts.push(`CPU: ${data.cpu}`);
1282
+ if (data.type) summaryParts.push(`Type: ${data.type}`);
1283
+ if (data.name) summaryParts.push(`Name: ${data.name}`);
1284
+ if (data.format) summaryParts.push(`Format: ${data.format}`);
1285
+ if (data.rps) summaryParts.push(`RPS: ${data.rps}`);
1286
+ if (data.replicas) summaryParts.push(`Replicas: ${data.replicas}`);
1287
+
1288
+ if (summaryParts.length > 0) {
1289
+ return summaryParts.slice(0, 3).join(', ') + (summaryParts.length > 3 ? '...' : '');
1290
+ } else {
1291
+ return `${keys.length} field${keys.length > 1 ? 's' : ''}: ${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}`;
1292
+ }
1293
+ }
1294
+ return '';
1295
+ };
1296
+
1297
+ // --- Animated Collapsible Fieldset Function ---
1298
+ window.toggleFieldset = function(legend) {
1299
+ const fieldset = legend.closest('fieldset');
1300
+ const content = fieldset.querySelector('.fieldset-content');
1301
+ const arrow = legend.querySelector('.collapse-arrow');
1302
+ const summary = legend.querySelector('.summary-preview');
1303
+ const isExpanded = content.classList.contains('expanded');
1304
+
1305
+ if (isExpanded) {
1306
+ // Collapse - show summary
1307
+ content.classList.remove('expanded');
1308
+ content.classList.add('collapsed');
1309
+ arrow.classList.remove('expanded');
1310
+ arrow.classList.add('collapsed');
1311
+ if (summary) summary.classList.add('show');
1312
+
1313
+ // Add subtle legend animation
1314
+ legend.style.transform = 'scale(0.98)';
1315
+ setTimeout(() => {
1316
+ legend.style.transform = 'scale(1)';
1317
+ }, 150);
1318
+ } else {
1319
+ // Expand - hide summary
1320
+ content.classList.remove('collapsed');
1321
+ content.classList.add('expanded');
1322
+ arrow.classList.remove('collapsed');
1323
+ arrow.classList.add('expanded');
1324
+ if (summary) summary.classList.remove('show');
1325
+
1326
+ // Add subtle legend animation
1327
+ legend.style.transform = 'scale(1.02)';
1328
+ setTimeout(() => {
1329
+ legend.style.transform = 'scale(1)';
1330
+ }, 150);
1331
+ }
1332
+ };
1333
+
1334
+ // --- Preset Button Event Listeners ---
1335
+ document.getElementById('preset-llm-training').addEventListener('click', () => loadPresetConfig('llm-training'));
1336
+ document.getElementById('preset-model-serving').addEventListener('click', () => loadPresetConfig('model-serving'));
1337
+ document.getElementById('preset-jupyter-dev').addEventListener('click', () => loadPresetConfig('jupyter-dev'));
1338
+ document.getElementById('preset-data-processing').addEventListener('click', () => loadPresetConfig('data-processing'));
1339
+ document.getElementById('preset-web-app').addEventListener('click', () => loadPresetConfig('web-app'));
1340
+ document.getElementById('preset-gpu-cluster').addEventListener('click', () => loadPresetConfig('gpu-cluster'));
1341
+
1342
+ // --- Initial Load ---
1343
+ yamlEditor.value = dstackTemplates.task;
1344
+ updateFormFromYaml();
1345
+ });
1346
+ </script>
1347
+
1348
+ </body>
1349
+ </html>