Spaces:
Running on T4
Running on T4
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>VIDRAFT PROMETHEUS</title> | |
| <!-- style.css removed - all CSS is inline --> | |
| <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| :root { | |
| --bg: #FAFAFA; | |
| --surface: #FFFFFF; | |
| --border: #E8E8E8; | |
| --border-hover: #D0D0D0; | |
| --text-primary: #1A1A1A; | |
| --text-secondary: #6B6B6B; | |
| --text-tertiary: #A0A0A0; | |
| --accent: #1A1A1A; | |
| --accent-hover: #333333; | |
| --red: #E53935; | |
| --red-dark: #C62828; | |
| --red-glow: rgba(229, 57, 53, 0.15); | |
| --green: #2E7D32; | |
| --amber: #F57F17; | |
| --radius: 12px; | |
| --radius-sm: 8px; | |
| --shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06); | |
| --shadow-md: 0 4px 16px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.04); | |
| --shadow-lg: 0 8px 32px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04); | |
| --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| body { | |
| font-family: 'DM Sans', -apple-system, sans-serif; | |
| background: var(--bg); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| overflow-x: hidden; padding-right: 314px; /* right panel = 310px + 4px scrollbar gap */ | |
| } | |
| .container { | |
| max-width: 100%; | |
| margin: 0; | |
| padding: 10px 16px 8px; | |
| } | |
| /* ── Header ── */ | |
| .header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 6px; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo-mark { | |
| width: 28px; height: 28px; | |
| background: linear-gradient(135deg, #E8593C, #D4A044); | |
| border-radius: 7px; | |
| display: flex; align-items: center; justify-content: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .logo-mark::before { | |
| content: ''; | |
| width: 14px; height: 14px; | |
| border-radius: 50%; | |
| background: var(--bg); | |
| position: absolute; | |
| } | |
| .logo-mark::after { | |
| content: ''; | |
| width: 6px; height: 6px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #E8593C, #D4A044); | |
| position: absolute; | |
| } | |
| .brand-name { | |
| font-size: 1.05em; | |
| font-weight: 700; | |
| letter-spacing: -0.03em; | |
| color: var(--text-primary); | |
| } | |
| .header-status { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| /* ── Status Pills ── */ | |
| .status-pills { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| margin-bottom: 6px; | |
| } | |
| .pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 3px 8px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 100px; | |
| font-size: 0.7em; | |
| font-family: 'JetBrains Mono', monospace; | |
| color: var(--text-secondary); | |
| transition: border-color var(--transition); | |
| } | |
| .pill:hover { border-color: var(--border-hover); } | |
| .pill-label { | |
| font-family: 'DM Sans', sans-serif; | |
| font-weight: 500; | |
| color: var(--text-tertiary); | |
| text-transform: uppercase; | |
| font-size: 0.9em; | |
| letter-spacing: 0.04em; | |
| } | |
| .pill-value { color: var(--text-primary); font-weight: 500; } | |
| .pill-dot { | |
| width: 6px; height: 6px; | |
| border-radius: 50%; | |
| background: var(--text-tertiary); | |
| flex-shrink: 0; | |
| } | |
| .pill-dot.active { background: var(--green); animation: dot-pulse 1.5s ease infinite; } | |
| .pill-dot.generating { background: var(--amber); animation: dot-pulse 0.8s ease infinite; } | |
| @keyframes dot-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } } | |
| /* ── Prompt Bar ── */ | |
| .prompt-bar { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 6px; | |
| align-items: stretch; | |
| } | |
| .prompt-input-wrap { | |
| flex: 1; | |
| position: relative; | |
| } | |
| .prompt-input { | |
| width: 100%; | |
| padding: 8px 12px; | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius); | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 0.85em; | |
| color: var(--text-primary); | |
| background: var(--surface); | |
| transition: all var(--transition); | |
| outline: none; | |
| } | |
| .prompt-input:focus { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 3px rgba(26,26,26,0.06); | |
| } | |
| .prompt-input::placeholder { color: var(--text-tertiary); } | |
| .btn-update { | |
| padding: 0 20px; | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius); | |
| background: var(--surface); | |
| color: var(--text-secondary); | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 0.85em; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| white-space: nowrap; | |
| } | |
| .btn-update:hover:not(:disabled) { | |
| border-color: var(--accent); | |
| color: var(--text-primary); | |
| background: var(--bg); | |
| } | |
| .btn-update:disabled { opacity: 0.4; cursor: not-allowed; } | |
| /* ── Canvas ── */ | |
| #canvas-container { | |
| position: relative; | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| box-shadow: var(--shadow-lg); | |
| margin-bottom: 0; | |
| } | |
| #renderCanvas { | |
| display: block; | |
| width: 100%; | |
| height: calc(100vh - 230px); min-height: 300px; | |
| } | |
| /* ── Player Bar ── */ | |
| .player-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 6px 12px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-top: none; | |
| border-radius: 0 0 var(--radius) var(--radius); | |
| margin-bottom: 6px; | |
| } | |
| .player-btn { | |
| width: 34px; height: 34px; | |
| border-radius: var(--radius-sm); | |
| border: 1.5px solid var(--border); | |
| background: var(--surface); | |
| color: var(--text-primary); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| flex-shrink: 0; | |
| position: relative; | |
| } | |
| .player-btn:hover:not(:disabled) { | |
| border-color: var(--accent); | |
| background: var(--bg); | |
| transform: translateY(-1px); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .player-btn:active:not(:disabled) { | |
| transform: translateY(0); | |
| } | |
| .player-btn:disabled { | |
| opacity: 0.3; | |
| cursor: not-allowed; | |
| } | |
| .player-btn svg { | |
| width: 18px; height: 18px; | |
| fill: currentColor; | |
| } | |
| /* Primary play/start button */ | |
| .player-btn.primary { | |
| width: 38px; height: 38px; | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: white; | |
| border-radius: 50%; | |
| } | |
| .player-btn.primary:hover:not(:disabled) { | |
| background: var(--accent-hover); | |
| border-color: var(--accent-hover); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| } | |
| /* Record button */ | |
| .player-btn.record-btn { | |
| border-color: var(--border); | |
| } | |
| .player-btn.record-btn .rec-circle { | |
| width: 14px; height: 14px; | |
| border-radius: 50%; | |
| background: var(--red); | |
| transition: all var(--transition); | |
| } | |
| .player-btn.record-btn.recording { | |
| border-color: var(--red); | |
| background: var(--red-glow); | |
| animation: rec-border-pulse 1.2s ease infinite; | |
| } | |
| .player-btn.record-btn.recording .rec-circle { | |
| border-radius: 3px; | |
| width: 12px; height: 12px; | |
| } | |
| @keyframes rec-border-pulse { | |
| 0%,100% { box-shadow: 0 0 0 0 var(--red-glow); } | |
| 50% { box-shadow: 0 0 0 6px transparent; } | |
| } | |
| .rec-timer { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.8em; | |
| color: var(--red); | |
| font-weight: 500; | |
| display: none; | |
| min-width: 42px; | |
| } | |
| .rec-timer.active { display: inline; } | |
| .player-divider { | |
| width: 1px; | |
| height: 24px; | |
| background: var(--border); | |
| flex-shrink: 0; | |
| } | |
| .player-spacer { flex: 1; } | |
| .player-btn.config-btn { | |
| border: none; | |
| background: transparent; | |
| color: var(--text-tertiary); | |
| width: 28px; height: 28px; | |
| } | |
| .player-btn.config-btn:hover { | |
| color: var(--text-primary); | |
| background: var(--bg); | |
| border: none; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .player-btn.config-btn svg { | |
| width: 20px; height: 20px; | |
| fill: none; | |
| stroke: currentColor; | |
| stroke-width: 1.5; | |
| stroke-linecap: round; | |
| stroke-linejoin: round; | |
| } | |
| /* Auto-set robot style when skeleton is ready */ | |
| var _checkRobot = setInterval(function() { | |
| if (window.app && window.app.skeleton) { | |
| window.app.skeleton.setStyle('robot'); | |
| clearInterval(_checkRobot); | |
| } | |
| }, 500); | |
| /* ── Preset Emoji Buttons ── */ | |
| .preset-section { | |
| margin-bottom: 6px; | |
| } | |
| .preset-label { | |
| font-size: 0.65em; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| color: var(--text-tertiary); | |
| margin-bottom: 4px; | |
| } | |
| .preset-grid { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 4px; | |
| } | |
| .preset-chip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 4px 9px 4px 6px; | |
| background: var(--surface); | |
| border: 1.5px solid var(--border); | |
| border-radius: 100px; | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 0.75em; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| user-select: none; | |
| white-space: nowrap; | |
| } | |
| .preset-chip:hover { | |
| border-color: var(--accent); | |
| color: var(--text-primary); | |
| background: var(--bg); | |
| transform: translateY(-1px); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .preset-chip:active { | |
| transform: translateY(0); | |
| } | |
| .preset-chip.active { | |
| border-color: var(--accent); | |
| background: var(--accent); | |
| color: #fff; | |
| } | |
| .preset-chip .emoji { | |
| font-size: 1.15em; | |
| line-height: 1; | |
| } | |
| .world-chip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 5px 10px 5px 7px; | |
| background: var(--surface); | |
| border: 1.5px solid var(--border); | |
| border-radius: 100px; | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 0.75em; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| user-select: none; | |
| white-space: nowrap; | |
| } | |
| .world-chip:hover { | |
| border-color: #E65100; | |
| color: var(--text-primary); | |
| transform: translateY(-1px); | |
| } | |
| .world-chip.active { | |
| border-color: #E65100; | |
| background: #FFF3E0; | |
| color: #E65100; | |
| font-weight: 600; | |
| } | |
| .world-chip .emoji { font-size: 1.1em; line-height: 1; } | |
| @media (max-width: 1100px) { body { padding-right: 0 !important; } } | |
| @media (max-width: 640px) { | |
| .preset-chip { padding: 5px 10px 5px 7px; font-size: 0.76em; } | |
| .preset-grid { gap: 4px; } | |
| } | |
| /* ── Conflict Warning ── */ | |
| .conflict-warning { | |
| padding: 14px 18px; | |
| background: #FFF8E1; | |
| border: 1px solid #FFE082; | |
| border-radius: var(--radius); | |
| margin-bottom: 6px; | |
| font-size: 0.82em; | |
| } | |
| .conflict-warning p { margin-bottom: 8px; } | |
| .conflict-warning .btn-row { display: flex; gap: 8px; } | |
| .btn-danger { | |
| padding: 8px 16px; | |
| background: var(--red); | |
| color: #fff; | |
| border: none; | |
| border-radius: var(--radius-sm); | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 0.85em; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: background var(--transition); | |
| } | |
| .btn-danger:hover { background: var(--red-dark); } | |
| .btn-ghost { | |
| padding: 8px 16px; | |
| background: transparent; | |
| color: var(--text-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 0.85em; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| } | |
| .btn-ghost:hover { border-color: var(--border-hover); } | |
| /* ── Footer Credit ── */ | |
| .footer-credit { | |
| text-align: center; | |
| font-size: 0.78em; | |
| color: var(--text-tertiary); | |
| padding: 2px 0 0; | |
| } | |
| /* ── Config Modal ── */ | |
| .modal-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0,0,0,0.25); | |
| backdrop-filter: blur(4px); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| } | |
| .modal-content { | |
| background: var(--surface); | |
| border-radius: 16px; | |
| width: 90%; | |
| max-width: 520px; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| box-shadow: var(--shadow-lg); | |
| border: 1px solid var(--border); | |
| } | |
| .modal-header { | |
| padding: 20px 24px 0; | |
| } | |
| .modal-header h2 { | |
| font-size: 1.1em; | |
| font-weight: 600; | |
| letter-spacing: -0.02em; | |
| } | |
| .modal-body { | |
| padding: 20px 24px; | |
| } | |
| .config-section { | |
| margin-bottom: 6px; | |
| } | |
| .config-section h3 { | |
| font-size: 0.8em; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| color: var(--text-tertiary); | |
| margin-bottom: 12px; | |
| } | |
| .config-field { | |
| margin-bottom: 14px; | |
| } | |
| .config-field label { | |
| display: block; | |
| font-size: 0.85em; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| margin-bottom: 6px; | |
| } | |
| .config-field input[type="number"], | |
| .config-field input[type="text"] { | |
| width: 100%; | |
| padding: 9px 12px; | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.85em; | |
| color: var(--text-primary); | |
| background: var(--bg); | |
| outline: none; | |
| transition: border-color var(--transition); | |
| } | |
| .config-field input:focus { | |
| border-color: var(--accent); | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .slider-container input[type="range"] { | |
| flex: 1; | |
| -webkit-appearance: none; | |
| height: 4px; | |
| border-radius: 2px; | |
| background: var(--border); | |
| outline: none; | |
| } | |
| .slider-container input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; height: 16px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| cursor: pointer; | |
| box-shadow: 0 1px 4px rgba(0,0,0,0.15); | |
| } | |
| .slider-value { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.8em; | |
| color: var(--text-secondary); | |
| min-width: 32px; | |
| text-align: right; | |
| } | |
| .param-hint { | |
| font-size: 0.75em; | |
| color: var(--text-tertiary); | |
| margin-top: 4px; | |
| display: block; | |
| } | |
| .modal-footer { | |
| padding: 0 24px 20px; | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 8px; | |
| } | |
| .modal-footer .btn { | |
| padding: 9px 20px; | |
| border-radius: var(--radius-sm); | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 0.85em; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| border: 1.5px solid var(--border); | |
| background: var(--surface); | |
| color: var(--text-secondary); | |
| } | |
| .modal-footer .btn:hover { | |
| border-color: var(--border-hover); | |
| } | |
| .modal-footer .btn-primary { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: #fff; | |
| } | |
| .modal-footer .btn-primary:hover { | |
| background: var(--accent-hover); | |
| } | |
| /* ── Tooltip for player buttons ── */ | |
| .player-btn[data-tip]::after { | |
| content: attr(data-tip); | |
| position: absolute; | |
| bottom: calc(100% + 6px); | |
| left: 50%; | |
| transform: translateX(-50%) scale(0.95); | |
| padding: 4px 10px; | |
| background: linear-gradient(135deg, #E8593C, #D4A044); | |
| color: #fff; | |
| font-size: 0.72em; | |
| font-weight: 500; | |
| border-radius: 6px; | |
| white-space: nowrap; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: all 0.15s ease; | |
| } | |
| .player-btn[data-tip]:hover::after { | |
| opacity: 1; | |
| transform: translateX(-50%) scale(1); | |
| } | |
| /* ── Style Selector ── */ | |
| .style-section { | |
| margin-bottom: 6px; | |
| } | |
| .style-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .style-row .preset-label { | |
| margin-bottom: 0; | |
| flex-shrink: 0; | |
| } | |
| .style-chip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 7px 14px 7px 10px; | |
| background: var(--surface); | |
| border: 1.5px solid var(--border); | |
| border-radius: 100px; | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 0.75em; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| user-select: none; | |
| white-space: nowrap; | |
| } | |
| .style-chip:hover { | |
| border-color: var(--accent); | |
| color: var(--text-primary); | |
| transform: translateY(-1px); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .style-chip:active { transform: translateY(0); } | |
| .style-chip.active { | |
| border-color: var(--accent); | |
| background: var(--accent); | |
| color: #fff; | |
| } | |
| .style-chip .emoji { font-size: 1.15em; line-height: 1; } | |
| .model-status { | |
| font-size: 0.75em; | |
| font-family: 'JetBrains Mono', monospace; | |
| color: var(--text-tertiary); | |
| white-space: nowrap; | |
| transition: color var(--transition); | |
| } | |
| .model-status.success { color: var(--green); } | |
| .model-status.error { color: var(--red); } | |
| .model-status.loading { color: var(--amber); } | |
| .style-chip.loading { | |
| pointer-events: none; | |
| opacity: 0.6; | |
| animation: chip-pulse 1s ease infinite; | |
| } | |
| @keyframes chip-pulse { 0%,100% { opacity: 0.6; } 50% { opacity: 0.9; } } | |
| /* ── Responsive ── */ | |
| @media (max-width: 1100px) { body { padding-right: 0 !important; } } | |
| @media (max-width: 640px) { | |
| .container { padding: 16px 12px; } | |
| .status-pills { gap: 5px; } | |
| .pill { padding: 4px 8px; font-size: 0.72em; } | |
| .player-bar { padding: 10px 12px; gap: 6px; } | |
| .player-btn { width: 28px; height: 28px; } | |
| .player-btn.primary { width: 40px; height: 40px; } | |
| .brand-name { font-size: 1.1em; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- Header --> | |
| <div class="header"> | |
| <div class="header-left"> | |
| <div class="logo-mark"></div> | |
| <span class="brand-name">PROMETHEUS <span style="font-size:0.6em;font-weight:400;color:var(--text-tertiary);letter-spacing:0.03em">World Model</span></span> | |
| </div> | |
| </div> | |
| <!-- Status Pills --> | |
| <div class="status-pills"> | |
| <div class="pill"> | |
| <span class="pill-dot" id="statusDot"></span> | |
| <span class="pill-label">Status</span> | |
| <span class="pill-value" id="status">Idle</span> | |
| </div> | |
| <div class="pill"> | |
| <span class="pill-label">Buffer</span> | |
| <span class="pill-value" id="bufferSize">0 / 4</span> | |
| </div> | |
| <div class="pill"> | |
| <span class="pill-label">FPS</span> | |
| <span class="pill-value" id="fps">0</span> | |
| </div> | |
| <div class="pill"> | |
| <span class="pill-label">Frames</span> | |
| <span class="pill-value" id="frameCount">0</span> | |
| </div> | |
| <div class="pill"> | |
| <span class="pill-label">α</span> | |
| <span class="pill-value" id="currentSmoothing">0.50</span> | |
| </div> | |
| <div class="pill"> | |
| <span class="pill-label">History</span> | |
| <span class="pill-value" id="currentHistory">-</span> | |
| </div> | |
| </div> | |
| <!-- Prompt Bar --> | |
| <div class="prompt-bar"> | |
| <div class="prompt-input-wrap"> | |
| <input type="text" id="motionText" class="prompt-input" placeholder="Describe a motion… e.g. walk forward, jump, dance" value="walk in a circle."> | |
| </div> | |
| <button id="updateBtn" class="btn-update" disabled>Update</button> | |
| </div> | |
| <!-- Motion Presets --> | |
| <div class="preset-section"> | |
| <div class="preset-label">Quick Presets</div> | |
| <div class="preset-grid" id="presetGrid"> | |
| <button class="preset-chip" data-prompt="a person walking forward"><span class="emoji">🚶</span>Walk</button> | |
| <button class="preset-chip" data-prompt="a person running fast"><span class="emoji">🏃</span>Run</button> | |
| <button class="preset-chip" data-prompt="a person jumping up"><span class="emoji">🦘</span>Jump</button> | |
| <button class="preset-chip" data-prompt="a person dancing happily"><span class="emoji">💃</span>Dance</button> | |
| <button class="preset-chip" data-prompt="a person sitting down on a chair"><span class="emoji">🪑</span>Sit</button> | |
| <button class="preset-chip" data-prompt="a person kicking a ball"><span class="emoji">⚽</span>Kick</button> | |
| <button class="preset-chip" data-prompt="a person punching"><span class="emoji">🥊</span>Punch</button> | |
| <button class="preset-chip" data-prompt="a person sneaking quietly"><span class="emoji">🥷</span>Sneak</button> | |
| </div> | |
| </div> | |
| <!-- Avatar Style Selector --> | |
| <div class="style-section"> | |
| <div class="style-row"> | |
| <span class="preset-label">Avatar</span> | |
| <button class="style-chip active" data-style="robot"> | |
| <span class="emoji">🤖</span>Robot | |
| </button> | |
| <button class="style-chip" data-style="neon"> | |
| <span class="emoji">💚</span>Neon | |
| </button> | |
| <button class="style-chip" data-style="human"> | |
| <span class="emoji">🧑</span>Human | |
| </button> | |
| <div class="player-divider" style="height:20px; margin: 0 2px;"></div> | |
| <button class="style-chip" id="tankBtn" title="Tank mode"> | |
| <span class="emoji">🪖</span>Tank | |
| </button> | |
| <button class="style-chip" id="uploadModelBtn" title="Upload .glb file"> | |
| <span class="emoji">📁</span>3D Model | |
| </button> | |
| <button class="style-chip" data-style="model" id="modelChip" style="display:none;"> | |
| <span class="emoji">✨</span><span id="modelChipLabel">Model</span> | |
| </button> | |
| <input type="file" id="glbFileInput" accept=".glb,.gltf" style="display:none;"> | |
| <span id="modelStatus" class="model-status"></span> | |
| </div> | |
| </div> | |
| <!-- World Selector --> | |
| <div class="style-section" style="margin-bottom:6px"> | |
| <div class="style-row"> | |
| <span class="preset-label">World</span> | |
| <button class="world-chip active" data-world="castle"><span class="emoji">🏰</span>Castle</button> | |
| <button class="world-chip" data-world="inferno"><span class="emoji">🔥</span>Inferno</button> | |
| <button class="world-chip" data-world="horde"><span class="emoji">🧟</span>Horde</button> | |
| <button class="world-chip" data-world="countdown"><span class="emoji">⏰</span>Countdown</button> | |
| <button class="world-chip" data-world="dilemma"><span class="emoji">🎭</span>Dilemma</button> | |
| </div> | |
| </div> | |
| <!-- Conflict Warning --> | |
| <div id="conflictWarning" class="conflict-warning" style="display: none;"> | |
| <p><strong>⚠️ Another user is currently generating!</strong></p> | |
| <p>Force stop their session and take over?</p> | |
| <div class="btn-row"> | |
| <button id="forceTakeoverBtn" class="btn-danger">Force Takeover</button> | |
| <button id="cancelTakeoverBtn" class="btn-ghost">Cancel</button> | |
| </div> | |
| </div> | |
| <!-- Canvas --> | |
| <div id="canvas-container"> | |
| <canvas id="renderCanvas"></canvas> | |
| </div> | |
| <!-- Player Bar --> | |
| <div class="player-bar"> | |
| <!-- Play / Reset (primary) --> | |
| <button id="startResetBtn" class="player-btn primary" data-tip="Start"> | |
| <svg viewBox="0 0 24 24"><polygon points="8,5 19,12 8,19"/></svg> | |
| </button> | |
| <!-- Pause / Resume --> | |
| <button id="pauseResumeBtn" class="player-btn" data-tip="Pause" disabled> | |
| <svg viewBox="0 0 24 24"> | |
| <rect x="6" y="5" width="4" height="14" rx="1"/> | |
| <rect x="14" y="5" width="4" height="14" rx="1"/> | |
| </svg> | |
| </button> | |
| <div class="player-divider"></div> | |
| <!-- Record --> | |
| <button id="recordBtn" class="player-btn record-btn" data-tip="Record" disabled> | |
| <div class="rec-circle"></div> | |
| </button> | |
| <span id="recTimer" class="rec-timer">00:00</span> | |
| <div class="player-spacer"></div> | |
| <!-- Config gear --> | |
| <button id="configBtn" class="player-btn config-btn" data-tip="Config"> | |
| <svg viewBox="0 0 24 24"> | |
| <circle cx="12" cy="12" r="3"/> | |
| <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <!-- Footer --> | |
| <div class="footer-credit">VIDRAFT PROMETHEUS — Powered by FloodDiffusion</div> | |
| </div> | |
| <!-- Config Modal --> | |
| <div id="configModal" class="modal-overlay" style="display: none;"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h2>Config</h2> | |
| </div> | |
| <div class="modal-body"> | |
| <div class="config-section"> | |
| <h3>Schedule Config</h3> | |
| <div id="scheduleConfigFields"></div> | |
| </div> | |
| <div class="config-section"> | |
| <h3>CFG Config</h3> | |
| <div id="cfgConfigFields"></div> | |
| </div> | |
| <div class="config-section"> | |
| <h3>Runtime Parameters</h3> | |
| <div class="config-field"> | |
| <label for="modalHistoryLength">History Length</label> | |
| <input type="number" id="modalHistoryLength" value=""> | |
| </div> | |
| <div class="config-field"> | |
| <label for="modalSmoothingAlpha">Smoothing α</label> | |
| <div class="slider-container"> | |
| <input type="range" id="modalSmoothingAlpha" min="0" max="1" step="0.05" value="0.5"> | |
| <span id="modalSmoothingValue" class="slider-value">0.50</span> | |
| </div> | |
| <span class="param-hint">0.0 = max smoothing, 1.0 = no smoothing</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button class="btn btn-secondary" id="configDiscardBtn">Discard</button> | |
| <button class="btn btn-primary" id="configSaveBtn">Update & Reset</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script> | |
| <script src="/skeleton.js"></script> | |
| <script src="/world_manager.js"></script> | |
| <script src="/entity_manager.js"></script> | |
| <script src="/input_controller.js"></script> | |
| <script src="/main.js"></script> | |
| <script> | |
| /* ── Avatar Style Selector + GLB Upload ── */ | |
| (function() { | |
| var chips = document.querySelectorAll('.style-chip[data-style]'); | |
| var uploadBtn = document.getElementById('uploadModelBtn'); | |
| var fileInput = document.getElementById('glbFileInput'); | |
| var modelChip = document.getElementById('modelChip'); | |
| var modelChipLabel = document.getElementById('modelChipLabel'); | |
| var modelStatus = document.getElementById('modelStatus'); | |
| function activateChip(chip) { | |
| document.querySelectorAll('.style-chip').forEach(function(c) { c.classList.remove('active'); }); | |
| chip.classList.add('active'); | |
| } | |
| /* Style chip clicks */ | |
| chips.forEach(function(chip) { | |
| chip.addEventListener('click', function() { | |
| var style = chip.getAttribute('data-style'); | |
| if (!style) return; | |
| activateChip(chip); | |
| if (window.app && window.app.skeleton) { | |
| window.app.skeleton.setStyle(style); | |
| } | |
| window._avatarType = 'humanoid'; | |
| }); | |
| }); | |
| /* Tank button → auto-load tank.glb */ | |
| var tankBtn = document.getElementById('tankBtn'); | |
| tankBtn.addEventListener('click', async function() { | |
| var skel = window.app && window.app.skeleton; | |
| if (!skel) { | |
| modelStatus.textContent = 'Start generation first'; | |
| modelStatus.className = 'model-status error'; | |
| return; | |
| } | |
| tankBtn.classList.add('loading'); | |
| modelStatus.textContent = 'Loading tank...'; | |
| modelStatus.className = 'model-status loading'; | |
| try { | |
| var resp = await fetch('/tank.glb'); | |
| if (!resp.ok) throw new Error('tank.glb not found'); | |
| var buf = await resp.arrayBuffer(); | |
| var result = await skel.loadGLBModel(buf, 'tank.glb'); | |
| tankBtn.classList.remove('loading'); | |
| if (result.success) { | |
| modelChipLabel.textContent = 'Tank'; | |
| modelChip.style.display = ''; | |
| modelChip.querySelector('.emoji').textContent = '🪖'; | |
| modelStatus.textContent = 'Tank loaded'; | |
| modelStatus.className = 'model-status success'; | |
| activateChip(modelChip); | |
| skel.setStyle('model'); | |
| window._avatarType = 'tank'; | |
| } else { | |
| modelStatus.textContent = result.error || 'Load failed'; | |
| modelStatus.className = 'model-status error'; | |
| } | |
| } catch(err) { | |
| tankBtn.classList.remove('loading'); | |
| modelStatus.textContent = err.message; | |
| modelStatus.className = 'model-status error'; | |
| } | |
| }); | |
| /* Upload button → trigger file input */ | |
| uploadBtn.addEventListener('click', function() { | |
| fileInput.click(); | |
| }); | |
| /* File selected → load GLB */ | |
| fileInput.addEventListener('change', async function(e) { | |
| var file = e.target.files[0]; | |
| if (!file) return; | |
| var skel = window.app && window.app.skeleton; | |
| if (!skel) { | |
| modelStatus.textContent = 'Start generation first'; | |
| modelStatus.className = 'model-status error'; | |
| return; | |
| } | |
| /* Show loading state */ | |
| uploadBtn.classList.add('loading'); | |
| modelStatus.textContent = 'Loading…'; | |
| modelStatus.className = 'model-status loading'; | |
| try { | |
| var arrayBuffer = await file.arrayBuffer(); | |
| var result = await skel.loadGLBModel(arrayBuffer, file.name); | |
| uploadBtn.classList.remove('loading'); | |
| if (result.success) { | |
| /* Show model chip with truncated filename */ | |
| var shortName = file.name.replace(/\.(glb|gltf)$/i, ''); | |
| if (shortName.length > 12) shortName = shortName.substring(0, 12) + '…'; | |
| modelChipLabel.textContent = shortName; | |
| modelChip.style.display = ''; | |
| /* Status message */ | |
| var msg = result.mapped + '/' + result.total + ' bones mapped'; | |
| if (result.warning) msg += ' ⚠'; | |
| modelStatus.textContent = msg; | |
| modelStatus.className = 'model-status ' + (result.mapped > 5 ? 'success' : 'loading'); | |
| /* Auto-switch to model style */ | |
| activateChip(modelChip); | |
| skel.setStyle('model'); | |
| } else { | |
| modelStatus.textContent = result.error || 'Load failed'; | |
| modelStatus.className = 'model-status error'; | |
| } | |
| } catch (err) { | |
| uploadBtn.classList.remove('loading'); | |
| modelStatus.textContent = err.message || 'Error'; | |
| modelStatus.className = 'model-status error'; | |
| } | |
| /* Reset file input so same file can be re-selected */ | |
| fileInput.value = ''; | |
| }); | |
| })(); | |
| /* Auto-set robot style when skeleton is ready */ | |
| var _checkRobot = setInterval(function() { | |
| if (window.app && window.app.skeleton) { | |
| window.app.skeleton.setStyle('robot'); | |
| clearInterval(_checkRobot); | |
| } | |
| }, 500); | |
| /* ── Preset Emoji Buttons ── */ | |
| (function() { | |
| var grid = document.getElementById('presetGrid'); | |
| var input = document.getElementById('motionText'); | |
| if (!grid || !input) return; | |
| grid.addEventListener('click', function(e) { | |
| var chip = e.target.closest('.preset-chip'); | |
| if (!chip) return; | |
| var prompt = chip.getAttribute('data-prompt'); | |
| if (!prompt) return; | |
| /* Tank mode: convert preset prompts */ | |
| if (window._avatarType === 'tank') { | |
| var tankMap = { | |
| 'a person walking forward': 'a tank rolling forward steadily', | |
| 'a person running fast': 'a tank advancing at full speed', | |
| 'a person jumping up': 'a tank climbing over obstacle', | |
| 'a person dancing happily': 'a tank spinning turret in celebration', | |
| 'a person sitting down on a chair': 'a tank halting and idling engine', | |
| 'a person kicking a ball': 'a tank firing cannon forward', | |
| 'a person punching': 'a tank ramming forward aggressively', | |
| 'a person sneaking quietly': 'a tank creeping forward in stealth', | |
| }; | |
| prompt = tankMap[prompt] || prompt.replace('a person ', 'a tank '); | |
| } | |
| /* Set input value */ | |
| input.value = prompt; | |
| /* Visual active state */ | |
| grid.querySelectorAll('.preset-chip').forEach(function(c) { c.classList.remove('active'); }); | |
| chip.classList.add('active'); | |
| /* Flash input to show change */ | |
| input.style.borderColor = 'var(--accent)'; | |
| input.style.boxShadow = '0 0 0 3px rgba(26,26,26,0.06)'; | |
| setTimeout(function() { | |
| input.style.borderColor = ''; | |
| input.style.boxShadow = ''; | |
| }, 600); | |
| /* Trigger update button click if it's enabled (generation running) */ | |
| var updateBtn = document.getElementById('updateBtn'); | |
| if (updateBtn && !updateBtn.disabled) { | |
| updateBtn.click(); | |
| } | |
| }); | |
| })(); | |
| /* ── World Selector ── */ | |
| (function() { | |
| var wbtns = document.querySelectorAll('.world-chip[data-world]'); | |
| console.log('[World] Buttons found:', wbtns.length); | |
| wbtns.forEach(function(btn) { | |
| btn.onclick = function(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| var world = btn.getAttribute('data-world'); | |
| console.log('[World] Clicked:', world); | |
| if (!world) return; | |
| wbtns.forEach(function(b) { b.classList.remove('active'); }); | |
| btn.classList.add('active'); | |
| var em = window.app ? window.app.entityManager : null; | |
| if (!em) { | |
| console.error('[World] No entityManager'); | |
| return; | |
| } | |
| try { | |
| // clean up existing NPC on world switch | |
| fetch('/api/npc/despawn', {method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'}).catch(function(){}); | |
| if (window.app) { | |
| window.app._npcSpawnedByUser = false; | |
| window.app._npcState = null; | |
| if (window.app.npcSkeleton) { | |
| try { window.app.npcSkeleton.destroySkeleton(); } catch(e){} | |
| window.app.npcSkeleton = null; | |
| } | |
| if (window.app._npcTankModel) { | |
| try { window.app.scene.remove(window.app._npcTankModel); } catch(e){} | |
| window.app._npcTankModel = null; | |
| } | |
| window._npcWorldPos = null; | |
| } | |
| em.loadWorldObjects(world); | |
| console.log('[World] OK:', world, em.colliders.length, 'colliders'); | |
| } catch(err) { | |
| console.error('[World] Error:', err); | |
| } | |
| if (world === 'dilemma') { | |
| setTimeout(function() { | |
| if (window.app) window.app._npcSpawnedByUser = true; | |
| fetch('/api/npc/spawn', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({type:'beast'})}).catch(function(){}); | |
| }, 500); | |
| } | |
| }; | |
| }); | |
| })(); | |
| /* ── Status dot updater ── */ | |
| (function() { | |
| var statusEl = document.getElementById('status'); | |
| var dot = document.getElementById('statusDot'); | |
| if (!statusEl || !dot) return; | |
| var observer = new MutationObserver(function() { | |
| var t = statusEl.textContent || ''; | |
| dot.className = 'pill-dot'; | |
| if (t.indexOf('Generat') >= 0) dot.classList.add('generating'); | |
| else if (t.indexOf('Idle') < 0 && t.indexOf('Error') < 0) dot.classList.add('active'); | |
| }); | |
| observer.observe(statusEl, { childList: true, characterData: true, subtree: true }); | |
| })(); | |
| /* ── Dynamic button icon swap (Play ↔ Reset) ── */ | |
| (function() { | |
| var btn = document.getElementById('startResetBtn'); | |
| var statusEl = document.getElementById('status'); | |
| var playIcon = '<svg viewBox="0 0 24 24"><polygon points="8,5 19,12 8,19"/></svg>'; | |
| var resetIcon = '<svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 1 3.2 6.8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><polyline points="3 7 3 13 9 13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'; | |
| if (!btn || !statusEl) return; | |
| var observer = new MutationObserver(function() { | |
| var t = statusEl.textContent || ''; | |
| if (t.indexOf('Idle') >= 0 || t === '') { | |
| btn.innerHTML = playIcon; | |
| btn.setAttribute('data-tip', 'Start'); | |
| } else { | |
| btn.innerHTML = resetIcon; | |
| btn.setAttribute('data-tip', 'Reset'); | |
| } | |
| }); | |
| observer.observe(statusEl, { childList: true, characterData: true, subtree: true }); | |
| })(); | |
| /* ── Dynamic Pause icon swap (Pause ↔ Resume) ── */ | |
| (function() { | |
| var btn = document.getElementById('pauseResumeBtn'); | |
| var pauseIcon = '<svg viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>'; | |
| var resumeIcon = '<svg viewBox="0 0 24 24"><polygon points="8,5 19,12 8,19"/></svg>'; | |
| var isPaused = false; | |
| if (!btn) return; | |
| btn.addEventListener('click', function() { | |
| isPaused = !isPaused; | |
| btn.innerHTML = isPaused ? resumeIcon : pauseIcon; | |
| btn.setAttribute('data-tip', isPaused ? 'Resume' : 'Pause'); | |
| }); | |
| })(); | |
| /* ── MP4 Recording (MediaRecorder + canvas.captureStream) ── */ | |
| (function() { | |
| var recordBtn = document.getElementById('recordBtn'); | |
| var recTimer = document.getElementById('recTimer'); | |
| var canvas = document.getElementById('renderCanvas'); | |
| var mediaRecorder = null; | |
| var recordedChunks = []; | |
| var timerInterval = null; | |
| var startTime = 0; | |
| var isRecording = false; | |
| var checkInterval = setInterval(function() { | |
| var statusEl = document.getElementById('status'); | |
| if (statusEl && statusEl.textContent && statusEl.textContent.indexOf('Generat') >= 0) { | |
| recordBtn.disabled = false; | |
| } | |
| if (canvas && canvas.width > 0 && document.getElementById('frameCount')) { | |
| var fc = document.getElementById('frameCount').textContent; | |
| if (parseInt(fc) > 0) recordBtn.disabled = false; | |
| } | |
| }, 1000); | |
| function formatTime(ms) { | |
| var s = Math.floor(ms / 1000); | |
| var m = Math.floor(s / 60); | |
| s = s % 60; | |
| return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s; | |
| } | |
| function startRecording() { | |
| recordedChunks = []; | |
| var stream = canvas.captureStream(30); | |
| var mimeType = 'video/webm;codecs=vp9'; | |
| if (!MediaRecorder.isTypeSupported(mimeType)) mimeType = 'video/webm;codecs=vp8'; | |
| if (!MediaRecorder.isTypeSupported(mimeType)) mimeType = 'video/webm'; | |
| mediaRecorder = new MediaRecorder(stream, { mimeType: mimeType, videoBitsPerSecond: 5000000 }); | |
| mediaRecorder.ondataavailable = function(e) { | |
| if (e.data && e.data.size > 0) recordedChunks.push(e.data); | |
| }; | |
| mediaRecorder.onstop = function() { | |
| var blob = new Blob(recordedChunks, { type: 'video/webm' }); | |
| var promptEl = document.getElementById('motionText'); | |
| var promptText = (promptEl && promptEl.value) ? promptEl.value.trim() : 'motion'; | |
| promptText = promptText.replace(/[^a-zA-Z0-9_\- ]/g, '').replace(/\s+/g, '_').substring(0, 40); | |
| var timestamp = new Date().toISOString().slice(0,19).replace(/[:\-T]/g, ''); | |
| var filename = 'PROMETHEUS_' + promptText + '_' + timestamp + '.webm'; | |
| var url = URL.createObjectURL(blob); | |
| var a = document.createElement('a'); | |
| a.href = url; a.download = filename; | |
| document.body.appendChild(a); a.click(); document.body.removeChild(a); | |
| setTimeout(function() { URL.revokeObjectURL(url); }, 5000); | |
| }; | |
| mediaRecorder.start(100); | |
| isRecording = true; | |
| startTime = Date.now(); | |
| recordBtn.classList.add('recording'); | |
| recTimer.classList.add('active'); | |
| timerInterval = setInterval(function() { | |
| recTimer.textContent = formatTime(Date.now() - startTime); | |
| }, 500); | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop(); | |
| isRecording = false; | |
| recordBtn.classList.remove('recording'); | |
| recTimer.classList.remove('active'); | |
| recTimer.textContent = '00:00'; | |
| if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } | |
| } | |
| recordBtn.addEventListener('click', function() { | |
| if (isRecording) stopRecording(); else startRecording(); | |
| }); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |