dstack-config-tmpl / index.html
chansung's picture
Update index.html
4bd799f verified
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dstack Configuration Editor</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Inter Font -->
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<!-- JS-YAML CDN -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js-yaml.min.js"></script>
<!-- CodeMirror for YAML syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-darker.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/yaml/yaml.min.js"></script>
<style>
/* Modern styling with animations and gradients */
html {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
scroll-behavior: smooth;
}
@supports (font-variation-settings: normal) {
html { font-family: 'Inter var', system-ui, -apple-system, sans-serif; }
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-attachment: fixed;
color: #1e293b;
min-height: 100vh;
position: relative;
}
/* Enhanced animated background with multiple layers */
body::after {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 200, 255, 0.2) 0%, transparent 50%);
pointer-events: none;
z-index: -1;
animation: float 20s ease-in-out infinite;
}
/* Animated background pattern */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
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");
pointer-events: none;
z-index: -1;
}
/* Beautiful custom scrollbar */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: rgba(148, 163, 184, 0.1);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 10px;
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #4f46e5, #7c3aed);
background-clip: content-box;
}
/* Form container bold text */
#form-container {
font-weight: bold;
}
/* YAML editor container overflow */
#yaml-editor-container {
overflow: auto;
}
/* Glass with lens distortion */
.glass {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0.06) 100%);
backdrop-filter: blur(16px) saturate(180%) contrast(120%);
-webkit-backdrop-filter: blur(16px) saturate(180%) contrast(120%);
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow:
0 8px 32px rgba(31, 38, 135, 0.37),
inset 0 1px 0 rgba(255, 255, 255, 0.5),
inset 0 -1px 0 rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
filter: contrast(1.1) brightness(1.05);
}
.glass-dark {
background: linear-gradient(135deg,
rgba(15, 23, 42, 0.75) 0%,
rgba(15, 23, 42, 0.55) 100%);
backdrop-filter: blur(20px) saturate(160%) contrast(110%);
-webkit-backdrop-filter: blur(20px) saturate(160%) contrast(110%);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -1px 0 rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
filter: contrast(1.08) brightness(1.02);
}
/* Lens magnification for cards */
.card {
box-shadow:
0 20px 60px rgba(31, 38, 135, 0.4),
0 10px 30px rgba(31, 38, 135, 0.3),
0 5px 15px rgba(31, 38, 135, 0.2);
transform: translateZ(0) scale(1);
filter: contrast(1.05) saturate(110%);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card:hover {
transform: translateZ(10px) translateY(-5px) scale(1.03);
filter: contrast(1.1) saturate(120%) brightness(1.05);
box-shadow:
0 30px 80px rgba(31, 38, 135, 0.5),
0 20px 50px rgba(31, 38, 135, 0.4),
0 10px 25px rgba(31, 38, 135, 0.3);
}
/* Lens focus buttons */
.btn-primary, .btn-secondary {
box-shadow:
0 8px 25px rgba(99, 102, 241, 0.4),
0 4px 15px rgba(99, 102, 241, 0.3),
0 2px 8px rgba(99, 102, 241, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
transform: translateZ(0) scale(1);
filter: contrast(1.1) saturate(120%);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-primary:hover, .btn-secondary:hover {
transform: translateZ(5px) translateY(-2px) scale(1.05);
filter: contrast(1.2) saturate(140%) brightness(1.1);
box-shadow:
0 12px 35px rgba(99, 102, 241, 0.5),
0 6px 20px rgba(99, 102, 241, 0.4),
0 3px 12px rgba(99, 102, 241, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
/* Lens refraction inputs */
.form-input, .form-select {
box-shadow:
inset 0 3px 8px rgba(0, 0, 0, 0.1),
inset 0 1px 4px rgba(0, 0, 0, 0.08),
0 2px 6px rgba(255, 255, 255, 0.1);
filter: contrast(1.05) saturate(105%);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.form-input:focus, .form-select:focus {
box-shadow:
inset 0 3px 8px rgba(0, 0, 0, 0.15),
inset 0 1px 4px rgba(0, 0, 0, 0.1),
0 4px 12px rgba(99, 102, 241, 0.2),
0 0 0 2px rgba(99, 102, 241, 0.3);
filter: contrast(1.1) saturate(115%) brightness(1.05);
transform: scale(1.01);
}
/* Glowing effects */
.glow {
box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
}
.glow:hover {
box-shadow: 0 0 30px rgba(99, 102, 241, 0.5);
transform: translateY(-2px);
}
/* Modern button styles */
.btn-primary {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border: none;
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
}
.btn-primary:hover {
background: linear-gradient(135deg, #4f46e5, #7c3aed);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.6);
transform: translateY(-1px);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
/* Enhanced form input with better readability */
.form-input {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.15) 0%,
rgba(255, 255, 255, 0.1) 100%);
border: 2px solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(10px) saturate(180%);
-webkit-backdrop-filter: blur(10px) saturate(180%);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
color: white;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
box-shadow:
0 4px 16px rgba(31, 38, 135, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
position: relative;
}
/* Annotation styles */
.annotation-container {
position: relative;
}
.annotation-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
z-index: 10;
background: rgba(99, 102, 241, 0.2);
border: 1px solid rgba(99, 102, 241, 0.4);
border-radius: 6px;
padding: 4px;
cursor: pointer;
transition: all 0.2s;
}
.annotation-btn:hover {
background: rgba(99, 102, 241, 0.3);
border-color: rgba(99, 102, 241, 0.6);
}
.annotation-btn.has-annotation {
background: rgba(34, 197, 94, 0.2);
border-color: rgba(34, 197, 94, 0.4);
}
.annotation-popup {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
width: 300px;
background: linear-gradient(135deg,
rgba(15, 23, 42, 0.95) 0%,
rgba(15, 23, 42, 0.9) 100%);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 20;
display: none;
}
.annotation-popup.show {
display: block;
animation: slideInUp 0.2s ease-out;
}
.annotation-input {
width: 100%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 8px 12px;
color: white;
font-size: 0.875rem;
resize: vertical;
min-height: 60px;
}
.annotation-input:focus {
outline: none;
border-color: rgba(99, 102, 241, 0.6);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
.annotation-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.annotation-save, .annotation-cancel {
padding: 6px 12px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.annotation-save {
background: rgba(34, 197, 94, 0.2);
border: 1px solid rgba(34, 197, 94, 0.4);
color: rgb(34, 197, 94);
}
.annotation-save:hover {
background: rgba(34, 197, 94, 0.3);
}
.annotation-cancel {
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.4);
color: rgb(239, 68, 68);
}
.annotation-cancel:hover {
background: rgba(239, 68, 68, 0.3);
}
.annotation-display {
margin-top: 8px;
padding: 8px 12px;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
border-radius: 8px;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.form-input:focus {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.25) 0%,
rgba(255, 255, 255, 0.18) 100%);
border-color: rgba(255, 255, 255, 0.6);
box-shadow:
0 0 0 2px rgba(255, 255, 255, 0.1),
0 4px 16px rgba(31, 38, 135, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
transform: translateY(-1px);
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
outline: none;
}
/* Enhanced dropdown with better readability */
.form-select {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.15) 0%,
rgba(255, 255, 255, 0.1) 100%);
border: 2px solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(10px) saturate(180%);
-webkit-backdrop-filter: blur(10px) saturate(180%);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
color: white;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
box-shadow:
0 4px 16px rgba(31, 38, 135, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
.form-select:focus {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.25) 0%,
rgba(255, 255, 255, 0.18) 100%);
border-color: rgba(255, 255, 255, 0.6);
box-shadow:
0 0 0 2px rgba(255, 255, 255, 0.1),
0 4px 16px rgba(31, 38, 135, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
transform: translateY(-1px);
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
outline: none;
}
.form-select option {
background: rgba(15, 23, 42, 0.95);
color: white;
padding: 8px 12px;
}
.form-select option:checked {
background: rgba(99, 102, 241, 0.8);
}
.form-select option:hover {
background: rgba(99, 102, 241, 0.6);
}
/* Remove default backgrounds from grouped components */
fieldset {
background-color: transparent !important;
background: none !important;
}
/* Override glass background for fieldsets - they should be transparent containers */
fieldset.glass {
background: transparent !important;
background-color: transparent !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
box-shadow: none !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
}
/* Keep fieldsets completely transparent even on hover */
fieldset.glass:hover {
background: transparent !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
/* Ensure legends have proper glass backgrounds */
fieldset legend {
background-color: transparent;
}
/* Global text readability improvements */
body, * {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Better text contrast for all white text */
.text-white {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
/* Enhanced placeholder text */
::placeholder {
color: rgba(255, 255, 255, 0.6);
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
/* Simple, fast collapsible */
.fieldset-content {
overflow: hidden;
}
.fieldset-content.expanded {
max-height: 500px;
overflow-y: auto;
display: block;
}
.fieldset-content.collapsed {
max-height: 0;
overflow: hidden;
display: none;
}
/* Custom scrollbar for fieldset content */
.fieldset-content::-webkit-scrollbar {
width: 6px;
}
.fieldset-content::-webkit-scrollbar-track {
background: rgba(148, 163, 184, 0.1);
border-radius: 3px;
}
.fieldset-content::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 3px;
}
.fieldset-content::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #4f46e5, #7c3aed);
}
/* Arrow rotation - no animation */
.collapse-arrow {
/* No transition for instant feedback */
}
.collapse-arrow.collapsed {
transform: rotate(-90deg);
}
.collapse-arrow.expanded {
transform: rotate(0deg);
}
/* Fieldset entrance animation */
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.fieldset-enter {
animation: slideInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Legend hover animation */
.legend-hover {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Preset buttons styling */
.glass-preset {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 100%);
backdrop-filter: blur(10px) saturate(180%);
-webkit-backdrop-filter: blur(10px) saturate(180%);
box-shadow:
0 4px 16px rgba(31, 38, 135, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.glass-preset:hover {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.15) 0%,
rgba(255, 255, 255, 0.08) 100%);
box-shadow:
0 6px 20px rgba(31, 38, 135, 0.3),
0 0 0 2px rgba(255, 255, 255, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
transform: translateY(-2px) scale(1.05);
}
.glass-preset:active {
transform: translateY(0) scale(1.02);
box-shadow:
0 2px 8px rgba(31, 38, 135, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
/* Summary preview styling */
.summary-preview {
opacity: 0;
max-height: 0;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 400;
margin-top: 0;
}
.summary-preview.show {
opacity: 1;
max-height: 100px;
margin-top: 0.5rem;
}
/* Side Drawer Styles */
.drawer-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 50;
opacity: 0;
visibility: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.drawer-overlay.open {
opacity: 1;
visibility: visible;
}
.drawer {
position: fixed;
top: 0;
right: 0;
width: 500px;
height: 100%;
background: linear-gradient(135deg,
rgba(15, 23, 42, 0.95) 0%,
rgba(15, 23, 42, 0.9) 100%);
backdrop-filter: blur(25px) saturate(200%);
border-left: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.3);
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 51;
display: flex;
flex-direction: column;
}
.drawer.open {
transform: translateX(0);
}
.drawer-header {
padding: 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
.drawer-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.drawer-footer {
padding: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* Drawer scrollbar */
.drawer-content::-webkit-scrollbar {
width: 6px;
}
.drawer-content::-webkit-scrollbar-track {
background: rgba(148, 163, 184, 0.1);
border-radius: 3px;
}
.drawer-content::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 3px;
}
/* Edit button for complex sections - same style as preset buttons */
.edit-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
font-size: 0.75rem;
font-weight: 600;
color: white;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 100%);
backdrop-filter: blur(10px) saturate(180%);
-webkit-backdrop-filter: blur(10px) saturate(180%);
box-shadow:
0 4px 16px rgba(31, 38, 135, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
}
.edit-button:hover {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.15) 0%,
rgba(255, 255, 255, 0.08) 100%);
box-shadow:
0 6px 20px rgba(31, 38, 135, 0.3),
0 0 0 2px rgba(255, 255, 255, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
transform: translateY(-2px) scale(1.05);
}
/* Animated status indicator */
.status-indicator {
position: relative;
overflow: hidden;
}
.status-indicator::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
/* Floating animation */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.float {
animation: float 6s ease-in-out infinite;
}
/* Gradient text */
.gradient-text {
background: linear-gradient(135deg, #6366f1, #8b5cf6, #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Enhanced card hover effects with glass morphism */
.card {
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.card:hover {
transform: translateY(-12px) scale(1.03);
box-shadow:
0 32px 64px rgba(31, 38, 135, 0.4),
0 16px 32px rgba(99, 102, 241, 0.2),
inset 0 2px 0 rgba(255, 255, 255, 0.6);
}
.card:hover .glass {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.15) 0%,
rgba(255, 255, 255, 0.08) 100%);
border-color: rgba(255, 255, 255, 0.4);
}
.card:hover .glass-dark {
background: linear-gradient(135deg,
rgba(15, 23, 42, 0.6) 0%,
rgba(15, 23, 42, 0.4) 100%);
border-color: rgba(255, 255, 255, 0.3);
}
/* CodeMirror customization for glassmorphism theme */
.CodeMirror {
background: transparent !important;
color: #e2e8f0 !important;
font-family: 'JetBrains Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace !important;
font-size: 14px !important;
line-height: 24px !important;
height: 100% !important;
border: none !important;
padding: 32px !important;
}
.CodeMirror-lines {
padding: 0 !important;
}
.CodeMirror pre {
line-height: 24px !important;
padding: 0 !important;
}
.CodeMirror-focused .CodeMirror-selected {
background: rgba(99, 102, 241, 0.2) !important;
}
.CodeMirror-selected {
background: rgba(99, 102, 241, 0.1) !important;
}
.CodeMirror-cursor {
border-left: 2px solid #6366f1 !important;
}
/* Custom line numbers styling */
#line-numbers {
user-select: none;
line-height: 24px;
pointer-events: auto; /* Allow clicking fold arrows */
}
#line-numbers .line-num {
padding-right: 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
pointer-events: none; /* Prevent selection on numbers */
}
.CodeMirror-activeline-background {
background: rgba(255, 255, 255, 0.05) !important;
}
/* YAML syntax highlighting colors for dark glassmorphism theme */
.cm-property {
color: #60a5fa !important; /* Blue for properties */
}
.cm-string {
color: #34d399 !important; /* Green for strings */
}
.cm-number {
color: #fbbf24 !important; /* Yellow for numbers */
}
.cm-comment {
color: rgba(255, 255, 255, 0.5) !important; /* Muted for comments */
font-style: italic;
}
.cm-atom {
color: #f472b6 !important; /* Pink for booleans/null */
}
/* Folded code indicator */
.CodeMirror-foldmarker {
background: rgba(99, 102, 241, 0.2);
border: 1px solid rgba(99, 102, 241, 0.4);
color: rgba(255, 255, 255, 0.8);
border-radius: 4px;
padding: 0 4px;
margin: 0 2px;
font-size: 11px;
cursor: pointer;
}
.CodeMirror-foldmarker:hover {
background: rgba(99, 102, 241, 0.3);
}
/* Style for inline fold arrows */
.cm-fold-arrow {
color: rgba(99, 102, 241, 0.8) !important;
cursor: pointer !important;
padding: 2px !important;
border-radius: 2px !important;
background: rgba(99, 102, 241, 0.1) !important;
margin-left: 4px !important;
}
.cm-fold-arrow:hover {
background: rgba(99, 102, 241, 0.2) !important;
transform: scale(1.1) !important;
}
/* Hover cursor for lines with arrows */
.CodeMirror-line:has-text("▼"),
.CodeMirror-line:has-text("▶") {
cursor: pointer;
}
.cm-keyword {
color: #a78bfa !important; /* Purple for keywords */
}
.cm-variable {
color: #e2e8f0 !important; /* Light gray for variables */
}
.cm-def {
color: #60a5fa !important; /* Blue for definitions */
}
.cm-bracket {
color: rgba(255, 255, 255, 0.8) !important; /* Light for brackets */
}
.cm-tag {
color: #f87171 !important; /* Red for tags */
}
.cm-link {
color: #60a5fa !important; /* Blue for links */
}
.cm-error {
background: rgba(239, 68, 68, 0.2) !important;
color: #fca5a5 !important;
}
/* Scrollbar for CodeMirror */
.CodeMirror-scrollbar-filler {
background: transparent !important;
}
.CodeMirror-vscrollbar::-webkit-scrollbar {
width: 8px;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-track {
background: rgba(148, 163, 184, 0.1);
border-radius: 4px;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 4px;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #4f46e5, #7c3aed);
}
.CodeMirror-hscrollbar::-webkit-scrollbar {
height: 8px;
}
.CodeMirror-hscrollbar::-webkit-scrollbar-track {
background: rgba(148, 163, 184, 0.1);
border-radius: 4px;
}
.CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 4px;
}
.CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #4f46e5, #7c3aed);
}
</style>
</head>
<body class="h-full antialiased">
<div id="app" class="flex flex-col h-full">
<!-- Preset Configuration Buttons -->
<section class="border-b border-white/10 bg-gradient-to-r from-white/5 to-white/2 backdrop-blur-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div class="flex items-center space-x-2 mb-3">
<svg class="w-5 h-5 text-white/70" 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>
<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>
</div>
<div class="flex flex-wrap gap-3">
<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">
<div class="flex items-center space-x-2">
<span class="text-lg">🧠</span>
<span>Open-R1</span>
</div>
</button>
<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">
<div class="flex items-center space-x-2">
<span class="text-lg">🚀</span>
<span>Model Serving</span>
</div>
</button>
<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">
<div class="flex items-center space-x-2">
<span class="text-lg">📊</span>
<span>Jupyter Dev</span>
</div>
</button>
<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">
<div class="flex items-center space-x-2">
<span class="text-lg"></span>
<span>Data Processing</span>
</div>
</button>
<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">
<div class="flex items-center space-x-2">
<span class="text-lg">🌐</span>
<span>Web App</span>
</div>
</button>
<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">
<div class="flex items-center space-x-2">
<span class="text-lg">🔥</span>
<span>GPU Cluster</span>
</div>
</button>
</div>
</div>
</section>
<!-- Main Content: Side-by-side editors -->
<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">
<!-- Left Side: Form-based UI Editor -->
<div class="flex flex-col glass rounded-2xl shadow-2xl ring-1 ring-white/20 overflow-hidden card glow">
<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">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-3 h-3 bg-gradient-to-r from-green-400 to-blue-500 rounded-full animate-pulse"></div>
<h2 class="text-xl font-bold text-white drop-shadow-lg">Visual Editor</h2>
</div>
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span class="text-xs text-white/70 font-medium">Live Preview</span>
</div>
</div>
<p class="text-sm text-white/80 mt-2 font-medium">Craft your dstack configuration with style</p>
</div>
<div class="flex-grow bg-gradient-to-b from-white/5 to-white/1 overflow-hidden">
<div id="form-container" class="h-full p-6 space-y-4 overflow-y-auto">
<!-- Dynamic form content will be injected here -->
</div>
<div id="empty-state" class="flex flex-col items-center justify-center h-64 text-center p-8" style="display: none;">
<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">
<svg class="w-8 h-8 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
</div>
<h3 class="text-lg font-semibold text-white/80 mb-2">Ready to Build</h3>
<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>
</div>
</div>
</div>
<!-- Right Side: Raw YAML Text Editor -->
<div class="flex flex-col glass-dark rounded-2xl shadow-2xl overflow-hidden card glow">
<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">
<div class="flex items-center space-x-3">
<div class="w-3 h-3 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full animate-pulse"></div>
<div>
<h2 class="text-xl font-bold text-white drop-shadow-lg">dstack.yml</h2>
<p class="text-sm text-white/70 mt-1 font-medium">Raw YAML configuration</p>
</div>
</div>
<!-- Error/Success indicator -->
<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>
</div>
<div class="flex-grow relative flex">
<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>
<div id="yaml-editor-container" class="flex-grow h-full"></div>
</div>
</div>
</main>
</div>
<!-- Side Drawer -->
<div id="drawer-overlay" class="drawer-overlay" onclick="closeDrawer()"></div>
<div id="drawer" class="drawer">
<div class="drawer-header">
<div class="flex items-center justify-between">
<h2 id="drawer-title" class="text-xl font-bold text-white">Edit Section</h2>
<button onclick="closeDrawer()" class="p-2 text-white/60 hover:text-white hover:bg-white/10 rounded-lg transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<p class="text-sm text-white/70 mt-2">Configure this section in detail</p>
</div>
<div id="drawer-content" class="drawer-content">
<!-- Dynamic content will be rendered here -->
</div>
<div class="drawer-footer">
<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">
Cancel
</button>
<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">
Save Changes
</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Element References ---
const formContainer = document.getElementById('form-container');
const yamlEditorContainer = document.getElementById('yaml-editor-container');
const statusIndicator = document.getElementById('status-indicator');
// --- Initialize CodeMirror ---
const lineNumbersDiv = document.getElementById('line-numbers');
const yamlEditor = CodeMirror(yamlEditorContainer, {
mode: 'yaml',
theme: 'material-darker',
lineNumbers: false, // Disable built-in line numbers
lineWrapping: true,
indentUnit: 2,
tabSize: 2,
autoCloseBrackets: true,
matchBrackets: true,
showCursorWhenSelecting: true,
styleActiveLine: true,
readOnly: false, // Make sure editor is editable
value: '',
placeholder: '# dstack configuration\ntype: task\nname: my-awesome-task\npython: "3.11"\ncommands:\n - pip install requirements.txt\n - python main.py',
extraKeys: {
"Enter": function(cm) {
// Check if current line has an arrow
const cursor = cm.getCursor();
const line = cm.getLine(cursor.line);
if (line && (line.includes(' ▼') || line.includes(' ▶'))) {
// Toggle section instead of adding new line
const cleanLine = line.replace(/ [▼▶]/g, '').trim();
const sectionKey = cleanLine.replace(':', '');
if (collapsedSections.has(sectionKey)) {
collapsedSections.delete(sectionKey);
} else {
collapsedSections.add(sectionKey);
}
displayCollapsedYaml();
return false; // Prevent default behavior
}
return CodeMirror.Pass; // Allow normal Enter behavior
}
}
});
// Simple collapsing state
let collapsedSections = new Set();
let isUpdatingDisplay = false;
let originalContent = '';
let originalSections = [];
// Get collapsible YAML sections
const getCollapsibleSections = () => {
const content = yamlEditor.getValue();
const lines = content.split('\n');
const sections = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
// Check if this is a YAML key that can be collapsed
if (trimmed.endsWith(':') && !trimmed.startsWith('-') && !trimmed.startsWith('#')) {
const currentIndent = line.length - line.trimStart().length;
const childLines = [];
// Find child lines
for (let j = i + 1; j < lines.length; j++) {
const nextLine = lines[j];
if (!nextLine || nextLine.trim() === '') continue;
const nextIndent = nextLine.length - nextLine.trimStart().length;
if (nextIndent > currentIndent) {
childLines.push(j);
} else {
break;
}
}
if (childLines.length > 0) {
sections.push({
headerLine: i,
childLines: childLines,
key: trimmed.replace(':', '')
});
}
}
}
return sections;
};
// Add arrows to YAML content without modifying the original
const addArrowsToContent = () => {
if (isUpdatingDisplay) return;
isUpdatingDisplay = true;
const content = yamlEditor.getValue();
console.log('Current content:', content);
if (content.includes(' ▼') || content.includes(' ▶')) {
console.log('Already has arrows, skipping');
isUpdatingDisplay = false;
return; // Already has arrows
}
// Store original content and sections for later use
originalContent = content;
originalSections = getCollapsibleSections();
console.log('Stored original content and sections:', originalSections.length);
const lines = content.split('\n');
const sections = originalSections;
console.log('Found sections:', sections);
sections.forEach(section => {
const isCollapsed = collapsedSections.has(section.key);
const arrow = isCollapsed ? ' ▶' : ' ▼';
console.log(`Adding ${arrow} to line ${section.headerLine}: "${lines[section.headerLine]}"`);
lines[section.headerLine] += arrow;
});
const newContent = lines.join('\n');
console.log('New content with arrows:', newContent);
if (newContent !== content) {
// Temporarily disable change events
yamlEditor.off('change');
yamlEditor.setValue(newContent);
// Re-enable change events
setTimeout(() => {
yamlEditor.on('change', () => {
if (!isUpdatingDisplay) {
updateFormFromYaml();
setTimeout(() => {
addArrowsToContent();
}, 200);
}
});
isUpdatingDisplay = false;
}, 50);
} else {
isUpdatingDisplay = false;
}
// Update line numbers
const lineCount = yamlEditor.lineCount();
let lineNumbersHTML = '';
for (let i = 1; i <= lineCount; i++) {
lineNumbersHTML += `<div class="line-num" style="height: 24px;">${i}</div>`;
}
lineNumbersDiv.innerHTML = lineNumbersHTML;
};
// Toggle collapse and update arrows
const toggleSection = (sectionKey) => {
console.log('Toggling section:', sectionKey);
if (collapsedSections.has(sectionKey)) {
collapsedSections.delete(sectionKey);
} else {
collapsedSections.add(sectionKey);
}
// Update the display with collapsed content
updateCollapsedDisplay();
};
// Update display with collapsed content
const updateCollapsedDisplay = () => {
if (isUpdatingDisplay) return;
isUpdatingDisplay = true;
console.log('=== UPDATE COLLAPSED DISPLAY ===');
// Use original content for expansion, current content for initial parsing
const content = originalContent || yamlEditor.getValue();
const lines = content.split('\n');
const sections = originalSections.length > 0 ? originalSections : getCollapsibleSections();
const displayLines = [];
console.log('Current collapsed sections:', Array.from(collapsedSections));
console.log('Available sections:', sections.map(s => s.key));
console.log('Using original content:', !!originalContent);
for (let i = 0; i < lines.length; i++) {
const section = sections.find(s => s.headerLine === i);
if (section) {
// This is a collapsible section header
const isCollapsed = collapsedSections.has(section.key);
const arrow = isCollapsed ? ' ▶' : ' ▼';
console.log(`Section ${section.key} at line ${i}: isCollapsed=${isCollapsed}, childLines=${JSON.stringify(section.childLines)}`);
// Always show the header with appropriate arrow
const cleanLine = lines[i].replace(/ [▼▶]/g, '');
displayLines.push(cleanLine + arrow);
// Add child lines only if not collapsed
if (!isCollapsed) {
console.log(`Adding child lines for ${section.key}:`, section.childLines);
section.childLines.forEach(childLineIndex => {
if (lines[childLineIndex]) {
console.log(` Adding child line ${childLineIndex}: ${lines[childLineIndex]}`);
displayLines.push(lines[childLineIndex]);
}
});
} else {
console.log(`Skipping child lines for collapsed section ${section.key}`);
}
// Skip the child lines in the main loop
i = section.childLines.length > 0 ? Math.max(...section.childLines) : i;
} else {
// Regular line - check if it's a child of a collapsed section
const parentSection = sections.find(s => s.childLines.includes(i));
if (!parentSection || !collapsedSections.has(parentSection.key)) {
displayLines.push(lines[i]);
}
}
}
const newContent = displayLines.join('\n');
const cursor = yamlEditor.getCursor();
console.log('Final display lines:', displayLines);
console.log('New content length:', newContent.length);
// Prevent triggering change events during update
yamlEditor.off('change');
yamlEditor.setValue(newContent);
yamlEditor.setCursor(Math.min(cursor.line, displayLines.length - 1), cursor.ch);
// Re-enable change events
setTimeout(() => {
yamlEditor.on('change', () => {
if (!isUpdatingDisplay) {
updateFormFromYaml();
setTimeout(() => {
addArrowsToContent();
}, 200);
}
});
isUpdatingDisplay = false;
}, 50);
// Update line numbers
const lineCount = displayLines.length;
let lineNumbersHTML = '';
for (let i = 1; i <= lineCount; i++) {
lineNumbersHTML += `<div class="line-num" style="height: 24px;">${i}</div>`;
}
lineNumbersDiv.innerHTML = lineNumbersHTML;
};
// Use DOM-based click detection instead of CodeMirror coordinates
yamlEditorContainer.addEventListener('click', (event) => {
console.log('=== DOM CLICK DEBUG ===');
// Find which line element was clicked
const target = event.target;
const lineElement = target.closest('.CodeMirror-line');
if (lineElement) {
// Get the text content of the clicked line
const lineText = lineElement.textContent || lineElement.innerText;
console.log('Clicked line text:', JSON.stringify(lineText));
console.log('Has ▼?', lineText.includes('▼'));
console.log('Has ▶?', lineText.includes('▶'));
if (lineText.includes('▼') || lineText.includes('▶')) {
console.log('Arrow detected in clicked line!');
event.preventDefault();
event.stopPropagation();
// Extract section key from the line text
const cleanLine = lineText.replace(/[▼▶]/g, '').trim();
const sectionKey = cleanLine.replace(':', '');
console.log('Raw line text:', JSON.stringify(lineText));
console.log('Clean line:', JSON.stringify(cleanLine));
console.log('Section key:', JSON.stringify(sectionKey));
console.log('Current collapsed sections before toggle:', Array.from(collapsedSections));
toggleSection(sectionKey);
console.log('Current collapsed sections after toggle:', Array.from(collapsedSections));
return false;
} else {
console.log('No arrow in clicked line');
}
} else {
console.log('Could not find line element');
}
});
// Keep the CodeMirror handler as backup
yamlEditor.on('mousedown', (cm, event) => {
// This is now just for debugging
const pos = cm.coordsChar({left: event.clientX, top: event.clientY});
console.log('CodeMirror detected click on line:', pos.line, 'Content:', cm.getLine(pos.line));
});
// Handle cursor change on hover
yamlEditor.on('mousemove', (cm, event) => {
const pos = cm.coordsChar({left: event.clientX, top: event.clientY});
const line = cm.getLine(pos.line);
if (line && (line.includes(' ▼') || line.includes(' ▶'))) {
yamlEditorContainer.style.cursor = 'pointer';
} else {
yamlEditorContainer.style.cursor = 'text';
}
});
// Auto-collapse all sections by default
const autoCollapseAll = () => {
const sections = getCollapsibleSections();
sections.forEach(section => {
collapsedSections.add(section.key);
});
updateCollapsedDisplay();
};
// Sync scrolling between line numbers and editor
yamlEditor.on('scroll', () => {
const scrollInfo = yamlEditor.getScrollInfo();
lineNumbersDiv.scrollTop = scrollInfo.top;
});
// Update display when content changes
yamlEditor.on('change', () => {
if (!isUpdatingDisplay) {
updateFormFromYaml();
// Add arrows after changes (but not too frequently)
setTimeout(() => {
addArrowsToContent();
}, 200);
}
});
// Initial display - don't add arrows yet, wait for template to load
console.log('Waiting for template to load before adding arrows...');
// Also add a global function to test manually
window.testArrows = () => {
console.log('Manual arrow test...');
addArrowsToContent();
};
// Add a simple test function to directly toggle sections
window.testToggle = (sectionKey) => {
console.log('=== MANUAL TOGGLE TEST ===');
console.log('Toggling section:', sectionKey);
console.log('Before:', Array.from(collapsedSections));
toggleSection(sectionKey);
console.log('After:', Array.from(collapsedSections));
};
// --- dstack Templates ---
const dstackTemplates = {
task: `type: task
name: my-task
python: "3.11"
commands:
- pip install -r requirements.txt
- python train.py
resources:
gpu: 24GB
memory: 32GB
ports:
- 8080
env:
- MODEL_NAME=llama2
- BATCH_SIZE=32`,
service: `type: service
name: my-service
image: ghcr.io/huggingface/text-generation-inference:latest
env:
- MODEL_ID=microsoft/DialoGPT-medium
commands:
- text-generation-launcher --port 8000 --trust-remote-code
port: 8000
resources:
gpu: 80GB
memory: 64GB
model:
type: chat
name: microsoft/DialoGPT-medium
format: tgi
replicas: 1..4
rate_limits:
- prefix: /api/
rps: 10
burst: 20`,
'dev-environment': `type: dev-environment
name: my-dev-env
python: "3.11"
ide: vscode
commands:
- pip install -r requirements.txt
- pip install jupyter
resources:
gpu: 24GB
memory: 32GB
cpu: 8
disk: 100GB
ports:
- 8888
env:
- JUPYTER_TOKEN=my-secret-token`
};
// --- Preset Button Event Handlers ---
const loadPresetConfig = (presetType) => {
if (presetConfigs[presetType]) {
if (yamlEditor.getValue().trim() === '' || confirm('This will replace the current content. Are you sure?')) {
yamlEditor.setValue(presetConfigs[presetType]);
updateFormFromYaml();
// Auto-fold after preset load
setTimeout(autoFoldAll, 150);
}
}
};
// --- Mock Preset Configurations ---
const presetConfigs = {
'open-r1': `type: task
name: open-r1
python: "3.11"
env:
- HF_TOKEN
- HUGGINGFACE_TOKEN
- WANDB_API_KEY
- E2B_API_KEY
- TARGET_BASE_MODEL
- TARGET_YAML
- TARGET_REWARD
commands:
- apt-get update && apt-get install -y wget gnupg
- wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.1-1_all.deb
- dpkg -i cuda-keyring_1.1-1_all.deb
- rm -f /etc/apt/sources.list.d/cuda*.list
- wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-ubuntu2004.pin
- mv cuda-ubuntu2004.pin /etc/apt/preferences.d/cuda-repository-pin-600
- 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
- dpkg -i cuda-repo-ubuntu2004-12-4-local_12.4.0-550.54.14-1_amd64.deb
- cp /var/cuda-repo-ubuntu2004-12-4-local/cuda-*-keyring.gpg /usr/share/keyrings/
- apt-get update
- apt-get install -y cuda-toolkit-12-4
- echo 'export PATH=/usr/local/cuda-12.4/bin:$PATH' >> ~/.bashrc
- echo 'export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
- source ~/.bashrc
- nvcc --version
- curl -LsSf https://astral.sh/uv/install.sh | sh
- source ~/.bashrc
- uv venv openr1 --python 3.11 && source openr1/bin/activate && uv pip install --upgrade pip
- uv pip install vllm==0.8.4
- uv pip install setuptools && uv pip install flash-attn --no-build-isolation
- git clone https://github.com/deep-diver/open-r1.git
- cp $TARGET_YAML open-r1/recipes/custom.yaml
- cat open-r1/recipes/custom.yaml
- cp $TARGET_REWARD open-r1/src/open_r1/code_rewards.py
- cat open-r1/src/open_r1/code_rewards.py
- cd open-r1
- echo "E2B_API_KEY=$E2B_API_KEY" >> ".env"
- cat .env
- GIT_LFS_SKIP_SMUDGE=1 uv pip install -e ".[dev]"
- git clone https://github.com/deep-diver/trl.git
- cd trl
- uv pip install .
- nohup bash -c 'CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model "$TARGET_BASE_MODEL"' > vllm.log 2>&1 &
- sleep 420
- cd ..
- 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
- pkill -f 'trl vllm-serve'
resources:
gpu: 80GB:8
disk: 600GB
shm_size: 2GB`,
'llm-training': `type: task
name: llm-training
python: "3.11"
commands:
- pip install torch transformers datasets accelerate
- python -m torch.distributed.launch --nproc_per_node=4 train.py
- python evaluate.py --checkpoint ./best_model
resources:
gpu: 80GB:4
memory: 256GB
cpu: 32
disk: 1TB
env:
- CUDA_VISIBLE_DEVICES=0,1,2,3
- TRANSFORMERS_CACHE=/opt/cache
- WANDB_PROJECT=llm-training
- MODEL_NAME=meta-llama/Llama-2-70b-hf
- BATCH_SIZE=8
- LEARNING_RATE=1e-5`,
'model-serving': `type: service
name: llm-api-server
image: ghcr.io/huggingface/text-generation-inference:latest
env:
- MODEL_ID=microsoft/DialoGPT-large
- MAX_CONCURRENT_REQUESTS=128
- MAX_INPUT_LENGTH=1024
- MAX_TOTAL_TOKENS=2048
commands:
- text-generation-launcher --port 8000 --trust-remote-code --quantize bitsandbytes
port: 8000
resources:
gpu: 40GB
memory: 64GB
cpu: 16
model:
type: chat
name: microsoft/DialoGPT-large
format: tgi
replicas: 2..8
rate_limits:
- prefix: /api/v1/chat
rps: 50
burst: 100
- prefix: /health
rps: 200`,
'jupyter-dev': `type: dev-environment
name: ml-jupyter-workspace
python: "3.11"
ide: jupyter
commands:
- pip install jupyter jupyterlab pandas numpy matplotlib seaborn scikit-learn
- pip install torch torchvision transformers datasets
- jupyter lab --ip=0.0.0.0 --port=8888 --no-browser --allow-root
resources:
gpu: 24GB
memory: 64GB
cpu: 16
disk: 500GB
ports:
- 8888
- 6006
env:
- JUPYTER_TOKEN=secure-token-123
- NVIDIA_VISIBLE_DEVICES=all
- PYTHONPATH=/workspace`,
'data-processing': `type: task
name: data-pipeline
python: "3.11"
commands:
- pip install pandas polars dask distributed apache-beam
- python extract_data.py --source s3://my-bucket/raw/
- python transform_data.py --workers 16
- python load_data.py --target s3://my-bucket/processed/
resources:
memory: 128GB
cpu: 32
disk: 2TB
env:
- AWS_ACCESS_KEY_ID=your-access-key
- AWS_SECRET_ACCESS_KEY=your-secret-key
- DASK_WORKERS=16
- CHUNK_SIZE=10000`,
'web-app': `type: service
name: streamlit-demo
python: "3.11"
commands:
- pip install streamlit plotly pandas numpy
- streamlit run app.py --server.port 8501 --server.address 0.0.0.0
port: 8501
resources:
memory: 8GB
cpu: 4
replicas: 1..3
rate_limits:
- rps: 100
burst: 200
env:
- STREAMLIT_THEME_BASE=dark
- STREAMLIT_SERVER_ENABLE_CORS=true`,
'gpu-cluster': `type: task
name: distributed-training
python: "3.11"
commands:
- pip install torch torchvision transformers accelerate deepspeed
- accelerate launch --multi_gpu --num_processes 8 train_distributed.py
- python consolidate_checkpoints.py
resources:
gpu: 40GB:8
memory: 512GB
cpu: 64
disk: 5TB
env:
- MASTER_ADDR=localhost
- MASTER_PORT=29500
- WORLD_SIZE=8
- NCCL_DEBUG=INFO
- CUDA_LAUNCH_BLOCKING=1
- MODEL_PARALLEL_SIZE=4
- DATA_PARALLEL_SIZE=2`
};
// --- Initial State & Data ---
let currentData = {}; // Holds the parsed YAML as a JS object
let isUpdating = false; // Prevents infinite update loops
// --- Utility Functions ---
/**
* Sets a value in a nested object based on a path string.
*/
const setNestedValue = (obj, path, value) => {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const nextKey = keys[i + 1];
if (!isNaN(parseInt(nextKey, 10)) && !Array.isArray(current[key])) {
current[key] = [];
} else if (isNaN(parseInt(nextKey, 10)) && typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
};
/**
* Deletes a key or an array element from a nested object.
*/
const deleteNestedValue = (obj, path) => {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
current = current[keys[i]];
}
const finalKey = keys[keys.length - 1];
if (Array.isArray(current)) {
current.splice(parseInt(finalKey, 10), 1);
} else {
delete current[finalKey];
}
};
// --- Core Rendering Functions ---
/**
* Generates an input field based on the value type and dstack context.
*/
const createInputField = (key, value, path) => {
const type = typeof value;
const id = `field-${path}`;
let inputHtml = '';
// Common classes for text-based inputs with compact styling
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";
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";
// Get field icon SVG based on key
const getFieldIcon = (key) => {
const iconMap = {
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>',
'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>'
};
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>';
};
const fieldIcon = getFieldIcon(key);
const fieldLabel = String(key).replace(/_/g, ' ');
// dstack-specific field handling
if (key === 'type' && typeof value === 'string') {
inputHtml = `
<div class="relative">
<select data-path="${path}" id="${id}" class="${selectClasses} pl-12 appearance-none cursor-pointer">
<option value="task" ${value === 'task' ? 'selected' : ''}>Task</option>
<option value="service" ${value === 'service' ? 'selected' : ''}>Service</option>
<option value="dev-environment" ${value === 'dev-environment' ? 'selected' : ''}>Dev Environment</option>
</select>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/60">
${fieldIcon}
</div>
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>`;
} else if (key === 'python' && typeof value === 'string') {
inputHtml = `
<div class="relative">
<select data-path="${path}" id="${id}" class="${selectClasses} pl-12 appearance-none cursor-pointer">
<option value="3.8" ${value === '3.8' ? 'selected' : ''}>Python 3.8</option>
<option value="3.9" ${value === '3.9' ? 'selected' : ''}>Python 3.9</option>
<option value="3.10" ${value === '3.10' ? 'selected' : ''}>Python 3.10</option>
<option value="3.11" ${value === '3.11' ? 'selected' : ''}>Python 3.11</option>
<option value="3.12" ${value === '3.12' ? 'selected' : ''}>Python 3.12</option>
</select>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/60">
${fieldIcon}
</div>
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>`;
} else if (key === 'ide' && typeof value === 'string') {
inputHtml = `
<div class="relative">
<select data-path="${path}" id="${id}" class="${selectClasses} pl-12 appearance-none cursor-pointer">
<option value="vscode" ${value === 'vscode' ? 'selected' : ''}>VS Code</option>
<option value="jupyter" ${value === 'jupyter' ? 'selected' : ''}>Jupyter</option>
<option value="ssh" ${value === 'ssh' ? 'selected' : ''}>SSH</option>
</select>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/60">
${fieldIcon}
</div>
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>`;
} else if (type === 'boolean') {
inputHtml = `
<div class="relative">
<div class="flex items-center mt-3 p-4 glass rounded-xl border border-white/10 hover:border-white/20 transition-all group">
<div class="flex items-center">
<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">
<div class="ml-4 flex items-center space-x-3">
<div class="text-white/60">${fieldIcon}</div>
<label for="${id}" class="text-sm font-semibold transition-colors ${value ? 'text-green-400' : 'text-white/70'}">${value ? 'Enabled' : 'Disabled'}</label>
</div>
</div>
<div class="ml-auto">
<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'}">
<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="${value ? 'M5 13l4 4L19 7' : 'M6 18L18 6M6 6l12 12'}"></path>
</svg>
</div>
</div>
</div>
</div>`;
} else if (type === 'number') {
inputHtml = `
<div class="relative">
<input type="number" data-path="${path}" id="${id}" value="${value}" class="${inputClasses} pl-12" placeholder="Enter number...">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/60">
${fieldIcon}
</div>
</div>`;
} else { // string or other
let placeholder = `Enter ${fieldLabel.toLowerCase()}...`;
if (key === 'name') placeholder = 'my-awesome-project';
else if (key === 'image') placeholder = 'ghcr.io/huggingface/transformers';
else if (key === 'value') placeholder = 'Enter value...';
// Calculate rows based on content
const lines = value.toString().split('\n').length;
const rows = Math.max(1, lines);
inputHtml = `
<div class="relative">
<textarea data-path="${path}" id="${id}" class="${inputClasses} pl-12 min-h-[2.5rem] resize-y auto-resize" placeholder="${placeholder}" rows="${rows}">${value}</textarea>
<div class="absolute top-3 left-0 pl-3 flex items-center pointer-events-none text-white/60">
${fieldIcon}
</div>
</div>`;
}
// Add compact help text for key fields only
let helpText = '';
if (key === 'gpu' || key === 'memory' || key === 'cpu') {
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>';
} else if (key === 'replicas') {
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>';
}
return `
<div class="mb-4 group">
<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">
<div class="text-white/80" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">${fieldIcon}</div>
<span style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7); font-weight: 600;">${fieldLabel}</span>
</label>
${inputHtml}
${helpText}
</div>`;
};
/**
* Recursively builds the form UI from a JavaScript object.
*/
const renderForm = (data, parentElement, parentPath = '') => {
if (Array.isArray(data)) {
// --- Render Array ---
const arrayContainer = document.createElement('div');
data.forEach((item, index) => {
const itemPath = parentPath ? `${parentPath}.${index}` : String(index);
const fieldset = document.createElement('fieldset');
fieldset.className = "glass border border-white/30 rounded-xl p-4 mb-4 relative group hover:glow transition-all duration-500 fieldset-enter";
const summaryText = generateSummary(item, itemPath);
fieldset.innerHTML = `
<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)">
<div class="flex items-center justify-between w-full">
<div class="flex items-center space-x-2">
<span>Item ${index + 1}</span>
<svg class="w-4 h-4 collapse-arrow expanded" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
<div class="summary-preview" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">${summaryText}</div>
</legend>
<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">
<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>
</button>
<div class="fieldset-content expanded mt-3">
</div>
`;
// Handle primitive values in arrays (like strings in commands array)
const contentDiv = fieldset.querySelector('.fieldset-content');
if (typeof item !== 'object' || item === null) {
contentDiv.innerHTML = createInputField('value', item, itemPath);
} else {
renderForm(item, contentDiv, itemPath);
}
arrayContainer.appendChild(fieldset);
});
// Smart default for array items based on parent context
let addButtonText = 'Add Item';
let newItemTemplate = "new_item";
if (parentPath === 'commands') {
addButtonText = 'Add Command';
newItemTemplate = 'python script.py';
} else if (parentPath === 'env') {
addButtonText = 'Add Environment Variable';
newItemTemplate = 'KEY=value';
} else if (parentPath === 'ports') {
addButtonText = 'Add Port';
newItemTemplate = 8080;
} else if (parentPath === 'rate_limits') {
addButtonText = 'Add Rate Limit';
newItemTemplate = { prefix: "/api/", rps: 10 };
}
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">
<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>`;
parentElement.appendChild(arrayContainer);
} else if (typeof data === 'object' && data !== null) {
// --- Render Object ---
Object.entries(data).forEach(([key, value]) => {
const currentPath = parentPath ? `${parentPath}.${key}` : key;
if (typeof value === 'object' && value !== null) {
// Check if this is a complex section that should use drawer editing
const complexSections = ['commands', 'env', 'resources', 'model', 'rate_limits', 'ports'];
const isComplexSection = complexSections.includes(key.toLowerCase());
if (isComplexSection) {
// Create simple summary with edit button
const summaryContainer = document.createElement('div');
summaryContainer.className = "mb-4 group";
const summaryText = generateSummary(value, currentPath);
summaryContainer.innerHTML = `
<div class="flex items-center justify-between mb-2">
<label class="flex items-center space-x-2 text-sm font-bold text-white capitalize">
<div class="text-white/80" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">
<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>
</div>
<span style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7); font-weight: 600;">${key.replace(/_/g, ' ')}</span>
</label>
<button class="edit-button" onclick="openDrawer('${currentPath}', '${key}')">
<svg class="w-3 h-3" 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.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>
</svg>
Edit
</button>
</div>
<div class="glass border border-white/30 rounded-xl p-4">
<div class="text-sm text-white/70" style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);">
${summaryText || 'Click Edit to configure'}
</div>
</div>
`;
parentElement.appendChild(summaryContainer);
} else {
// Regular collapsible section for simple objects
const groupContainer = document.createElement('div');
groupContainer.className = "mb-4 group";
const summaryText = generateSummary(value, currentPath);
groupContainer.innerHTML = `
<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;">
<svg class="w-4 h-4 collapse-arrow expanded" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 9l-7 7-7-7"></path>
</svg>
<span>${key.replace(/_/g, ' ')}</span>
</div>
<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>
<div class="fieldset-content expanded glass border border-white/30 rounded-xl p-4">
</div>
`;
const contentDiv = groupContainer.querySelector('.fieldset-content');
renderForm(value, contentDiv, currentPath);
parentElement.appendChild(groupContainer);
}
} else {
parentElement.innerHTML += createInputField(key, value, currentPath);
}
});
}
};
// --- Update & Sync Functions ---
const updateYamlEditor = () => {
if (isUpdating) return;
isUpdating = true;
try {
const yamlString = jsyaml.dump(currentData, { indent: 2 });
yamlEditor.setValue(yamlString);
// Auto-collapse sections after setting content
setTimeout(() => {
autoCollapseAll();
}, 50);
statusIndicator.textContent = 'Synced';
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';
yamlEditorContainer.classList.remove('border-red-500');
} catch (e) {
console.error("Error dumping YAML:", e);
}
setTimeout(() => { isUpdating = false; }, 50);
};
const updateFormFromYaml = () => {
if (isUpdating) return;
isUpdating = true;
try {
const newData = jsyaml.load(yamlEditor.getValue());
if (JSON.stringify(newData) !== JSON.stringify(currentData)) {
currentData = newData || {};
formContainer.innerHTML = ''; // Clear previous form
// Hide/show empty state based on content
const emptyState = document.getElementById('empty-state');
if (Object.keys(currentData).length === 0 || yamlEditor.getValue().trim() === '') {
emptyState.style.display = 'flex';
formContainer.style.display = 'none';
} else {
emptyState.style.display = 'none';
formContainer.style.display = 'block';
renderForm(currentData, formContainer);
}
}
statusIndicator.textContent = 'Valid';
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';
yamlEditorContainer.classList.remove('border-red-500');
} catch (e) {
statusIndicator.textContent = 'Error';
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';
yamlEditorContainer.classList.add('border-red-500');
console.warn("Invalid YAML:", e.message);
}
setTimeout(() => { isUpdating = false; }, 50);
};
const updateDataFromForm = (path, value) => {
setNestedValue(currentData, path, value);
updateYamlEditor();
};
// --- Event Listeners ---
// Change event is now handled in the CodeMirror initialization above
formContainer.addEventListener('input', (e) => {
if (e.target.dataset.path) {
const path = e.target.dataset.path;
let value = e.target.value;
if (e.target.type === 'checkbox') {
value = e.target.checked;
// Update label for checkbox
const label = e.target.nextElementSibling;
if (label) {
label.textContent = value ? 'Enabled' : 'Disabled';
}
} else if (e.target.type === 'number') {
value = parseFloat(value) || 0;
}
updateDataFromForm(path, value);
}
});
formContainer.addEventListener('click', (e) => {
const button = e.target.closest('button');
if (!button) return;
const path = button.dataset.path;
if (button.classList.contains('remove-btn')) {
if (confirm('Are you sure you want to remove this item?')) {
deleteNestedValue(currentData, path);
formContainer.innerHTML = '';
renderForm(currentData, formContainer);
updateYamlEditor();
}
} else if (button.classList.contains('add-btn')) {
let newItem;
try {
newItem = JSON.parse(button.dataset.template);
} catch (e) {
newItem = button.dataset.template;
}
const currentArray = path ? currentData[path] : currentData;
const newIndex = Array.isArray(currentArray) ? currentArray.length : 0;
setNestedValue(currentData, `${path ? path + '.' : ''}${newIndex}`, newItem);
formContainer.innerHTML = '';
renderForm(currentData, formContainer);
updateYamlEditor();
}
});
const generateSummary = (data, path) => {
if (Array.isArray(data)) {
if (data.length === 0) return "Empty";
return `${data.length} item${data.length > 1 ? 's' : ''}`;
} else if (typeof data === 'object' && data !== null) {
const keys = Object.keys(data);
if (keys.length === 0) return "Empty";
return `${keys.length} field${keys.length > 1 ? 's' : ''}: ${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}`;
}
return '';
};
// --- Fast Collapsible Section Function ---
window.toggleFieldset = function(headerElement) {
const groupContainer = headerElement.closest('.group');
const content = groupContainer.querySelector('.fieldset-content');
const arrow = headerElement.querySelector('.collapse-arrow');
const summary = groupContainer.querySelector('.summary-preview');
const isExpanded = content.classList.contains('expanded');
if (isExpanded) {
// Collapse - show summary, hide container
content.classList.remove('expanded');
content.classList.add('collapsed');
arrow.classList.remove('expanded');
arrow.classList.add('collapsed');
if (summary) summary.style.display = 'block';
} else {
// Expand - hide summary, show container
content.classList.remove('collapsed');
content.classList.add('expanded');
arrow.classList.remove('collapsed');
arrow.classList.add('expanded');
if (summary) summary.style.display = 'none';
}
};
// --- Preset Button Event Listeners ---
document.getElementById('preset-open-r1').addEventListener('click', () => loadPresetConfig('open-r1'));
document.getElementById('preset-model-serving').addEventListener('click', () => loadPresetConfig('model-serving'));
document.getElementById('preset-jupyter-dev').addEventListener('click', () => loadPresetConfig('jupyter-dev'));
document.getElementById('preset-data-processing').addEventListener('click', () => loadPresetConfig('data-processing'));
document.getElementById('preset-web-app').addEventListener('click', () => loadPresetConfig('web-app'));
document.getElementById('preset-gpu-cluster').addEventListener('click', () => loadPresetConfig('gpu-cluster'));
// --- Annotation System ---
let annotations = {}; // Store annotations by path
window.toggleAnnotation = function(btn, event) {
event.preventDefault();
event.stopPropagation();
const path = btn.dataset.path;
const popup = document.getElementById(`annotation-popup-${path}`);
const isVisible = popup.classList.contains('show');
// Close all other popups
document.querySelectorAll('.annotation-popup.show').forEach(p => {
p.classList.remove('show');
});
if (!isVisible) {
popup.classList.add('show');
const textarea = popup.querySelector('.annotation-input');
textarea.value = annotations[path] || '';
textarea.focus();
}
};
window.saveAnnotation = function(path) {
const popup = document.getElementById(`annotation-popup-${path}`);
const textarea = popup.querySelector('.annotation-input');
const value = textarea.value.trim();
const btn = document.querySelector(`[data-path="${path}"].annotation-btn`);
const display = document.getElementById(`annotation-display-${path}`);
if (value) {
annotations[path] = value;
btn.classList.add('has-annotation');
display.textContent = value;
display.style.display = 'block';
} else {
delete annotations[path];
btn.classList.remove('has-annotation');
display.style.display = 'none';
}
popup.classList.remove('show');
};
window.cancelAnnotation = function(path) {
const popup = document.getElementById(`annotation-popup-${path}`);
popup.classList.remove('show');
};
// Close annotation popups when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.annotation-container')) {
document.querySelectorAll('.annotation-popup.show').forEach(p => {
p.classList.remove('show');
});
}
});
// --- Drawer Management ---
let currentDrawerPath = null;
let currentDrawerKey = null;
let drawerData = null;
window.openDrawer = function(path, key) {
currentDrawerPath = path;
currentDrawerKey = key;
// Get the data for this section
const keys = path.split('.');
let data = currentData;
for (const k of keys) {
if (data && typeof data === 'object') {
data = data[k];
}
}
drawerData = JSON.parse(JSON.stringify(data || {})); // Deep clone
// Update drawer content
const drawerTitle = document.getElementById('drawer-title');
const drawerContent = document.getElementById('drawer-content');
drawerTitle.textContent = `Edit ${key.replace(/_/g, ' ')}`;
// Simple drawer interface - just render the form
drawerContent.innerHTML = '';
renderForm(drawerData, drawerContent, '');
// Auto-resize all textareas after rendering
setTimeout(() => {
drawerContent.querySelectorAll('textarea.auto-resize').forEach(textarea => {
autoResizeTextarea(textarea);
});
}, 50);
// Show drawer
const overlay = document.getElementById('drawer-overlay');
const drawer = document.getElementById('drawer');
overlay.classList.add('open');
drawer.classList.add('open');
};
window.closeDrawer = function() {
const overlay = document.getElementById('drawer-overlay');
const drawer = document.getElementById('drawer');
overlay.classList.remove('open');
drawer.classList.remove('open');
currentDrawerPath = null;
currentDrawerKey = null;
drawerData = null;
};
window.saveDrawer = function() {
if (currentDrawerPath && drawerData) {
// Update the main data
setNestedValue(currentData, currentDrawerPath, drawerData);
// Update YAML and form
updateYamlEditor();
formContainer.innerHTML = '';
renderForm(currentData, formContainer);
closeDrawer();
}
};
// Auto-resize textarea function
const autoResizeTextarea = (textarea) => {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
};
// Handle drawer form changes
document.addEventListener('input', (e) => {
if (e.target.closest('#drawer-content') && e.target.dataset.path) {
const path = e.target.dataset.path;
let value = e.target.value;
if (e.target.type === 'checkbox') {
value = e.target.checked;
} else if (e.target.type === 'number') {
value = parseFloat(value) || 0;
}
setNestedValue(drawerData, path, value);
// Auto-resize textareas
if (e.target.tagName === 'TEXTAREA' && e.target.classList.contains('auto-resize')) {
autoResizeTextarea(e.target);
}
}
});
// Handle drawer form clicks (add/remove buttons)
document.addEventListener('click', (e) => {
const button = e.target.closest('button');
if (!button || !button.closest('#drawer-content')) return;
const path = button.dataset.path;
if (button.classList.contains('remove-btn')) {
if (confirm('Are you sure you want to remove this item?')) {
deleteNestedValue(drawerData, path);
const drawerContent = document.getElementById('drawer-content');
drawerContent.innerHTML = '';
renderForm(drawerData, drawerContent, '');
}
} else if (button.classList.contains('add-btn')) {
let newItem;
try {
newItem = JSON.parse(button.dataset.template);
} catch (e) {
newItem = button.dataset.template;
}
const currentArray = path ? drawerData[path] : drawerData;
const newIndex = Array.isArray(currentArray) ? currentArray.length : 0;
setNestedValue(drawerData, `${path ? path + '.' : ''}${newIndex}`, newItem);
const drawerContent = document.getElementById('drawer-content');
drawerContent.innerHTML = '';
renderForm(drawerData, drawerContent, '');
}
});
// --- Initial Load ---
yamlEditor.setValue(dstackTemplates.task);
updateFormFromYaml();
// Add arrows and auto-collapse after template is loaded
setTimeout(() => {
console.log('Template loaded, now adding arrows...');
addArrowsToContent();
setTimeout(() => {
console.log('Auto-collapsing all sections...');
autoCollapseAll();
}, 100);
}, 100);
});
</script>
</body>
</html>