World-Model / index.html
SeaWolf-AI's picture
Upload 9 files
5fcfe30 verified
<!DOCTYPE html>
<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 &amp; 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>