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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1262 -219
index.html CHANGED
@@ -14,6 +14,12 @@
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 {
@@ -82,76 +88,110 @@
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 */
@@ -207,17 +247,134 @@
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 */
@@ -241,12 +398,12 @@
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
  }
@@ -310,31 +467,42 @@
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 {
@@ -414,6 +582,121 @@
414
  margin-top: 0.5rem;
415
  }
416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  /* Animated status indicator */
418
  .status-indicator {
419
  position: relative;
@@ -481,48 +764,193 @@
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">
@@ -534,10 +962,10 @@
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">
@@ -621,27 +1049,407 @@
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 = {
@@ -702,15 +1510,71 @@ env:
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"
@@ -1011,10 +1875,14 @@ env:
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>`;
@@ -1106,27 +1974,59 @@ env:
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
  }
@@ -1140,10 +2040,14 @@ env:
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
  }
@@ -1154,14 +2058,14 @@ env:
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 {
@@ -1172,11 +2076,11 @@ env:
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);
@@ -1188,7 +2092,7 @@ env:
1188
  };
1189
 
1190
  // --- Event Listeners ---
1191
- yamlEditor.addEventListener('input', updateFormFromYaml);
1192
 
1193
  formContainer.addEventListener('input', (e) => {
1194
  if (e.target.dataset.path) {
@@ -1237,111 +2141,250 @@ env:
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
 
 
14
  <!-- JS-YAML CDN -->
15
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js-yaml.min.js"></script>
16
 
17
+ <!-- CodeMirror for YAML syntax highlighting -->
18
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
19
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-darker.min.css">
20
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
21
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/yaml/yaml.min.js"></script>
22
+
23
  <style>
24
  /* Modern styling with animations and gradients */
25
  html {
 
88
  background-clip: content-box;
89
  }
90
 
91
+ /* Form container bold text */
92
+ #form-container {
93
+ font-weight: bold;
94
+ }
95
+
96
+ /* YAML editor container overflow */
97
+ #yaml-editor-container {
98
+ overflow: auto;
99
+ }
100
+
101
+ /* Glass with lens distortion */
102
  .glass {
103
  background: linear-gradient(135deg,
104
+ rgba(255, 255, 255, 0.12) 0%,
105
+ rgba(255, 255, 255, 0.06) 100%);
106
+ backdrop-filter: blur(16px) saturate(180%) contrast(120%);
107
+ -webkit-backdrop-filter: blur(16px) saturate(180%) contrast(120%);
108
+ border: 1px solid rgba(255, 255, 255, 0.25);
109
  box-shadow:
110
  0 8px 32px rgba(31, 38, 135, 0.37),
111
  inset 0 1px 0 rgba(255, 255, 255, 0.5),
112
  inset 0 -1px 0 rgba(255, 255, 255, 0.1);
113
  position: relative;
114
  overflow: hidden;
115
+ filter: contrast(1.1) brightness(1.05);
116
  }
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  .glass-dark {
120
  background: linear-gradient(135deg,
121
+ rgba(15, 23, 42, 0.75) 0%,
122
+ rgba(15, 23, 42, 0.55) 100%);
123
+ backdrop-filter: blur(20px) saturate(160%) contrast(110%);
124
+ -webkit-backdrop-filter: blur(20px) saturate(160%) contrast(110%);
125
+ border: 1px solid rgba(255, 255, 255, 0.15);
126
  box-shadow:
127
  0 8px 32px rgba(0, 0, 0, 0.3),
128
  inset 0 1px 0 rgba(255, 255, 255, 0.3),
129
  inset 0 -1px 0 rgba(255, 255, 255, 0.1);
130
  position: relative;
131
  overflow: hidden;
132
+ filter: contrast(1.08) brightness(1.02);
133
  }
134
 
135
+ /* Lens magnification for cards */
136
+ .card {
137
+ box-shadow:
138
+ 0 20px 60px rgba(31, 38, 135, 0.4),
139
+ 0 10px 30px rgba(31, 38, 135, 0.3),
140
+ 0 5px 15px rgba(31, 38, 135, 0.2);
141
+ transform: translateZ(0) scale(1);
142
+ filter: contrast(1.05) saturate(110%);
143
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
 
 
 
144
  }
145
 
146
+ .card:hover {
147
+ transform: translateZ(10px) translateY(-5px) scale(1.03);
148
+ filter: contrast(1.1) saturate(120%) brightness(1.05);
149
+ box-shadow:
150
+ 0 30px 80px rgba(31, 38, 135, 0.5),
151
+ 0 20px 50px rgba(31, 38, 135, 0.4),
152
+ 0 10px 25px rgba(31, 38, 135, 0.3);
153
+ }
154
+
155
+ /* Lens focus buttons */
156
+ .btn-primary, .btn-secondary {
157
+ box-shadow:
158
+ 0 8px 25px rgba(99, 102, 241, 0.4),
159
+ 0 4px 15px rgba(99, 102, 241, 0.3),
160
+ 0 2px 8px rgba(99, 102, 241, 0.2),
161
+ inset 0 1px 0 rgba(255, 255, 255, 0.3);
162
+ transform: translateZ(0) scale(1);
163
+ filter: contrast(1.1) saturate(120%);
164
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
165
+ }
166
+
167
+ .btn-primary:hover, .btn-secondary:hover {
168
+ transform: translateZ(5px) translateY(-2px) scale(1.05);
169
+ filter: contrast(1.2) saturate(140%) brightness(1.1);
170
+ box-shadow:
171
+ 0 12px 35px rgba(99, 102, 241, 0.5),
172
+ 0 6px 20px rgba(99, 102, 241, 0.4),
173
+ 0 3px 12px rgba(99, 102, 241, 0.3),
174
+ inset 0 1px 0 rgba(255, 255, 255, 0.4);
175
+ }
176
+
177
+ /* Lens refraction inputs */
178
+ .form-input, .form-select {
179
+ box-shadow:
180
+ inset 0 3px 8px rgba(0, 0, 0, 0.1),
181
+ inset 0 1px 4px rgba(0, 0, 0, 0.08),
182
+ 0 2px 6px rgba(255, 255, 255, 0.1);
183
+ filter: contrast(1.05) saturate(105%);
184
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
185
+ }
186
+
187
+ .form-input:focus, .form-select:focus {
188
+ box-shadow:
189
+ inset 0 3px 8px rgba(0, 0, 0, 0.15),
190
+ inset 0 1px 4px rgba(0, 0, 0, 0.1),
191
+ 0 4px 12px rgba(99, 102, 241, 0.2),
192
+ 0 0 0 2px rgba(99, 102, 241, 0.3);
193
+ filter: contrast(1.1) saturate(115%) brightness(1.05);
194
+ transform: scale(1.01);
195
  }
196
 
197
  /* Glowing effects */
 
247
  position: relative;
248
  }
249
 
250
+ /* Annotation styles */
251
+ .annotation-container {
252
+ position: relative;
253
+ }
254
+
255
+ .annotation-btn {
256
+ position: absolute;
257
+ right: 8px;
258
+ top: 50%;
259
+ transform: translateY(-50%);
260
+ z-index: 10;
261
+ background: rgba(99, 102, 241, 0.2);
262
+ border: 1px solid rgba(99, 102, 241, 0.4);
263
+ border-radius: 6px;
264
+ padding: 4px;
265
+ cursor: pointer;
266
+ transition: all 0.2s;
267
+ }
268
+
269
+ .annotation-btn:hover {
270
+ background: rgba(99, 102, 241, 0.3);
271
+ border-color: rgba(99, 102, 241, 0.6);
272
+ }
273
+
274
+ .annotation-btn.has-annotation {
275
+ background: rgba(34, 197, 94, 0.2);
276
+ border-color: rgba(34, 197, 94, 0.4);
277
+ }
278
+
279
+ .annotation-popup {
280
+ position: absolute;
281
+ top: 100%;
282
+ right: 0;
283
+ margin-top: 8px;
284
+ width: 300px;
285
+ background: linear-gradient(135deg,
286
+ rgba(15, 23, 42, 0.95) 0%,
287
+ rgba(15, 23, 42, 0.9) 100%);
288
+ backdrop-filter: blur(20px);
289
+ border: 1px solid rgba(255, 255, 255, 0.2);
290
+ border-radius: 12px;
291
+ padding: 16px;
292
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
293
+ z-index: 20;
294
+ display: none;
295
+ }
296
+
297
+ .annotation-popup.show {
298
+ display: block;
299
+ animation: slideInUp 0.2s ease-out;
300
+ }
301
+
302
+ .annotation-input {
303
+ width: 100%;
304
+ background: rgba(255, 255, 255, 0.1);
305
+ border: 1px solid rgba(255, 255, 255, 0.3);
306
+ border-radius: 8px;
307
+ padding: 8px 12px;
308
+ color: white;
309
+ font-size: 0.875rem;
310
+ resize: vertical;
311
+ min-height: 60px;
312
+ }
313
+
314
+ .annotation-input:focus {
315
+ outline: none;
316
+ border-color: rgba(99, 102, 241, 0.6);
317
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
318
+ }
319
+
320
+ .annotation-actions {
321
+ display: flex;
322
+ gap: 8px;
323
+ margin-top: 12px;
324
+ }
325
+
326
+ .annotation-save, .annotation-cancel {
327
+ padding: 6px 12px;
328
+ border-radius: 6px;
329
+ font-size: 0.75rem;
330
+ font-weight: 600;
331
+ cursor: pointer;
332
+ transition: all 0.2s;
333
+ }
334
+
335
+ .annotation-save {
336
+ background: rgba(34, 197, 94, 0.2);
337
+ border: 1px solid rgba(34, 197, 94, 0.4);
338
+ color: rgb(34, 197, 94);
339
+ }
340
+
341
+ .annotation-save:hover {
342
+ background: rgba(34, 197, 94, 0.3);
343
+ }
344
+
345
+ .annotation-cancel {
346
+ background: rgba(239, 68, 68, 0.2);
347
+ border: 1px solid rgba(239, 68, 68, 0.4);
348
+ color: rgb(239, 68, 68);
349
+ }
350
+
351
+ .annotation-cancel:hover {
352
+ background: rgba(239, 68, 68, 0.3);
353
+ }
354
+
355
+ .annotation-display {
356
+ margin-top: 8px;
357
+ padding: 8px 12px;
358
+ background: rgba(34, 197, 94, 0.1);
359
+ border: 1px solid rgba(34, 197, 94, 0.2);
360
+ border-radius: 8px;
361
+ font-size: 0.75rem;
362
+ color: rgba(255, 255, 255, 0.8);
363
+ line-height: 1.4;
364
+ }
365
+
366
  .form-input:focus {
367
  background: linear-gradient(135deg,
368
  rgba(255, 255, 255, 0.25) 0%,
369
  rgba(255, 255, 255, 0.18) 100%);
370
+ border-color: rgba(255, 255, 255, 0.6);
371
  box-shadow:
372
+ 0 0 0 2px rgba(255, 255, 255, 0.1),
373
+ 0 4px 16px rgba(31, 38, 135, 0.3),
374
  inset 0 1px 0 rgba(255, 255, 255, 0.5);
375
+ transform: translateY(-1px);
376
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
377
+ outline: none;
378
  }
379
 
380
  /* Enhanced dropdown with better readability */
 
398
  background: linear-gradient(135deg,
399
  rgba(255, 255, 255, 0.25) 0%,
400
  rgba(255, 255, 255, 0.18) 100%);
401
+ border-color: rgba(255, 255, 255, 0.6);
402
  box-shadow:
403
+ 0 0 0 2px rgba(255, 255, 255, 0.1),
404
+ 0 4px 16px rgba(31, 38, 135, 0.3),
405
  inset 0 1px 0 rgba(255, 255, 255, 0.5);
406
+ transform: translateY(-1px);
407
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
408
  outline: none;
409
  }
 
467
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
468
  }
469
 
470
+ /* Simple, fast collapsible */
471
  .fieldset-content {
 
472
  overflow: hidden;
473
+ }
474
+
475
+ .fieldset-content.expanded {
476
+ max-height: 500px;
477
+ overflow-y: auto;
478
+ display: block;
479
  }
480
 
481
  .fieldset-content.collapsed {
482
  max-height: 0;
483
+ overflow: hidden;
484
+ display: none;
 
 
 
485
  }
486
 
487
+ /* Custom scrollbar for fieldset content */
488
+ .fieldset-content::-webkit-scrollbar {
489
+ width: 6px;
490
+ }
491
+ .fieldset-content::-webkit-scrollbar-track {
492
+ background: rgba(148, 163, 184, 0.1);
493
+ border-radius: 3px;
494
+ }
495
+ .fieldset-content::-webkit-scrollbar-thumb {
496
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
497
+ border-radius: 3px;
498
+ }
499
+ .fieldset-content::-webkit-scrollbar-thumb:hover {
500
+ background: linear-gradient(135deg, #4f46e5, #7c3aed);
501
  }
502
 
503
+ /* Arrow rotation - no animation */
504
  .collapse-arrow {
505
+ /* No transition for instant feedback */
506
  }
507
 
508
  .collapse-arrow.collapsed {
 
582
  margin-top: 0.5rem;
583
  }
584
 
585
+ /* Side Drawer Styles */
586
+ .drawer-overlay {
587
+ position: fixed;
588
+ top: 0;
589
+ left: 0;
590
+ width: 100%;
591
+ height: 100%;
592
+ background: rgba(0, 0, 0, 0.5);
593
+ backdrop-filter: blur(4px);
594
+ z-index: 50;
595
+ opacity: 0;
596
+ visibility: hidden;
597
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
598
+ }
599
+
600
+
601
+ .drawer-overlay.open {
602
+ opacity: 1;
603
+ visibility: visible;
604
+ }
605
+
606
+ .drawer {
607
+ position: fixed;
608
+ top: 0;
609
+ right: 0;
610
+ width: 500px;
611
+ height: 100%;
612
+ background: linear-gradient(135deg,
613
+ rgba(15, 23, 42, 0.95) 0%,
614
+ rgba(15, 23, 42, 0.9) 100%);
615
+ backdrop-filter: blur(25px) saturate(200%);
616
+ border-left: 1px solid rgba(255, 255, 255, 0.2);
617
+ box-shadow: -8px 0 32px rgba(0, 0, 0, 0.3);
618
+ transform: translateX(100%);
619
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
620
+ z-index: 51;
621
+ display: flex;
622
+ flex-direction: column;
623
+ }
624
+
625
+ .drawer.open {
626
+ transform: translateX(0);
627
+ }
628
+
629
+ .drawer-header {
630
+ padding: 24px;
631
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
632
+ flex-shrink: 0;
633
+ }
634
+
635
+ .drawer-content {
636
+ flex: 1;
637
+ overflow-y: auto;
638
+ padding: 24px;
639
+ }
640
+
641
+ .drawer-footer {
642
+ padding: 24px;
643
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
644
+ flex-shrink: 0;
645
+ display: flex;
646
+ gap: 12px;
647
+ justify-content: flex-end;
648
+ }
649
+
650
+ /* Drawer scrollbar */
651
+ .drawer-content::-webkit-scrollbar {
652
+ width: 6px;
653
+ }
654
+
655
+ .drawer-content::-webkit-scrollbar-track {
656
+ background: rgba(148, 163, 184, 0.1);
657
+ border-radius: 3px;
658
+ }
659
+
660
+ .drawer-content::-webkit-scrollbar-thumb {
661
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
662
+ border-radius: 3px;
663
+ }
664
+
665
+
666
+ /* Edit button for complex sections - same style as preset buttons */
667
+ .edit-button {
668
+ display: inline-flex;
669
+ align-items: center;
670
+ gap: 6px;
671
+ padding: 8px 12px;
672
+ font-size: 0.75rem;
673
+ font-weight: 600;
674
+ color: white;
675
+ background: linear-gradient(135deg,
676
+ rgba(255, 255, 255, 0.1) 0%,
677
+ rgba(255, 255, 255, 0.05) 100%);
678
+ backdrop-filter: blur(10px) saturate(180%);
679
+ -webkit-backdrop-filter: blur(10px) saturate(180%);
680
+ box-shadow:
681
+ 0 4px 16px rgba(31, 38, 135, 0.2),
682
+ inset 0 1px 0 rgba(255, 255, 255, 0.3);
683
+ border: 1px solid rgba(255, 255, 255, 0.2);
684
+ border-radius: 12px;
685
+ cursor: pointer;
686
+ transition: all 0.2s;
687
+ }
688
+
689
+ .edit-button:hover {
690
+ background: linear-gradient(135deg,
691
+ rgba(255, 255, 255, 0.15) 0%,
692
+ rgba(255, 255, 255, 0.08) 100%);
693
+ box-shadow:
694
+ 0 6px 20px rgba(31, 38, 135, 0.3),
695
+ 0 0 0 2px rgba(255, 255, 255, 0.2),
696
+ inset 0 1px 0 rgba(255, 255, 255, 0.4);
697
+ transform: translateY(-2px) scale(1.05);
698
+ }
699
+
700
  /* Animated status indicator */
701
  .status-indicator {
702
  position: relative;
 
764
  rgba(15, 23, 42, 0.4) 100%);
765
  border-color: rgba(255, 255, 255, 0.3);
766
  }
767
+
768
+ /* CodeMirror customization for glassmorphism theme */
769
+ .CodeMirror {
770
+ background: transparent !important;
771
+ color: #e2e8f0 !important;
772
+ font-family: 'JetBrains Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace !important;
773
+ font-size: 14px !important;
774
+ line-height: 24px !important;
775
+ height: 100% !important;
776
+ border: none !important;
777
+ padding: 32px !important;
778
+ }
779
+
780
+ .CodeMirror-lines {
781
+ padding: 0 !important;
782
+ }
783
+
784
+ .CodeMirror pre {
785
+ line-height: 24px !important;
786
+ padding: 0 !important;
787
+ }
788
+
789
+ .CodeMirror-focused .CodeMirror-selected {
790
+ background: rgba(99, 102, 241, 0.2) !important;
791
+ }
792
+
793
+ .CodeMirror-selected {
794
+ background: rgba(99, 102, 241, 0.1) !important;
795
+ }
796
+
797
+ .CodeMirror-cursor {
798
+ border-left: 2px solid #6366f1 !important;
799
+ }
800
+
801
+ /* Custom line numbers styling */
802
+ #line-numbers {
803
+ user-select: none;
804
+ line-height: 24px;
805
+ pointer-events: auto; /* Allow clicking fold arrows */
806
+ }
807
+
808
+ #line-numbers .line-num {
809
+ padding-right: 8px;
810
+ font-size: 12px;
811
+ color: rgba(255, 255, 255, 0.4);
812
+ pointer-events: none; /* Prevent selection on numbers */
813
+ }
814
+
815
+
816
+ .CodeMirror-activeline-background {
817
+ background: rgba(255, 255, 255, 0.05) !important;
818
+ }
819
+
820
+ /* YAML syntax highlighting colors for dark glassmorphism theme */
821
+ .cm-property {
822
+ color: #60a5fa !important; /* Blue for properties */
823
+ }
824
+
825
+ .cm-string {
826
+ color: #34d399 !important; /* Green for strings */
827
+ }
828
+
829
+ .cm-number {
830
+ color: #fbbf24 !important; /* Yellow for numbers */
831
+ }
832
+
833
+ .cm-comment {
834
+ color: rgba(255, 255, 255, 0.5) !important; /* Muted for comments */
835
+ font-style: italic;
836
+ }
837
+
838
+ .cm-atom {
839
+ color: #f472b6 !important; /* Pink for booleans/null */
840
+ }
841
+
842
+
843
+ /* Folded code indicator */
844
+ .CodeMirror-foldmarker {
845
+ background: rgba(99, 102, 241, 0.2);
846
+ border: 1px solid rgba(99, 102, 241, 0.4);
847
+ color: rgba(255, 255, 255, 0.8);
848
+ border-radius: 4px;
849
+ padding: 0 4px;
850
+ margin: 0 2px;
851
+ font-size: 11px;
852
+ cursor: pointer;
853
+ }
854
+
855
+ .CodeMirror-foldmarker:hover {
856
+ background: rgba(99, 102, 241, 0.3);
857
+ }
858
+
859
+ /* Style for inline fold arrows */
860
+ .cm-fold-arrow {
861
+ color: rgba(99, 102, 241, 0.8) !important;
862
+ cursor: pointer !important;
863
+ padding: 2px !important;
864
+ border-radius: 2px !important;
865
+ background: rgba(99, 102, 241, 0.1) !important;
866
+ margin-left: 4px !important;
867
+ }
868
+
869
+ .cm-fold-arrow:hover {
870
+ background: rgba(99, 102, 241, 0.2) !important;
871
+ transform: scale(1.1) !important;
872
+ }
873
+
874
+ /* Hover cursor for lines with arrows */
875
+ .CodeMirror-line:has-text("▼"),
876
+ .CodeMirror-line:has-text("▶") {
877
+ cursor: pointer;
878
+ }
879
+
880
+ .cm-keyword {
881
+ color: #a78bfa !important; /* Purple for keywords */
882
+ }
883
+
884
+ .cm-variable {
885
+ color: #e2e8f0 !important; /* Light gray for variables */
886
+ }
887
+
888
+ .cm-def {
889
+ color: #60a5fa !important; /* Blue for definitions */
890
+ }
891
+
892
+ .cm-bracket {
893
+ color: rgba(255, 255, 255, 0.8) !important; /* Light for brackets */
894
+ }
895
+
896
+ .cm-tag {
897
+ color: #f87171 !important; /* Red for tags */
898
+ }
899
+
900
+ .cm-link {
901
+ color: #60a5fa !important; /* Blue for links */
902
+ }
903
+
904
+ .cm-error {
905
+ background: rgba(239, 68, 68, 0.2) !important;
906
+ color: #fca5a5 !important;
907
+ }
908
+
909
+ /* Scrollbar for CodeMirror */
910
+ .CodeMirror-scrollbar-filler {
911
+ background: transparent !important;
912
+ }
913
+
914
+ .CodeMirror-vscrollbar::-webkit-scrollbar {
915
+ width: 8px;
916
+ }
917
+
918
+ .CodeMirror-vscrollbar::-webkit-scrollbar-track {
919
+ background: rgba(148, 163, 184, 0.1);
920
+ border-radius: 4px;
921
+ }
922
+
923
+ .CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
924
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
925
+ border-radius: 4px;
926
+ }
927
+
928
+ .CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover {
929
+ background: linear-gradient(135deg, #4f46e5, #7c3aed);
930
+ }
931
+
932
+ .CodeMirror-hscrollbar::-webkit-scrollbar {
933
+ height: 8px;
934
+ }
935
+
936
+ .CodeMirror-hscrollbar::-webkit-scrollbar-track {
937
+ background: rgba(148, 163, 184, 0.1);
938
+ border-radius: 4px;
939
+ }
940
+
941
+ .CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
942
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
943
+ border-radius: 4px;
944
+ }
945
+
946
+ .CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover {
947
+ background: linear-gradient(135deg, #4f46e5, #7c3aed);
948
+ }
949
  </style>
950
  </head>
951
  <body class="h-full antialiased">
952
 
953
  <div id="app" class="flex flex-col h-full">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
954
 
955
  <!-- Preset Configuration Buttons -->
956
  <section class="border-b border-white/10 bg-gradient-to-r from-white/5 to-white/2 backdrop-blur-sm">
 
962
  <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>
963
  </div>
964
  <div class="flex flex-wrap gap-3">
965
+ <button id="preset-open-r1" 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">
966
  <div class="flex items-center space-x-2">
967
+ <span class="text-lg">🧠</span>
968
+ <span>Open-R1</span>
969
  </div>
970
  </button>
971
  <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">
 
1049
  <!-- Error/Success indicator -->
1050
  <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>
1051
  </div>
1052
+ <div class="flex-grow relative flex">
1053
+ <div id="line-numbers" class="flex-shrink-0 w-12 bg-slate-900/30 border-r border-white/10 text-right text-xs text-white/40 font-mono leading-6 pt-8 pr-2 overflow-hidden"></div>
1054
+ <div id="yaml-editor-container" class="flex-grow h-full"></div>
 
 
 
 
 
1055
  </div>
1056
  </div>
1057
  </main>
1058
  </div>
1059
 
1060
+ <!-- Side Drawer -->
1061
+ <div id="drawer-overlay" class="drawer-overlay" onclick="closeDrawer()"></div>
1062
+ <div id="drawer" class="drawer">
1063
+ <div class="drawer-header">
1064
+ <div class="flex items-center justify-between">
1065
+ <h2 id="drawer-title" class="text-xl font-bold text-white">Edit Section</h2>
1066
+ <button onclick="closeDrawer()" class="p-2 text-white/60 hover:text-white hover:bg-white/10 rounded-lg transition-all">
1067
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1068
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
1069
+ </svg>
1070
+ </button>
1071
+ </div>
1072
+ <p class="text-sm text-white/70 mt-2">Configure this section in detail</p>
1073
+ </div>
1074
+
1075
+ <div id="drawer-content" class="drawer-content">
1076
+ <!-- Dynamic content will be rendered here -->
1077
+ </div>
1078
+
1079
+ <div class="drawer-footer">
1080
+ <button onclick="closeDrawer()" class="px-4 py-2 text-sm font-semibold text-white/80 bg-white/10 border border-white/20 rounded-lg hover:bg-white/20 transition-all">
1081
+ Cancel
1082
+ </button>
1083
+ <button onclick="saveDrawer()" class="px-6 py-2 text-sm font-semibold text-white bg-gradient-to-r from-green-500 to-emerald-500 rounded-lg hover:from-green-600 hover:to-emerald-600 transition-all">
1084
+ Save Changes
1085
+ </button>
1086
+ </div>
1087
+ </div>
1088
+
1089
  <script>
1090
  document.addEventListener('DOMContentLoaded', () => {
1091
  // --- DOM Element References ---
1092
  const formContainer = document.getElementById('form-container');
1093
+ const yamlEditorContainer = document.getElementById('yaml-editor-container');
1094
  const statusIndicator = document.getElementById('status-indicator');
1095
+
1096
+ // --- Initialize CodeMirror ---
1097
+ const lineNumbersDiv = document.getElementById('line-numbers');
1098
+ const yamlEditor = CodeMirror(yamlEditorContainer, {
1099
+ mode: 'yaml',
1100
+ theme: 'material-darker',
1101
+ lineNumbers: false, // Disable built-in line numbers
1102
+ lineWrapping: true,
1103
+ indentUnit: 2,
1104
+ tabSize: 2,
1105
+ autoCloseBrackets: true,
1106
+ matchBrackets: true,
1107
+ showCursorWhenSelecting: true,
1108
+ styleActiveLine: true,
1109
+ readOnly: false, // Make sure editor is editable
1110
+ value: '',
1111
+ placeholder: '# dstack configuration\ntype: task\nname: my-awesome-task\npython: "3.11"\ncommands:\n - pip install requirements.txt\n - python main.py',
1112
+ extraKeys: {
1113
+ "Enter": function(cm) {
1114
+ // Check if current line has an arrow
1115
+ const cursor = cm.getCursor();
1116
+ const line = cm.getLine(cursor.line);
1117
+
1118
+ if (line && (line.includes(' ▼') || line.includes(' ▶'))) {
1119
+ // Toggle section instead of adding new line
1120
+ const cleanLine = line.replace(/ [▼▶]/g, '').trim();
1121
+ const sectionKey = cleanLine.replace(':', '');
1122
+
1123
+ if (collapsedSections.has(sectionKey)) {
1124
+ collapsedSections.delete(sectionKey);
1125
+ } else {
1126
+ collapsedSections.add(sectionKey);
1127
+ }
1128
+
1129
+ displayCollapsedYaml();
1130
+ return false; // Prevent default behavior
1131
+ }
1132
+
1133
+ return CodeMirror.Pass; // Allow normal Enter behavior
1134
+ }
1135
+ }
1136
+ });
1137
+
1138
+ // Simple collapsing state
1139
+ let collapsedSections = new Set();
1140
+ let isUpdatingDisplay = false;
1141
+ let originalContent = '';
1142
+ let originalSections = [];
1143
+
1144
+ // Get collapsible YAML sections
1145
+ const getCollapsibleSections = () => {
1146
+ const content = yamlEditor.getValue();
1147
+ const lines = content.split('\n');
1148
+ const sections = [];
1149
+
1150
+ for (let i = 0; i < lines.length; i++) {
1151
+ const line = lines[i];
1152
+ const trimmed = line.trim();
1153
+
1154
+ // Check if this is a YAML key that can be collapsed
1155
+ if (trimmed.endsWith(':') && !trimmed.startsWith('-') && !trimmed.startsWith('#')) {
1156
+ const currentIndent = line.length - line.trimStart().length;
1157
+ const childLines = [];
1158
+
1159
+ // Find child lines
1160
+ for (let j = i + 1; j < lines.length; j++) {
1161
+ const nextLine = lines[j];
1162
+ if (!nextLine || nextLine.trim() === '') continue;
1163
+
1164
+ const nextIndent = nextLine.length - nextLine.trimStart().length;
1165
+ if (nextIndent > currentIndent) {
1166
+ childLines.push(j);
1167
+ } else {
1168
+ break;
1169
+ }
1170
+ }
1171
+
1172
+ if (childLines.length > 0) {
1173
+ sections.push({
1174
+ headerLine: i,
1175
+ childLines: childLines,
1176
+ key: trimmed.replace(':', '')
1177
+ });
1178
+ }
1179
+ }
1180
+ }
1181
+ return sections;
1182
+ };
1183
+
1184
+ // Add arrows to YAML content without modifying the original
1185
+ const addArrowsToContent = () => {
1186
+ if (isUpdatingDisplay) return;
1187
+ isUpdatingDisplay = true;
1188
+
1189
+ const content = yamlEditor.getValue();
1190
+ console.log('Current content:', content);
1191
+
1192
+ if (content.includes(' ▼') || content.includes(' ▶')) {
1193
+ console.log('Already has arrows, skipping');
1194
+ isUpdatingDisplay = false;
1195
+ return; // Already has arrows
1196
+ }
1197
+
1198
+ // Store original content and sections for later use
1199
+ originalContent = content;
1200
+ originalSections = getCollapsibleSections();
1201
+ console.log('Stored original content and sections:', originalSections.length);
1202
+
1203
+ const lines = content.split('\n');
1204
+ const sections = originalSections;
1205
+
1206
+ console.log('Found sections:', sections);
1207
+
1208
+ sections.forEach(section => {
1209
+ const isCollapsed = collapsedSections.has(section.key);
1210
+ const arrow = isCollapsed ? ' ▶' : ' ▼';
1211
+ console.log(`Adding ${arrow} to line ${section.headerLine}: "${lines[section.headerLine]}"`);
1212
+ lines[section.headerLine] += arrow;
1213
+ });
1214
+
1215
+ const newContent = lines.join('\n');
1216
+ console.log('New content with arrows:', newContent);
1217
+
1218
+ if (newContent !== content) {
1219
+ // Temporarily disable change events
1220
+ yamlEditor.off('change');
1221
+ yamlEditor.setValue(newContent);
1222
+
1223
+ // Re-enable change events
1224
+ setTimeout(() => {
1225
+ yamlEditor.on('change', () => {
1226
+ if (!isUpdatingDisplay) {
1227
+ updateFormFromYaml();
1228
+ setTimeout(() => {
1229
+ addArrowsToContent();
1230
+ }, 200);
1231
+ }
1232
+ });
1233
+ isUpdatingDisplay = false;
1234
+ }, 50);
1235
+ } else {
1236
+ isUpdatingDisplay = false;
1237
+ }
1238
+
1239
+ // Update line numbers
1240
+ const lineCount = yamlEditor.lineCount();
1241
+ let lineNumbersHTML = '';
1242
+ for (let i = 1; i <= lineCount; i++) {
1243
+ lineNumbersHTML += `<div class="line-num" style="height: 24px;">${i}</div>`;
1244
+ }
1245
+ lineNumbersDiv.innerHTML = lineNumbersHTML;
1246
+ };
1247
+
1248
+ // Toggle collapse and update arrows
1249
+ const toggleSection = (sectionKey) => {
1250
+ console.log('Toggling section:', sectionKey);
1251
+
1252
+ if (collapsedSections.has(sectionKey)) {
1253
+ collapsedSections.delete(sectionKey);
1254
+ } else {
1255
+ collapsedSections.add(sectionKey);
1256
+ }
1257
+
1258
+ // Update the display with collapsed content
1259
+ updateCollapsedDisplay();
1260
+ };
1261
+
1262
+ // Update display with collapsed content
1263
+ const updateCollapsedDisplay = () => {
1264
+ if (isUpdatingDisplay) return;
1265
+ isUpdatingDisplay = true;
1266
+
1267
+ console.log('=== UPDATE COLLAPSED DISPLAY ===');
1268
+ // Use original content for expansion, current content for initial parsing
1269
+ const content = originalContent || yamlEditor.getValue();
1270
+ const lines = content.split('\n');
1271
+ const sections = originalSections.length > 0 ? originalSections : getCollapsibleSections();
1272
+ const displayLines = [];
1273
+
1274
+ console.log('Current collapsed sections:', Array.from(collapsedSections));
1275
+ console.log('Available sections:', sections.map(s => s.key));
1276
+ console.log('Using original content:', !!originalContent);
1277
+
1278
+ for (let i = 0; i < lines.length; i++) {
1279
+ const section = sections.find(s => s.headerLine === i);
1280
+
1281
+ if (section) {
1282
+ // This is a collapsible section header
1283
+ const isCollapsed = collapsedSections.has(section.key);
1284
+ const arrow = isCollapsed ? ' ▶' : ' ▼';
1285
+
1286
+ console.log(`Section ${section.key} at line ${i}: isCollapsed=${isCollapsed}, childLines=${JSON.stringify(section.childLines)}`);
1287
+
1288
+ // Always show the header with appropriate arrow
1289
+ const cleanLine = lines[i].replace(/ [▼▶]/g, '');
1290
+ displayLines.push(cleanLine + arrow);
1291
+
1292
+ // Add child lines only if not collapsed
1293
+ if (!isCollapsed) {
1294
+ console.log(`Adding child lines for ${section.key}:`, section.childLines);
1295
+ section.childLines.forEach(childLineIndex => {
1296
+ if (lines[childLineIndex]) {
1297
+ console.log(` Adding child line ${childLineIndex}: ${lines[childLineIndex]}`);
1298
+ displayLines.push(lines[childLineIndex]);
1299
+ }
1300
+ });
1301
+ } else {
1302
+ console.log(`Skipping child lines for collapsed section ${section.key}`);
1303
+ }
1304
+
1305
+ // Skip the child lines in the main loop
1306
+ i = section.childLines.length > 0 ? Math.max(...section.childLines) : i;
1307
+ } else {
1308
+ // Regular line - check if it's a child of a collapsed section
1309
+ const parentSection = sections.find(s => s.childLines.includes(i));
1310
+ if (!parentSection || !collapsedSections.has(parentSection.key)) {
1311
+ displayLines.push(lines[i]);
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ const newContent = displayLines.join('\n');
1317
+ const cursor = yamlEditor.getCursor();
1318
+
1319
+ console.log('Final display lines:', displayLines);
1320
+ console.log('New content length:', newContent.length);
1321
+
1322
+ // Prevent triggering change events during update
1323
+ yamlEditor.off('change');
1324
+ yamlEditor.setValue(newContent);
1325
+ yamlEditor.setCursor(Math.min(cursor.line, displayLines.length - 1), cursor.ch);
1326
+
1327
+ // Re-enable change events
1328
+ setTimeout(() => {
1329
+ yamlEditor.on('change', () => {
1330
+ if (!isUpdatingDisplay) {
1331
+ updateFormFromYaml();
1332
+ setTimeout(() => {
1333
+ addArrowsToContent();
1334
+ }, 200);
1335
+ }
1336
+ });
1337
+ isUpdatingDisplay = false;
1338
+ }, 50);
1339
+
1340
+ // Update line numbers
1341
+ const lineCount = displayLines.length;
1342
+ let lineNumbersHTML = '';
1343
+ for (let i = 1; i <= lineCount; i++) {
1344
+ lineNumbersHTML += `<div class="line-num" style="height: 24px;">${i}</div>`;
1345
+ }
1346
+ lineNumbersDiv.innerHTML = lineNumbersHTML;
1347
+ };
1348
+
1349
+ // Use DOM-based click detection instead of CodeMirror coordinates
1350
+ yamlEditorContainer.addEventListener('click', (event) => {
1351
+ console.log('=== DOM CLICK DEBUG ===');
1352
+
1353
+ // Find which line element was clicked
1354
+ const target = event.target;
1355
+ const lineElement = target.closest('.CodeMirror-line');
1356
+
1357
+ if (lineElement) {
1358
+ // Get the text content of the clicked line
1359
+ const lineText = lineElement.textContent || lineElement.innerText;
1360
+ console.log('Clicked line text:', JSON.stringify(lineText));
1361
+ console.log('Has ▼?', lineText.includes('▼'));
1362
+ console.log('Has ▶?', lineText.includes('▶'));
1363
+
1364
+ if (lineText.includes('▼') || lineText.includes('▶')) {
1365
+ console.log('Arrow detected in clicked line!');
1366
+ event.preventDefault();
1367
+ event.stopPropagation();
1368
+
1369
+ // Extract section key from the line text
1370
+ const cleanLine = lineText.replace(/[▼▶]/g, '').trim();
1371
+ const sectionKey = cleanLine.replace(':', '');
1372
+
1373
+ console.log('Raw line text:', JSON.stringify(lineText));
1374
+ console.log('Clean line:', JSON.stringify(cleanLine));
1375
+ console.log('Section key:', JSON.stringify(sectionKey));
1376
+ console.log('Current collapsed sections before toggle:', Array.from(collapsedSections));
1377
+
1378
+ toggleSection(sectionKey);
1379
+
1380
+ console.log('Current collapsed sections after toggle:', Array.from(collapsedSections));
1381
+ return false;
1382
+ } else {
1383
+ console.log('No arrow in clicked line');
1384
+ }
1385
+ } else {
1386
+ console.log('Could not find line element');
1387
+ }
1388
+ });
1389
+
1390
+ // Keep the CodeMirror handler as backup
1391
+ yamlEditor.on('mousedown', (cm, event) => {
1392
+ // This is now just for debugging
1393
+ const pos = cm.coordsChar({left: event.clientX, top: event.clientY});
1394
+ console.log('CodeMirror detected click on line:', pos.line, 'Content:', cm.getLine(pos.line));
1395
+ });
1396
+
1397
+ // Handle cursor change on hover
1398
+ yamlEditor.on('mousemove', (cm, event) => {
1399
+ const pos = cm.coordsChar({left: event.clientX, top: event.clientY});
1400
+ const line = cm.getLine(pos.line);
1401
+
1402
+ if (line && (line.includes(' ▼') || line.includes(' ▶'))) {
1403
+ yamlEditorContainer.style.cursor = 'pointer';
1404
+ } else {
1405
+ yamlEditorContainer.style.cursor = 'text';
1406
+ }
1407
+ });
1408
+
1409
+ // Auto-collapse all sections by default
1410
+ const autoCollapseAll = () => {
1411
+ const sections = getCollapsibleSections();
1412
+ sections.forEach(section => {
1413
+ collapsedSections.add(section.key);
1414
+ });
1415
+ updateCollapsedDisplay();
1416
+ };
1417
+
1418
+
1419
+ // Sync scrolling between line numbers and editor
1420
+ yamlEditor.on('scroll', () => {
1421
+ const scrollInfo = yamlEditor.getScrollInfo();
1422
+ lineNumbersDiv.scrollTop = scrollInfo.top;
1423
+ });
1424
+
1425
+ // Update display when content changes
1426
+ yamlEditor.on('change', () => {
1427
+ if (!isUpdatingDisplay) {
1428
+ updateFormFromYaml();
1429
+ // Add arrows after changes (but not too frequently)
1430
+ setTimeout(() => {
1431
+ addArrowsToContent();
1432
+ }, 200);
1433
+ }
1434
+ });
1435
+
1436
+ // Initial display - don't add arrows yet, wait for template to load
1437
+ console.log('Waiting for template to load before adding arrows...');
1438
+
1439
+ // Also add a global function to test manually
1440
+ window.testArrows = () => {
1441
+ console.log('Manual arrow test...');
1442
+ addArrowsToContent();
1443
+ };
1444
+
1445
+ // Add a simple test function to directly toggle sections
1446
+ window.testToggle = (sectionKey) => {
1447
+ console.log('=== MANUAL TOGGLE TEST ===');
1448
+ console.log('Toggling section:', sectionKey);
1449
+ console.log('Before:', Array.from(collapsedSections));
1450
+ toggleSection(sectionKey);
1451
+ console.log('After:', Array.from(collapsedSections));
1452
+ };
1453
 
1454
  // --- dstack Templates ---
1455
  const dstackTemplates = {
 
1510
  // --- Preset Button Event Handlers ---
1511
  const loadPresetConfig = (presetType) => {
1512
  if (presetConfigs[presetType]) {
1513
+ if (yamlEditor.getValue().trim() === '' || confirm('This will replace the current content. Are you sure?')) {
1514
+ yamlEditor.setValue(presetConfigs[presetType]);
1515
  updateFormFromYaml();
1516
+ // Auto-fold after preset load
1517
+ setTimeout(autoFoldAll, 150);
1518
  }
1519
  }
1520
  };
1521
 
1522
  // --- Mock Preset Configurations ---
1523
  const presetConfigs = {
1524
+ 'open-r1': `type: task
1525
+ name: open-r1
1526
+ python: "3.11"
1527
+ env:
1528
+ - HF_TOKEN
1529
+ - HUGGINGFACE_TOKEN
1530
+ - WANDB_API_KEY
1531
+ - E2B_API_KEY
1532
+ - TARGET_BASE_MODEL
1533
+ - TARGET_YAML
1534
+ - TARGET_REWARD
1535
+ commands:
1536
+ - apt-get update && apt-get install -y wget gnupg
1537
+ - wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.1-1_all.deb
1538
+ - dpkg -i cuda-keyring_1.1-1_all.deb
1539
+ - rm -f /etc/apt/sources.list.d/cuda*.list
1540
+ - wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-ubuntu2004.pin
1541
+ - mv cuda-ubuntu2004.pin /etc/apt/preferences.d/cuda-repository-pin-600
1542
+ - wget https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda-repo-ubuntu2004-12-4-local_12.4.0-550.54.14-1_amd64.deb
1543
+ - dpkg -i cuda-repo-ubuntu2004-12-4-local_12.4.0-550.54.14-1_amd64.deb
1544
+ - cp /var/cuda-repo-ubuntu2004-12-4-local/cuda-*-keyring.gpg /usr/share/keyrings/
1545
+ - apt-get update
1546
+ - apt-get install -y cuda-toolkit-12-4
1547
+ - echo 'export PATH=/usr/local/cuda-12.4/bin:$PATH' >> ~/.bashrc
1548
+ - echo 'export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
1549
+ - source ~/.bashrc
1550
+ - nvcc --version
1551
+ - curl -LsSf https://astral.sh/uv/install.sh | sh
1552
+ - source ~/.bashrc
1553
+ - uv venv openr1 --python 3.11 && source openr1/bin/activate && uv pip install --upgrade pip
1554
+ - uv pip install vllm==0.8.4
1555
+ - uv pip install setuptools && uv pip install flash-attn --no-build-isolation
1556
+ - git clone https://github.com/deep-diver/open-r1.git
1557
+ - cp $TARGET_YAML open-r1/recipes/custom.yaml
1558
+ - cat open-r1/recipes/custom.yaml
1559
+ - cp $TARGET_REWARD open-r1/src/open_r1/code_rewards.py
1560
+ - cat open-r1/src/open_r1/code_rewards.py
1561
+ - cd open-r1
1562
+ - echo "E2B_API_KEY=$E2B_API_KEY" >> ".env"
1563
+ - cat .env
1564
+ - GIT_LFS_SKIP_SMUDGE=1 uv pip install -e ".[dev]"
1565
+ - git clone https://github.com/deep-diver/trl.git
1566
+ - cd trl
1567
+ - uv pip install .
1568
+ - nohup bash -c 'CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model "$TARGET_BASE_MODEL"' > vllm.log 2>&1 &
1569
+ - sleep 420
1570
+ - cd ..
1571
+ - CUDA_VISIBLE_DEVICES=1,2,3,4,5,6,7 ACCELERATE_LOG_LEVEL=info accelerate launch --config_file recipes/accelerate_configs/zero2.yaml --num_processes=7 src/open_r1/grpo.py --config recipes/custom.yaml
1572
+ - pkill -f 'trl vllm-serve'
1573
+ resources:
1574
+ gpu: 80GB:8
1575
+ disk: 600GB
1576
+ shm_size: 2GB`,
1577
+
1578
  'llm-training': `type: task
1579
  name: llm-training
1580
  python: "3.11"
 
1875
  else if (key === 'image') placeholder = 'ghcr.io/huggingface/transformers';
1876
  else if (key === 'value') placeholder = 'Enter value...';
1877
 
1878
+ // Calculate rows based on content
1879
+ const lines = value.toString().split('\n').length;
1880
+ const rows = Math.max(1, lines);
1881
+
1882
  inputHtml = `
1883
  <div class="relative">
1884
+ <textarea data-path="${path}" id="${id}" class="${inputClasses} pl-12 min-h-[2.5rem] resize-y auto-resize" placeholder="${placeholder}" rows="${rows}">${value}</textarea>
1885
+ <div class="absolute top-3 left-0 pl-3 flex items-center pointer-events-none text-white/60">
1886
  ${fieldIcon}
1887
  </div>
1888
  </div>`;
 
1974
  Object.entries(data).forEach(([key, value]) => {
1975
  const currentPath = parentPath ? `${parentPath}.${key}` : key;
1976
  if (typeof value === 'object' && value !== null) {
1977
+ // Check if this is a complex section that should use drawer editing
1978
+ const complexSections = ['commands', 'env', 'resources', 'model', 'rate_limits', 'ports'];
1979
+ const isComplexSection = complexSections.includes(key.toLowerCase());
1980
+
1981
+ if (isComplexSection) {
1982
+ // Create simple summary with edit button
1983
+ const summaryContainer = document.createElement('div');
1984
+ summaryContainer.className = "mb-4 group";
1985
+ const summaryText = generateSummary(value, currentPath);
1986
+ summaryContainer.innerHTML = `
1987
+ <div class="flex items-center justify-between mb-2">
1988
+ <label class="flex items-center space-x-2 text-sm font-bold text-white capitalize">
1989
+ <div class="text-white/80" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">
1990
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1991
+ <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>
1992
+ </svg>
1993
+ </div>
1994
+ <span style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7); font-weight: 600;">${key.replace(/_/g, ' ')}</span>
1995
+ </label>
1996
+ <button class="edit-button" onclick="openDrawer('${currentPath}', '${key}')">
1997
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1998
+ <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.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>
1999
  </svg>
2000
+ Edit
2001
+ </button>
2002
+ </div>
2003
+ <div class="glass border border-white/30 rounded-xl p-4">
2004
+ <div class="text-sm text-white/70" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">
2005
+ ${summaryText || 'Click Edit to configure'}
2006
  </div>
2007
  </div>
2008
+ `;
2009
+ parentElement.appendChild(summaryContainer);
2010
+ } else {
2011
+ // Regular collapsible section for simple objects
2012
+ const groupContainer = document.createElement('div');
2013
+ groupContainer.className = "mb-4 group";
2014
+ const summaryText = generateSummary(value, currentPath);
2015
+ groupContainer.innerHTML = `
2016
+ <div class="flex items-center space-x-2 text-sm font-bold text-white capitalize mb-2 group-hover:text-white transition-colors cursor-pointer" onclick="toggleFieldset(this)" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7); font-weight: 600;">
2017
+ <svg class="w-4 h-4 collapse-arrow expanded" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2018
+ <path d="M19 9l-7 7-7-7"></path>
2019
+ </svg>
2020
+ <span>${key.replace(/_/g, ' ')}</span>
2021
+ </div>
2022
+ <div class="summary-preview" style="display: none; font-size: 0.75rem; color: rgba(255, 255, 255, 0.7); margin-bottom: 0.5rem; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">${summaryText}</div>
2023
+ <div class="fieldset-content expanded glass border border-white/30 rounded-xl p-4">
2024
+ </div>
2025
+ `;
2026
+ const contentDiv = groupContainer.querySelector('.fieldset-content');
2027
+ renderForm(value, contentDiv, currentPath);
2028
+ parentElement.appendChild(groupContainer);
2029
+ }
2030
  } else {
2031
  parentElement.innerHTML += createInputField(key, value, currentPath);
2032
  }
 
2040
  isUpdating = true;
2041
  try {
2042
  const yamlString = jsyaml.dump(currentData, { indent: 2 });
2043
+ yamlEditor.setValue(yamlString);
2044
+ // Auto-collapse sections after setting content
2045
+ setTimeout(() => {
2046
+ autoCollapseAll();
2047
+ }, 50);
2048
  statusIndicator.textContent = 'Synced';
2049
  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';
2050
+ yamlEditorContainer.classList.remove('border-red-500');
2051
  } catch (e) {
2052
  console.error("Error dumping YAML:", e);
2053
  }
 
2058
  if (isUpdating) return;
2059
  isUpdating = true;
2060
  try {
2061
+ const newData = jsyaml.load(yamlEditor.getValue());
2062
  if (JSON.stringify(newData) !== JSON.stringify(currentData)) {
2063
  currentData = newData || {};
2064
  formContainer.innerHTML = ''; // Clear previous form
2065
 
2066
  // Hide/show empty state based on content
2067
  const emptyState = document.getElementById('empty-state');
2068
+ if (Object.keys(currentData).length === 0 || yamlEditor.getValue().trim() === '') {
2069
  emptyState.style.display = 'flex';
2070
  formContainer.style.display = 'none';
2071
  } else {
 
2076
  }
2077
  statusIndicator.textContent = 'Valid';
2078
  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';
2079
+ yamlEditorContainer.classList.remove('border-red-500');
2080
  } catch (e) {
2081
  statusIndicator.textContent = 'Error';
2082
  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';
2083
+ yamlEditorContainer.classList.add('border-red-500');
2084
  console.warn("Invalid YAML:", e.message);
2085
  }
2086
  setTimeout(() => { isUpdating = false; }, 50);
 
2092
  };
2093
 
2094
  // --- Event Listeners ---
2095
+ // Change event is now handled in the CodeMirror initialization above
2096
 
2097
  formContainer.addEventListener('input', (e) => {
2098
  if (e.target.dataset.path) {
 
2141
  }
2142
  });
2143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2144
 
 
2145
  const generateSummary = (data, path) => {
2146
  if (Array.isArray(data)) {
2147
  if (data.length === 0) return "Empty";
2148
+ return `${data.length} item${data.length > 1 ? 's' : ''}`;
 
 
 
 
 
 
 
 
 
 
2149
  } else if (typeof data === 'object' && data !== null) {
2150
  const keys = Object.keys(data);
2151
  if (keys.length === 0) return "Empty";
2152
+ return `${keys.length} field${keys.length > 1 ? 's' : ''}: ${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2153
  }
2154
  return '';
2155
  };
2156
+
2157
 
2158
+ // --- Fast Collapsible Section Function ---
2159
+ window.toggleFieldset = function(headerElement) {
2160
+ const groupContainer = headerElement.closest('.group');
2161
+ const content = groupContainer.querySelector('.fieldset-content');
2162
+ const arrow = headerElement.querySelector('.collapse-arrow');
2163
+ const summary = groupContainer.querySelector('.summary-preview');
2164
  const isExpanded = content.classList.contains('expanded');
2165
 
2166
  if (isExpanded) {
2167
+ // Collapse - show summary, hide container
2168
  content.classList.remove('expanded');
2169
  content.classList.add('collapsed');
2170
  arrow.classList.remove('expanded');
2171
  arrow.classList.add('collapsed');
2172
+ if (summary) summary.style.display = 'block';
 
 
 
 
 
 
2173
  } else {
2174
+ // Expand - hide summary, show container
2175
  content.classList.remove('collapsed');
2176
  content.classList.add('expanded');
2177
  arrow.classList.remove('collapsed');
2178
  arrow.classList.add('expanded');
2179
+ if (summary) summary.style.display = 'none';
 
 
 
 
 
 
2180
  }
2181
  };
2182
 
2183
  // --- Preset Button Event Listeners ---
2184
+ document.getElementById('preset-open-r1').addEventListener('click', () => loadPresetConfig('open-r1'));
2185
  document.getElementById('preset-model-serving').addEventListener('click', () => loadPresetConfig('model-serving'));
2186
  document.getElementById('preset-jupyter-dev').addEventListener('click', () => loadPresetConfig('jupyter-dev'));
2187
  document.getElementById('preset-data-processing').addEventListener('click', () => loadPresetConfig('data-processing'));
2188
  document.getElementById('preset-web-app').addEventListener('click', () => loadPresetConfig('web-app'));
2189
  document.getElementById('preset-gpu-cluster').addEventListener('click', () => loadPresetConfig('gpu-cluster'));
2190
 
2191
+ // --- Annotation System ---
2192
+ let annotations = {}; // Store annotations by path
2193
+
2194
+ window.toggleAnnotation = function(btn, event) {
2195
+ event.preventDefault();
2196
+ event.stopPropagation();
2197
+ const path = btn.dataset.path;
2198
+ const popup = document.getElementById(`annotation-popup-${path}`);
2199
+ const isVisible = popup.classList.contains('show');
2200
+
2201
+ // Close all other popups
2202
+ document.querySelectorAll('.annotation-popup.show').forEach(p => {
2203
+ p.classList.remove('show');
2204
+ });
2205
+
2206
+ if (!isVisible) {
2207
+ popup.classList.add('show');
2208
+ const textarea = popup.querySelector('.annotation-input');
2209
+ textarea.value = annotations[path] || '';
2210
+ textarea.focus();
2211
+ }
2212
+ };
2213
+
2214
+ window.saveAnnotation = function(path) {
2215
+ const popup = document.getElementById(`annotation-popup-${path}`);
2216
+ const textarea = popup.querySelector('.annotation-input');
2217
+ const value = textarea.value.trim();
2218
+ const btn = document.querySelector(`[data-path="${path}"].annotation-btn`);
2219
+ const display = document.getElementById(`annotation-display-${path}`);
2220
+
2221
+ if (value) {
2222
+ annotations[path] = value;
2223
+ btn.classList.add('has-annotation');
2224
+ display.textContent = value;
2225
+ display.style.display = 'block';
2226
+ } else {
2227
+ delete annotations[path];
2228
+ btn.classList.remove('has-annotation');
2229
+ display.style.display = 'none';
2230
+ }
2231
+
2232
+ popup.classList.remove('show');
2233
+ };
2234
+
2235
+ window.cancelAnnotation = function(path) {
2236
+ const popup = document.getElementById(`annotation-popup-${path}`);
2237
+ popup.classList.remove('show');
2238
+ };
2239
+
2240
+ // Close annotation popups when clicking outside
2241
+ document.addEventListener('click', (e) => {
2242
+ if (!e.target.closest('.annotation-container')) {
2243
+ document.querySelectorAll('.annotation-popup.show').forEach(p => {
2244
+ p.classList.remove('show');
2245
+ });
2246
+ }
2247
+ });
2248
+
2249
+ // --- Drawer Management ---
2250
+ let currentDrawerPath = null;
2251
+ let currentDrawerKey = null;
2252
+ let drawerData = null;
2253
+
2254
+ window.openDrawer = function(path, key) {
2255
+ currentDrawerPath = path;
2256
+ currentDrawerKey = key;
2257
+
2258
+ // Get the data for this section
2259
+ const keys = path.split('.');
2260
+ let data = currentData;
2261
+ for (const k of keys) {
2262
+ if (data && typeof data === 'object') {
2263
+ data = data[k];
2264
+ }
2265
+ }
2266
+ drawerData = JSON.parse(JSON.stringify(data || {})); // Deep clone
2267
+
2268
+ // Update drawer content
2269
+ const drawerTitle = document.getElementById('drawer-title');
2270
+ const drawerContent = document.getElementById('drawer-content');
2271
+
2272
+ drawerTitle.textContent = `Edit ${key.replace(/_/g, ' ')}`;
2273
+
2274
+ // Simple drawer interface - just render the form
2275
+ drawerContent.innerHTML = '';
2276
+ renderForm(drawerData, drawerContent, '');
2277
+
2278
+ // Auto-resize all textareas after rendering
2279
+ setTimeout(() => {
2280
+ drawerContent.querySelectorAll('textarea.auto-resize').forEach(textarea => {
2281
+ autoResizeTextarea(textarea);
2282
+ });
2283
+ }, 50);
2284
+
2285
+ // Show drawer
2286
+ const overlay = document.getElementById('drawer-overlay');
2287
+ const drawer = document.getElementById('drawer');
2288
+ overlay.classList.add('open');
2289
+ drawer.classList.add('open');
2290
+ };
2291
+
2292
+ window.closeDrawer = function() {
2293
+ const overlay = document.getElementById('drawer-overlay');
2294
+ const drawer = document.getElementById('drawer');
2295
+ overlay.classList.remove('open');
2296
+ drawer.classList.remove('open');
2297
+
2298
+ currentDrawerPath = null;
2299
+ currentDrawerKey = null;
2300
+ drawerData = null;
2301
+ };
2302
+
2303
+ window.saveDrawer = function() {
2304
+ if (currentDrawerPath && drawerData) {
2305
+ // Update the main data
2306
+ setNestedValue(currentData, currentDrawerPath, drawerData);
2307
+
2308
+ // Update YAML and form
2309
+ updateYamlEditor();
2310
+ formContainer.innerHTML = '';
2311
+ renderForm(currentData, formContainer);
2312
+
2313
+ closeDrawer();
2314
+ }
2315
+ };
2316
+
2317
+ // Auto-resize textarea function
2318
+ const autoResizeTextarea = (textarea) => {
2319
+ textarea.style.height = 'auto';
2320
+ textarea.style.height = textarea.scrollHeight + 'px';
2321
+ };
2322
+
2323
+ // Handle drawer form changes
2324
+ document.addEventListener('input', (e) => {
2325
+ if (e.target.closest('#drawer-content') && e.target.dataset.path) {
2326
+ const path = e.target.dataset.path;
2327
+ let value = e.target.value;
2328
+ if (e.target.type === 'checkbox') {
2329
+ value = e.target.checked;
2330
+ } else if (e.target.type === 'number') {
2331
+ value = parseFloat(value) || 0;
2332
+ }
2333
+ setNestedValue(drawerData, path, value);
2334
+
2335
+ // Auto-resize textareas
2336
+ if (e.target.tagName === 'TEXTAREA' && e.target.classList.contains('auto-resize')) {
2337
+ autoResizeTextarea(e.target);
2338
+ }
2339
+ }
2340
+ });
2341
+
2342
+ // Handle drawer form clicks (add/remove buttons)
2343
+ document.addEventListener('click', (e) => {
2344
+ const button = e.target.closest('button');
2345
+ if (!button || !button.closest('#drawer-content')) return;
2346
+
2347
+ const path = button.dataset.path;
2348
+ if (button.classList.contains('remove-btn')) {
2349
+ if (confirm('Are you sure you want to remove this item?')) {
2350
+ deleteNestedValue(drawerData, path);
2351
+ const drawerContent = document.getElementById('drawer-content');
2352
+ drawerContent.innerHTML = '';
2353
+ renderForm(drawerData, drawerContent, '');
2354
+ }
2355
+ } else if (button.classList.contains('add-btn')) {
2356
+ let newItem;
2357
+ try {
2358
+ newItem = JSON.parse(button.dataset.template);
2359
+ } catch (e) {
2360
+ newItem = button.dataset.template;
2361
+ }
2362
+
2363
+ const currentArray = path ? drawerData[path] : drawerData;
2364
+ const newIndex = Array.isArray(currentArray) ? currentArray.length : 0;
2365
+ setNestedValue(drawerData, `${path ? path + '.' : ''}${newIndex}`, newItem);
2366
+
2367
+ const drawerContent = document.getElementById('drawer-content');
2368
+ drawerContent.innerHTML = '';
2369
+ renderForm(drawerData, drawerContent, '');
2370
+ }
2371
+ });
2372
+
2373
+
2374
  // --- Initial Load ---
2375
+ yamlEditor.setValue(dstackTemplates.task);
2376
  updateFormFromYaml();
2377
+
2378
+ // Add arrows and auto-collapse after template is loaded
2379
+ setTimeout(() => {
2380
+ console.log('Template loaded, now adding arrows...');
2381
+ addArrowsToContent();
2382
+
2383
+ setTimeout(() => {
2384
+ console.log('Auto-collapsing all sections...');
2385
+ autoCollapseAll();
2386
+ }, 100);
2387
+ }, 100);
2388
  });
2389
  </script>
2390