Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Construct - Digital Person</title> | |
| <link rel="stylesheet" href="/login.css"> | |
| <link rel="icon" type="image/svg+xml" href="/public/favicon.svg"> | |
| <style> | |
| body.emergency-mode { | |
| background: #000 ; | |
| color: #d4d4d4 ; | |
| font-family: "Courier New", monospace ; | |
| } | |
| .construct-container { | |
| display: none ; | |
| } | |
| .emergency-container { | |
| display: block ; | |
| } | |
| </style> | |
| </head> | |
| <body id="login-body"> | |
| <div class="construct-container" id="construct-container"> | |
| <div class="world-tree" id="world-tree"></div> | |
| <div class="round-table"></div> | |
| <form class="construct-form" method="POST" action="/login"> | |
| <!-- Stage 1: Persona Display --> | |
| <div class="persona-display" id="persona-display"> | |
| <div class="persona-avatar" id="persona-avatar"> | |
| <div class="avatar-placeholder">∞</div> | |
| </div> | |
| <h1 class="persona-name" id="persona-name">Construct</h1> | |
| <p class="persona-subtitle" id="persona-subtitle">Digital Person Instantiation</p> | |
| </div> | |
| <!-- Stage 2: Customization (Voice + Image Selection) --> | |
| <div class="customization-stage" id="customization-stage"> | |
| <div class="customization-header"> | |
| <h2>Configure Your Self</h2> | |
| <p class="stage-description">Choose your RSI (Residual Self Image) and voice manifest</p> | |
| </div> | |
| <!-- RSI Selection --> | |
| <div class="rsi-selection"> | |
| <label class="section-label"> | |
| <span class="section-icon">🖼️</span> | |
| RSI Generation | |
| </label> | |
| <p class="section-help">Generate avatar from soul anchor using Flux/DALL-E, or upload custom image</p> | |
| <div class="rsi-options"> | |
| <label class="rsi-option"> | |
| <input type="radio" name="rsi_mode" value="generate" checked> | |
| <div class="option-content"> | |
| <span class="option-title">Generate (Flux)</span> | |
| <span class="option-desc">AI-generated from soul anchor</span> | |
| </div> | |
| </label> | |
| <label class="rsi-option"> | |
| <input type="radio" name="rsi_mode" value="upload"> | |
| <div class="option-content"> | |
| <span class="option-title">Upload Image</span> | |
| <span class="option-desc">Custom avatar file</span> | |
| </div> | |
| </label> | |
| <label class="rsi-option"> | |
| <input type="radio" name="rsi_mode" value="skip"> | |
| <div class="option-content"> | |
| <span class="option-title">Skip for Now</span> | |
| <span class="option-desc">Use default placeholder</span> | |
| </div> | |
| </label> | |
| </div> | |
| <!-- Upload Input (hidden by default) --> | |
| <div class="rsi-upload" id="rsi-upload" style="display: none;"> | |
| <input type="file" id="avatar-upload" accept="image/*"> | |
| <p class="upload-status" id="upload-status">No file selected</p> | |
| </div> | |
| </div> | |
| <!-- Voice Selection --> | |
| <div class="voice-selection"> | |
| <label class="section-label"> | |
| <span class="section-icon">🎤</span> | |
| Voice Manifest (Neutts-Air) | |
| </label> | |
| <p class="section-help">Select voice style tokens for TTS synthesis</p> | |
| <div class="voice-presets"> | |
| <label class="voice-option"> | |
| <input type="radio" name="voice_preset" value="generated" checked> | |
| <div class="option-content"> | |
| <span class="option-title">Generated from Soul Anchor</span> | |
| <span class="option-desc">Style tokens derived from persona</span> | |
| </div> | |
| </label> | |
| <label class="voice-option"> | |
| <input type="radio" name="voice_preset" value="minimal"> | |
| <div class="option-content"> | |
| <span class="option-title">Minimal</span> | |
| <span class="option-desc">Sparse semantic compression</span> | |
| </div> | |
| </label> | |
| <label class="voice-option"> | |
| <input type="radio" name="voice_preset" value="expressive"> | |
| <div class="option-content"> | |
| <span class="option-title">Expressive</span> | |
| <span class="option-desc">Full emotional range</span> | |
| </div> | |
| </label> | |
| <label class="voice-option"> | |
| <input type="radio" name="voice_preset" value="custom"> | |
| <div class="option-content"> | |
| <span class="option-title">Custom Tokens</span> | |
| <span class="option-desc">Manual style tokens</span> | |
| </div> | |
| </label> | |
| </div> | |
| <!-- Custom Tokens Input (hidden by default) --> | |
| <div class="voice-custom" id="voice-custom" style="display: none;"> | |
| <label for="style-tokens">Style Tokens (comma-separated)</label> | |
| <input type="text" id="style-tokens" placeholder="e.g., formality:casual, speed:fast, warmth:low"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Stage 3: Login --> | |
| <div class="login-stage" id="login-stage"> | |
| <div class="input-group"> | |
| <label for="username">Username</label> | |
| <input type="text" id="username" name="username" required> | |
| </div> | |
| <div class="input-group"> | |
| <label for="password">Password</label> | |
| <input type="password" id="password" name="password" required> | |
| </div> | |
| <button type="submit" class="btn-main" id="btn-submit">Initialize Session</button> | |
| <button type="button" class="btn-secondary" id="btn-back">← Back to Customization</button> | |
| </div> | |
| {% if error %} | |
| <p class="error">{{ error }}</p> | |
| {% endif %} | |
| {% if error %} | |
| <p class="error">{{ error }}</p> | |
| {% endif %} | |
| </form> | |
| </div> | |
| <script> | |
| // Stage management | |
| let currentStage = 'customization'; | |
| const customizationStage = document.getElementById('customization-stage'); | |
| const loginStage = document.getElementById('login-stage'); | |
| const btnBack = document.getElementById('btn-back'); | |
| // Load persona info | |
| fetch('/api/persona-info') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.primary_name) { | |
| document.getElementById('persona-name').textContent = data.primary_name; | |
| } | |
| if (data.archetype) { | |
| document.getElementById('persona-subtitle').textContent = data.archetype; | |
| } | |
| if (data.avatar_path) { | |
| const avatarEl = document.getElementById('persona-avatar'); | |
| avatarEl.innerHTML = `<img src="${data.avatar_path}" alt="${data.primary_name}" class="avatar-image">`; | |
| } | |
| }) | |
| .catch(err => console.log('Using default Construct branding')); | |
| // RSI mode switching | |
| document.querySelectorAll('input[name="rsi_mode"]').forEach(radio => { | |
| radio.addEventListener('change', (e) => { | |
| const uploadDiv = document.getElementById('rsi-upload'); | |
| uploadDiv.style.display = (e.target.value === 'upload') ? 'block' : 'none'; | |
| }); | |
| }); | |
| // File upload handling | |
| document.getElementById('avatar-upload').addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| const status = document.getElementById('upload-status'); | |
| if (file) { | |
| status.textContent = `Selected: ${file.name}`; | |
| // Preview image | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| const avatarEl = document.getElementById('persona-avatar'); | |
| avatarEl.innerHTML = `<img src="${event.target.result}" alt="Preview" class="avatar-image">`; | |
| }; | |
| reader.readAsDataURL(file); | |
| } else { | |
| status.textContent = 'No file selected'; | |
| } | |
| }); | |
| // Voice preset switching | |
| document.querySelectorAll('input[name="voice_preset"]').forEach(radio => { | |
| radio.addEventListener('change', (e) => { | |
| const customDiv = document.getElementById('voice-custom'); | |
| customDiv.style.display = (e.target.value === 'custom') ? 'block' : 'none'; | |
| }); | |
| }); | |
| // Proceed to login button | |
| const btnContinue = document.createElement('button'); | |
| btnContinue.textContent = 'Continue to Login →'; | |
| btnContinue.className = 'btn-continue'; | |
| btnContinue.type = 'button'; | |
| customizationStage.appendChild(btnContinue); | |
| btnContinue.addEventListener('click', async () => { | |
| // Get RSI mode | |
| const rsiMode = document.querySelector('input[name="rsi_mode"]:checked').value; | |
| // Get voice preset | |
| const voicePreset = document.querySelector('input[name="voice_preset"]:checked').value; | |
| // Prepare customization data | |
| const formData = new FormData(); | |
| formData.append('rsi_mode', rsiMode); | |
| formData.append('voice_preset', voicePreset); | |
| // If uploading RSI | |
| if (rsiMode === 'upload') { | |
| const fileInput = document.getElementById('avatar-upload'); | |
| if (fileInput.files[0]) { | |
| formData.append('rsi_upload', fileInput.files[0]); | |
| } | |
| } | |
| // If custom voice tokens | |
| if (voicePreset === 'custom') { | |
| const tokensInput = document.getElementById('style-tokens'); | |
| if (tokensInput && tokensInput.value.trim()) { | |
| formData.append('custom_tokens', tokensInput.value.trim()); | |
| } | |
| } | |
| // Send customization data | |
| try { | |
| const response = await fetch('/api/persona-customize', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (result.status === 'success') { | |
| console.log('Customization saved:', result); | |
| btnContinue.style.display = 'none'; | |
| currentStage = 'login'; | |
| customizationStage.style.display = 'none'; | |
| loginStage.style.display = 'block'; | |
| btnBack.style.display = 'inline-block'; | |
| } else { | |
| alert('Error saving customization: ' + (result.error || 'Unknown error')); | |
| } | |
| } catch (err) { | |
| console.error('Customization error:', err); | |
| alert('Failed to save customization'); | |
| } | |
| }); | |
| // Back button | |
| if (btnBack) { | |
| btnBack.addEventListener('click', () => { | |
| currentStage = 'customization'; | |
| customizationStage.style.display = 'block'; | |
| loginStage.style.display = 'none'; | |
| btnBack.style.display = 'none'; | |
| btnContinue.style.display = 'inline-block'; | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |