| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>StrokeGPT - AI Hardware Controller</title> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap'); |
| |
| :root { |
| --background-darker: #21222C; |
| --background: #282a36; |
| --background-lighter: #343746; |
| --foreground: #f8f8f2; |
| --comment: #6272a4; |
| --cyan: #8be9fd; |
| --purple: #bd93f9; |
| --pink: #ff79c6; |
| --red: #ff5555; |
| --yellow: #f1fa8c; |
| --shadow: rgba(0, 0, 0, 0.2); |
| } |
| |
| * { box-sizing: border-box; } |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| background-color: var(--background-darker); |
| color: var(--foreground); |
| margin: 0; |
| height: 100vh; |
| overflow: hidden; |
| display: grid; |
| grid-template-columns: 1fr 320px; |
| transition: grid-template-columns 0.35s ease-in-out; |
| } |
| |
| |
| @media (max-width: 768px) { |
| body { |
| grid-template-columns: 1fr 0px; |
| font-size: 16px; |
| } |
| body:not(.sidebar-collapsed) { |
| grid-template-columns: 1fr 280px; |
| } |
| #splash-screen p { |
| font-size: 1.5rem; |
| padding: 0 20px; |
| text-align: center; |
| } |
| #top-bar h1 { |
| font-size: 1.2rem; |
| } |
| .top-bar-info { |
| font-size: 0.8rem; |
| } |
| #user-chat-input { |
| font-size: 16px; |
| } |
| .setting-section { |
| padding: 15px; |
| } |
| .input-text, .select-box { |
| font-size: 16px; |
| padding: 12px; |
| } |
| .my-button { |
| padding: 12px 16px; |
| font-size: 16px; |
| } |
| } |
| body.sidebar-collapsed { grid-template-columns: 1fr 0px; } |
| |
| #splash-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: #000; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 10000; transition: opacity 1s ease-out; opacity: 1; } |
| #splash-screen.hidden { opacity: 0; pointer-events: none; } |
| #splash-screen img { max-width: 90%; max-height: 70%; object-fit: contain; border-radius: 16px; } |
| #splash-screen p { margin-top: 20px; font-size: 2rem; color: #fff; text-shadow: 0 0 10px var(--pink), 0 0 20px var(--pink); animation: pulse 2s infinite; cursor: pointer; } |
| @keyframes pulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } } |
| |
| #main-area { flex-grow: 1; display: flex; flex-direction: column; height: 100%; background-color: var(--background); overflow: hidden; } |
| |
| #sidebar { padding: 20px; background-color: var(--background-darker); overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; gap: 20px; border-left: 1px solid var(--background-lighter); } |
| |
| #web-browser { |
| transition: opacity 0.3s ease; |
| } |
| |
| #web-browser:hover { |
| opacity: 0.9; |
| } |
| |
| @media (max-width: 768px) { |
| #sidebar { |
| position: fixed; |
| top: 0; |
| right: 0; |
| height: 100vh; |
| z-index: 1000; |
| transform: translateX(100%); |
| transition: transform 0.3s ease-in-out; |
| width: 300px; |
| box-shadow: -5px 0 15px rgba(0,0,0,0.3); |
| } |
| body:not(.sidebar-collapsed) #sidebar { |
| transform: translateX(0); |
| } |
| #toggle-sidebar-btn { |
| z-index: 1001; |
| } |
| #web-browser { |
| height: 150px; |
| } |
| } |
| |
| #top-bar { padding: 10px 20px; background-color: var(--background); border-bottom: 1px solid var(--background-lighter); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; position: relative; } |
| #toggle-sidebar-btn { position: absolute; top: 50%; right: 20px; transform: translateY(-50%); background: var(--background-lighter); border: none; color: var(--foreground); width: 32px; height: 32px; border-radius: 50%; cursor: pointer; transition: transform 0.3s ease-in-out, background-color 0.2s ease; font-size: 1.2em; display: flex; align-items: center; justify-content: center; } |
| #toggle-sidebar-btn:hover { background-color: var(--comment); } |
| body.sidebar-collapsed #toggle-sidebar-btn { transform: translateY(-50%) rotate(180deg); } |
| |
| #top-bar h1 { margin: 0; font-size: 1.5rem; color: var(--cyan); font-weight: 600; letter-spacing: 1px; text-shadow: 0 0 5px rgba(139, 233, 253, 0.4); } |
| #top-bar .top-bar-info { display: flex; align-items: center; gap: 15px; margin-right: 45px; } |
| #mood-display, #edging-timer { background-color: var(--background-lighter); padding: 6px 12px; border-radius: 8px; font-size: 0.9em; } |
| #edging-timer { color: var(--yellow); font-weight: 600; } |
| |
| #chat-view { |
| flex-grow: 1; |
| padding: 20px; |
| overflow-y: auto; |
| display: flex; |
| flex-direction: column; |
| background-image: url('/static/chat-background.png'); |
| background-size: cover; |
| background-position: center; |
| background-repeat: no-repeat; |
| background-attachment: fixed; |
| position: relative; |
| } |
| |
| #chat-view::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: rgba(40, 42, 54, 0.7); |
| z-index: 1; |
| } |
| |
| #chat-messages-container { |
| position: relative; |
| z-index: 2; |
| } |
| #bottom-input-area { padding: 20px; border-top: 1px solid var(--background-lighter); background-color: var(--background); flex-shrink: 0; position: relative; z-index: 10; } |
| |
| .chat-message-container { |
| display: flex; |
| align-items: flex-end; |
| gap: 10px; |
| margin-bottom: 15px; |
| max-width: 85%; |
| animation: fadeIn 0.4s ease-out; |
| position: relative; |
| z-index: 3; |
| } |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } |
| |
| .chat-pfp { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; flex-shrink: 0; } |
| .message-content { display: flex; flex-direction: column; } |
| .message-bubble { padding: 12px 16px; border-radius: 18px; line-height: 1.5; word-wrap: break-word; } |
| .message-bubble pre { font-family: 'Courier New', Courier, monospace; font-size: 0.9em; text-align: left; white-space: pre; } |
| |
| .bot-bubble { align-self: flex-start; } |
| .bot-bubble .message-bubble { background-color: var(--background-lighter); border-top-left-radius: 4px; } |
| .bot-bubble .speaker-name { align-self: flex-start; } |
| |
| .user-bubble { align-self: flex-end; } |
| .user-bubble .message-bubble { background-color: var(--comment); color: var(--foreground); border-top-right-radius: 4px; } |
| |
| .speaker-name { font-size: 0.8em; margin-bottom: 4px; color: var(--purple); font-weight: 600; } |
| .user-bubble .speaker-name { align-self: flex-end; } |
| |
| #visualizer-box { height: 40px; background-color: var(--background-darker); border-radius: 8px; margin-bottom: 15px; padding: 5px; pointer-events: none; } |
| .setting-section { border-radius: 12px; padding: 15px; background-color: var(--background); box-shadow: 0 4px 12px var(--shadow); } |
| .setting-section h3 { margin-top: 0; margin-bottom: 15px; font-size: 1.1rem; font-weight: 600; color: var(--cyan); border-bottom: 1px solid var(--background-lighter); padding-bottom: 10px; } |
| |
| .my-button { padding: 10px 16px; border: none; background-color: var(--comment); color: var(--foreground); border-radius: 8px; cursor: pointer; transition: all 0.2s ease; font-size: 1em; font-weight: 600; width: 100%; box-shadow: 0 2px 4px var(--shadow); } |
| .my-button:hover { background-color: #798dcc; transform: translateY(-2px); box-shadow: 0 4px 8px var(--shadow); } |
| .my-button:active { transform: translateY(0); box-shadow: 0 2px 4px var(--shadow); } |
| .sidebar-button.edging { background-color: var(--yellow); color: var(--background); } |
| .sidebar-button.edging:hover { background-color: #f7ffae; } |
| .sidebar-button.milking { background-color: var(--pink); } |
| .sidebar-button.milking:hover { background-color: #ff92d0; } |
| .sidebar-button.stop { background-color: var(--red); } |
| .sidebar-button.stop:hover { background-color: #ff6e6e; } |
| |
| .input-text, .select-box, input[type="number"] { padding: 10px; border: 1px solid var(--background-lighter); background-color: var(--background-lighter); color: var(--foreground); border-radius: 8px; font-size: 1em; width: 100%; transition: border-color 0.2s ease, box-shadow 0.2s ease; } |
| .input-text:focus, .select-box:focus, input[type="number"]:focus { outline: none; border-color: var(--purple); box-shadow: 0 0 0 3px rgba(189, 147, 249, 0.3); } |
| |
| #message-input-line { display: flex; gap: 10px; align-items: center; } |
| #message-input-line .my-button { width: auto; flex-shrink: 0; } |
| #message-input-line .input-text { flex-grow: 1; } |
| |
| .audio-toggle-line { display: flex; align-items: center; gap: 8px; padding-top: 10px; } |
| |
| #status-text { margin-top: 15px; font-size: 0.9em; text-align: center; color: var(--purple); } |
| #setup-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: none; align-items: center; justify-content: center; z-index: 9999; color: var(--foreground); } |
| #setup-box { text-align: center; background-color: var(--background); padding: 30px; border-radius: 12px; border: 1px solid var(--background-lighter); box-shadow: 0 8px 32px var(--shadow); } |
| #setup-box h2 { color: var(--cyan); margin-top: 0; } |
| #setup-box button { margin-top: 15px; } |
| .slider-container { margin: 15px 0; } |
| |
| .timing-row { display: flex; align-items: center; gap: 5px; margin-bottom: 8px; } |
| .timing-row label { flex-basis: 50px; font-size: 0.9em; } |
| .timing-row input { flex-grow: 1; width: 40px; } |
| .timing-row span { padding: 0 5px; } |
| |
| .typing-dots span { display: inline-block; animation: blink 1.4s infinite both; font-size: 1.5em; line-height: 0; } |
| .typing-dots span:nth-child(2) { animation-delay: 0.2s; } |
| .typing-dots span:nth-child(3) { animation-delay: 0.4s; } |
| @keyframes blink { 0% { opacity: 0.2; } 20% { opacity: 1; } 100% { opacity: 0.2; } } |
| |
| #easter-egg-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #000; color: #0f0; font-family: 'Courier New', monospace; font-size: 1.5em; display: none; align-items: center; justify-content: center; z-index: 10001; opacity: 0; transition: opacity 1s ease-in-out; text-align: center; } |
| |
| </style> |
| </head> |
| <body> |
| <div id="splash-screen"> |
| <img src="/static/splash.jpg" alt="StrokeGPT" onerror="this.style.display='none'"> |
| <p>Click anywhere to start</p> |
| </div> |
| <div id="main-area"> |
| <div id="top-bar"> |
| <h1>StrokeGPT</h1> |
| <div class="top-bar-info"> |
| <div id="edging-timer" style="display: none;">00:00</div> |
| <div id="mood-display">Mood: ...</div> |
| </div> |
| <button id="toggle-sidebar-btn">«</button> |
| </div> |
| <div id="chat-view"> |
| <div id="chat-messages-container"> |
| <div id="typing-indicator" class="chat-message-container bot-bubble" style="display: none;"> |
| <img class="chat-pfp" id="typing-indicator-pfp" src="/static/default-pfp.png" alt="pfp" onerror="this.style.display='none'"> |
| <div class="message-content"> |
| <p class="speaker-name">BOT</p> |
| <p class="message-bubble typing-dots"><span>.</span><span>.</span><span>.</span></p> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div id="bottom-input-area"> |
| <div id="visualizer-box" style="pointer-events: none;"><canvas id="rhythm-canvas" style="pointer-events: none;"></canvas></div> |
| <div id="message-input-line"> |
| <input type="text" id="user-chat-input" class="input-text" placeholder="Type a message or command..." style="z-index: 100; position: relative;"> |
| <button id="send-chat-btn" class="my-button">Send</button> |
| </div> |
| <div id="status-text">Status: Ready to chat! (Note: Ollama AI server needed for responses)</div> |
| </div> |
| </div> |
| <div id="sidebar"> |
| <div class="setting-section"> |
| <h3>Web Browser</h3> |
| <div id="browser-controls"> |
| <input type="url" id="browser-url" class="input-text" placeholder="Enter URL (e.g., youtube.com)" style="margin-bottom: 10px;"> |
| <div style="display: flex; gap: 5px; margin-bottom: 10px;"> |
| <button id="browser-go" class="my-button" style="flex: 1;">Go</button> |
| <button id="browser-home" class="my-button" style="flex: 1;">Home</button> |
| <button id="browser-refresh" class="my-button" style="flex: 1;">β³</button> |
| </div> |
| </div> |
| <div id="browser-container"> |
| <iframe id="web-browser" src="about:blank" style="width: 100%; height: 200px; border: 1px solid var(--background-lighter); border-radius: 8px; background: var(--background-lighter);"></iframe> |
| </div> |
| </div> |
| <div class="setting-section"> |
| <h3>Persona</h3> |
| <label for="pfp-upload" style="cursor: pointer; display: block; margin-bottom: 15px;"> |
| <img id="ai-pfp-preview" src="/static/default-pfp.png" style="width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin: 10px auto; display: block; border: 2px solid var(--comment);" onerror="this.style.display='none'"> |
| </label> |
| <input type="file" id="pfp-upload" accept="image/*" style="display: none;"> |
| <input type="text" id="ai-name-input" class="input-text" placeholder="AI's Name..." style="margin-bottom: 10px;"> |
| <button id="set-ai-name-btn" class="my-button" style="margin-bottom: 15px;">Set Name</button> |
| <input type="text" id="persona-input" class="input-text" placeholder="Describe the AI's persona..."> |
| <button id="set-persona-btn" class="my-button" style="margin-top: 10px;">Set Persona</button> |
| </div> |
| <div class="setting-section"> |
| <h3>Mode Timings (Seconds)</h3> |
| <div class="timing-row"><label for="auto-min-time">Auto:</label><input type="number" id="auto-min-time" min="1" max="60" step="1" value="4"><span>-</span><input type="number" id="auto-max-time" min="1" max="60" step="1" value="7"></div> |
| <div class="timing-row"><label for="edging-min-time">Edging:</label><input type="number" id="edging-min-time" min="1" max="60" step="1" value="5"><span>-</span><input type="number" id="edging-max-time" min="1" max="60" step="1" value="8"></div> |
| <div class="timing-row"><label for="milking-min-time">Milking:</label><input type="number" id="milking-min-time" min="1" max="60" step="1" value="2"><span>-</span><input type="number" id="milking-max-time" min="1" max="60" step="1" value="5"></div> |
| <button id="save-timings-btn" class="my-button">Save Timings</button> |
| </div> |
| <div class="setting-section"> |
| <h3>Voice Output (ElevenLabs)</h3> |
| <input type="password" id="elevenlabs-key-input" class="input-text" placeholder="ElevenLabs API Key"> |
| <button id="set-elevenlabs-key-button" class="my-button">Set Key</button> |
| <select id="elevenlabs-voice-select-box" class="select-box" disabled><option>Set API Key to load voices...</option></select> |
| <div class="audio-toggle-line"> |
| <label for="elevenlabs-enabled-checkbox">Enable Voice:</label> |
| <input type="checkbox" id="elevenlabs-enabled-checkbox" disabled> |
| </div> |
| </div> |
| <div class="setting-section"> |
| <h3>Hardware Settings</h3> |
| <input type="password" id="handy-key-input" class="input-text" placeholder="Hardware API Key"> |
| <button id="set-handy-key-btn" class="my-button">Set Key</button> |
| </div> |
| <div class="setting-section"> |
| <h3>Control Modes</h3> |
| <div style="margin-bottom: 10px;"> |
| <label for="pattern-mode-checkbox" style="display: flex; align-items: center; gap: 8px;"> |
| <input type="checkbox" id="pattern-mode-checkbox"> |
| <span>Use Pattern Mode</span> |
| </label> |
| </div> |
| <button id="auto-mode-btn" class="my-button sidebar-button">Auto Mode</button> |
| <button id="edging-mode-btn" class="my-button sidebar-button edging">Edging Mode</button> |
| <button id="milking-mode-btn" class="my-button sidebar-button milking">Milking Mode</button> |
| <button id="edge-signal-btn" class="my-button" style="display: none;">I'm Close!</button> |
| <button id="stop-all-btn" class="my-button sidebar-button stop">Stop All</button> |
| </div> |
| </div> |
|
|
| |
| <div id="setup-overlay"> |
| <div id="setup-box"> |
| <h2>Welcome to StrokeGPT</h2> |
| <p>Let's get you set up quickly.</p> |
| <div style="margin: 20px 0;"> |
| <label>Hardware API Key:</label> |
| <input type="password" id="setup-handy-key" class="input-text" placeholder="Enter your API key"> |
| </div> |
| <div style="margin: 20px 0;"> |
| <label>AI Persona:</label> |
| <input type="text" id="setup-persona" class="input-text" placeholder="Describe your ideal AI partner" value="An energetic and passionate girlfriend"> |
| </div> |
| <button id="setup-done-btn" class="my-button">Get Started</button> |
| </div> |
| </div> |
|
|
| <div id="easter-egg-overlay"> |
| <div>CLASSIFIED ACCESS GRANTED<br><br>METAL GEAR SOLID REFERENCE DETECTED<br><br>INITIATING TACTICAL ESPIONAGE ACTION</div> |
| </div> |
|
|
| <script> |
| |
| let currentAIName = "BOT"; |
| let currentPersona = ""; |
| let currentHandyKey = ""; |
| let currentElevenLabsKey = ""; |
| let audioEnabled = false; |
| let sidebarCollapsed = false; |
| let isSetupComplete = false; |
| let isPolling = false; |
| let pendingAudioRequests = []; |
| |
| |
| function showStatus(text, isError = false) { |
| const statusEl = document.getElementById('status-text'); |
| statusEl.textContent = text; |
| statusEl.style.color = isError ? 'var(--red)' : 'var(--purple)'; |
| } |
| |
| function addChatMessage(content, sender = "bot", useCurrentName = true) { |
| const container = document.getElementById('chat-messages-container'); |
| const msgDiv = document.createElement('div'); |
| msgDiv.className = `chat-message-container ${sender === 'user' ? 'user-bubble' : 'bot-bubble'}`; |
| |
| const displayName = sender === 'bot' && useCurrentName ? currentAIName : 'You'; |
| const pfpSrc = sender === 'bot' ? document.getElementById('ai-pfp-preview').src : '/static/default-pfp.png'; |
| |
| msgDiv.innerHTML = ` |
| <img class="chat-pfp" src="${pfpSrc}" alt="pfp" onerror="this.style.display='none'"> |
| <div class="message-content"> |
| <p class="speaker-name">${displayName}</p> |
| <p class="message-bubble">${content}</p> |
| </div> |
| `; |
| |
| container.appendChild(msgDiv); |
| container.scrollTop = container.scrollHeight; |
| |
| if (sender === 'bot' && audioEnabled) { |
| requestAudioForMessage(content); |
| } |
| } |
| |
| function showTypingIndicator() { |
| document.getElementById('typing-indicator').style.display = 'flex'; |
| document.getElementById('typing-indicator-pfp').src = document.getElementById('ai-pfp-preview').src; |
| document.querySelector('#typing-indicator .speaker-name').textContent = currentAIName; |
| } |
| |
| function hideTypingIndicator() { |
| document.getElementById('typing-indicator').style.display = 'none'; |
| } |
| |
| function updateMoodDisplay(mood) { |
| document.getElementById('mood-display').textContent = `Mood: ${mood}`; |
| } |
| |
| function updateEdgingTimer(elapsed) { |
| const timerEl = document.getElementById('edging-timer'); |
| if (elapsed !== null) { |
| const minutes = Math.floor(elapsed / 60); |
| const seconds = elapsed % 60; |
| timerEl.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
| timerEl.style.display = 'block'; |
| } else { |
| timerEl.style.display = 'none'; |
| } |
| } |
| |
| function requestAudioForMessage(text) { |
| |
| const cleanText = text.replace(/<[^>]+>/g, '').trim(); |
| if (!cleanText || cleanText.startsWith('(') || cleanText.startsWith('[')) return; |
| |
| |
| if (pendingAudioRequests.length > 2) return; |
| |
| pendingAudioRequests.push(cleanText); |
| setTimeout(() => { |
| fetch('/get_audio') |
| .then(response => { |
| if (response.ok && response.headers.get('content-type')?.includes('audio')) { |
| return response.blob(); |
| } |
| return null; |
| }) |
| .then(blob => { |
| if (blob) { |
| const audio = new Audio(URL.createObjectURL(blob)); |
| audio.play().catch(e => console.warn('Audio playback failed:', e)); |
| } |
| pendingAudioRequests.shift(); |
| }) |
| .catch(e => { |
| console.warn('Audio request failed:', e); |
| pendingAudioRequests.shift(); |
| }); |
| }, 1000); |
| } |
| |
| |
| async function sendMessage(message) { |
| try { |
| showTypingIndicator(); |
| const response = await fetch('/send_message', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| message: message, |
| persona_desc: currentPersona, |
| key: currentHandyKey |
| }) |
| }); |
| |
| const data = await response.json(); |
| hideTypingIndicator(); |
| |
| if (data.status === 'no_key_set') { |
| showStatus('Please set your hardware API key first.', true); |
| return false; |
| } else if (data.status === 'message_relayed_to_active_mode') { |
| showStatus('Message sent to active mode.'); |
| return true; |
| } else if (data.status === 'ok') { |
| showStatus('Message sent successfully.'); |
| return true; |
| } else { |
| showStatus(`Command executed: ${data.status}`); |
| return true; |
| } |
| } catch (error) { |
| hideTypingIndicator(); |
| showStatus('Failed to send message. Check connection.', true); |
| console.error('Send message error:', error); |
| return false; |
| } |
| } |
| |
| async function checkSettings() { |
| try { |
| const response = await fetch('/check_settings'); |
| const data = await response.json(); |
| |
| if (data.configured) { |
| currentPersona = data.persona; |
| currentHandyKey = data.handy_key; |
| currentAIName = data.ai_name || 'BOT'; |
| currentElevenLabsKey = data.elevenlabs_key || ''; |
| |
| document.getElementById('persona-input').value = currentPersona; |
| document.getElementById('ai-name-input').value = currentAIName; |
| document.getElementById('handy-key-input').value = currentHandyKey; |
| document.getElementById('elevenlabs-key-input').value = currentElevenLabsKey; |
| |
| if (data.pfp) { |
| document.getElementById('ai-pfp-preview').src = data.pfp; |
| } |
| |
| if (data.timings) { |
| document.getElementById('auto-min-time').value = data.timings.auto_min; |
| document.getElementById('auto-max-time').value = data.timings.auto_max; |
| document.getElementById('edging-min-time').value = data.timings.edging_min; |
| document.getElementById('edging-max-time').value = data.timings.edging_max; |
| document.getElementById('milking-min-time').value = data.timings.milking_min; |
| document.getElementById('milking-max-time').value = data.timings.milking_max; |
| } |
| |
| showStatus('Settings loaded successfully.'); |
| isSetupComplete = true; |
| return true; |
| } else { |
| showSetupOverlay(); |
| return false; |
| } |
| } catch (error) { |
| showStatus('Failed to load settings.', true); |
| console.error('Settings check error:', error); |
| return false; |
| } |
| } |
| |
| async function pollMessages() { |
| if (isPolling) return; |
| isPolling = true; |
| |
| try { |
| const response = await fetch('/poll_messages'); |
| const data = await response.json(); |
| |
| data.messages.forEach(msg => addChatMessage(msg)); |
| updateMoodDisplay(data.mood); |
| updateEdgingTimer(data.edging_elapsed); |
| |
| |
| const edgeBtn = document.getElementById('edge-signal-btn'); |
| if (data.auto_mode_name === 'edging') { |
| edgeBtn.style.display = 'block'; |
| } else { |
| edgeBtn.style.display = 'none'; |
| } |
| |
| |
| const autoBtn = document.getElementById('auto-mode-btn'); |
| autoBtn.textContent = data.auto_active ? 'Stop Auto' : 'Auto Mode'; |
| |
| } catch (error) { |
| console.warn('Polling error:', error); |
| } finally { |
| isPolling = false; |
| } |
| } |
| |
| |
| function setupEventHandlers() { |
| |
| function hideSplashScreen() { |
| if (!document.getElementById('splash-screen').classList.contains('hidden')) { |
| document.getElementById('splash-screen').classList.add('hidden'); |
| setTimeout(() => checkSettings(), 1000); |
| } |
| } |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter') { |
| hideSplashScreen(); |
| } |
| }); |
| |
| |
| document.getElementById('splash-screen').addEventListener('click', hideSplashScreen); |
| document.getElementById('splash-screen').addEventListener('touchstart', hideSplashScreen); |
| |
| |
| document.getElementById('user-chat-input').addEventListener('keydown', (e) => { |
| if (e.key === 'Enter') { |
| sendChatMessage(); |
| } |
| }); |
| |
| document.getElementById('send-chat-btn').addEventListener('click', sendChatMessage); |
| |
| |
| document.getElementById('toggle-sidebar-btn').addEventListener('click', () => { |
| sidebarCollapsed = !sidebarCollapsed; |
| document.body.classList.toggle('sidebar-collapsed', sidebarCollapsed); |
| }); |
| |
| |
| document.getElementById('set-ai-name-btn').addEventListener('click', async () => { |
| const name = document.getElementById('ai-name-input').value.trim() || 'BOT'; |
| try { |
| const response = await fetch('/set_ai_name', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ name }) |
| }); |
| const data = await response.json(); |
| |
| if (data.status === 'special_persona_activated') { |
| currentAIName = data.persona; |
| addChatMessage(data.message, 'bot', false); |
| showStatus(`Special persona activated: ${data.persona}`); |
| } else { |
| currentAIName = name; |
| showStatus('AI name updated successfully.'); |
| } |
| } catch (error) { |
| showStatus('Failed to set AI name.', true); |
| } |
| }); |
| |
| |
| document.getElementById('set-persona-btn').addEventListener('click', () => { |
| currentPersona = document.getElementById('persona-input').value.trim(); |
| if (currentPersona) { |
| showStatus('Persona updated successfully.'); |
| } |
| }); |
| |
| |
| document.getElementById('pfp-upload').addEventListener('change', (e) => { |
| const file = e.target.files[0]; |
| if (file) { |
| const reader = new FileReader(); |
| reader.onload = async (event) => { |
| const base64 = event.target.result; |
| document.getElementById('ai-pfp-preview').src = base64; |
| |
| try { |
| await fetch('/set_profile_picture', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ pfp_b64: base64 }) |
| }); |
| showStatus('Profile picture updated.'); |
| } catch (error) { |
| showStatus('Failed to save profile picture.', true); |
| } |
| }; |
| reader.readAsDataURL(file); |
| } |
| }); |
| |
| |
| document.getElementById('set-handy-key-btn').addEventListener('click', async () => { |
| const key = document.getElementById('handy-key-input').value.trim(); |
| if (!key) return; |
| |
| try { |
| await fetch('/set_handy_key', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ key }) |
| }); |
| currentHandyKey = key; |
| showStatus('Hardware key saved successfully.'); |
| } catch (error) { |
| showStatus('Failed to save hardware key.', true); |
| } |
| }); |
| |
| |
| document.getElementById('set-elevenlabs-key-button').addEventListener('click', async () => { |
| const key = document.getElementById('elevenlabs-key-input').value.trim(); |
| if (!key) return; |
| |
| try { |
| const response = await fetch('/setup_elevenlabs', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ api_key: key }) |
| }); |
| |
| const data = await response.json(); |
| if (data.status === 'success') { |
| currentElevenLabsKey = key; |
| populateVoiceDropdown(data.voices); |
| showStatus('ElevenLabs key set successfully.'); |
| } else { |
| showStatus('Invalid ElevenLabs key.', true); |
| } |
| } catch (error) { |
| showStatus('Failed to set ElevenLabs key.', true); |
| } |
| }); |
| |
| document.getElementById('elevenlabs-enabled-checkbox').addEventListener('change', async (e) => { |
| const voiceId = document.getElementById('elevenlabs-voice-select-box').value; |
| audioEnabled = e.target.checked; |
| |
| try { |
| await fetch('/setup_elevenlabs', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| voice_id: voiceId, |
| enabled: audioEnabled |
| }) |
| }); |
| showStatus(`Voice ${audioEnabled ? 'enabled' : 'disabled'}.`); |
| } catch (error) { |
| showStatus('Failed to update voice settings.', true); |
| } |
| }); |
| |
| |
| document.getElementById('save-timings-btn').addEventListener('click', async () => { |
| const timings = { |
| auto_min_time: parseFloat(document.getElementById('auto-min-time').value), |
| auto_max_time: parseFloat(document.getElementById('auto-max-time').value), |
| edging_min_time: parseFloat(document.getElementById('edging-min-time').value), |
| edging_max_time: parseFloat(document.getElementById('edging-max-time').value), |
| milking_min_time: parseFloat(document.getElementById('milking-min-time').value), |
| milking_max_time: parseFloat(document.getElementById('milking-max-time').value) |
| }; |
| |
| try { |
| await fetch('/save_timings', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(timings) |
| }); |
| showStatus('Timings saved successfully.'); |
| } catch (error) { |
| showStatus('Failed to save timings.', true); |
| } |
| }); |
| |
| |
| document.getElementById('auto-mode-btn').addEventListener('click', () => { |
| handleModeAction('auto'); |
| }); |
| |
| document.getElementById('edging-mode-btn').addEventListener('click', () => { |
| handleModeAction('edging'); |
| }); |
| |
| document.getElementById('milking-mode-btn').addEventListener('click', () => { |
| handleModeAction('milking'); |
| }); |
| |
| document.getElementById('stop-all-btn').addEventListener('click', () => { |
| handleModeAction('stop'); |
| }); |
| |
| document.getElementById('edge-signal-btn').addEventListener('click', async () => { |
| try { |
| await fetch('/signal_edge', { method: 'POST' }); |
| showStatus('Edge signal sent.'); |
| } catch (error) { |
| showStatus('Failed to send edge signal.', true); |
| } |
| }); |
| |
| |
| document.getElementById('setup-done-btn').addEventListener('click', async () => { |
| const handyKey = document.getElementById('setup-handy-key').value.trim(); |
| const persona = document.getElementById('setup-persona').value.trim(); |
| |
| if (!handyKey) { |
| alert('Please enter your hardware API key.'); |
| return; |
| } |
| |
| try { |
| await fetch('/set_handy_key', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ key: handyKey }) |
| }); |
| |
| currentHandyKey = handyKey; |
| currentPersona = persona; |
| document.getElementById('setup-overlay').style.display = 'none'; |
| |
| |
| document.getElementById('handy-key-input').value = handyKey; |
| document.getElementById('persona-input').value = persona; |
| |
| isSetupComplete = true; |
| showStatus('Setup complete! You can now chat with your AI.'); |
| addChatMessage('Hello! I\'m ready to chat. How can I make you feel good today?'); |
| |
| } catch (error) { |
| alert('Failed to save settings. Please try again.'); |
| } |
| }); |
| } |
| |
| function sendChatMessage() { |
| const input = document.getElementById('user-chat-input'); |
| const message = input.value.trim(); |
| |
| if (!message) return; |
| if (!isSetupComplete) { |
| showStatus('Please complete setup first.', true); |
| return; |
| } |
| |
| addChatMessage(message, 'user'); |
| input.value = ''; |
| sendMessage(message); |
| } |
| |
| function populateVoiceDropdown(voices) { |
| const select = document.getElementById('elevenlabs-voice-select-box'); |
| select.innerHTML = '<option value="">Select a voice...</option>'; |
| |
| Object.entries(voices).forEach(([name, id]) => { |
| const option = document.createElement('option'); |
| option.value = id; |
| option.textContent = name; |
| select.appendChild(option); |
| }); |
| |
| select.disabled = false; |
| document.getElementById('elevenlabs-enabled-checkbox').disabled = false; |
| } |
| |
| function showSetupOverlay() { |
| document.getElementById('setup-overlay').style.display = 'flex'; |
| } |
| |
| async function handleModeAction(action) { |
| try { |
| const usePatterns = document.getElementById('pattern-mode-checkbox').checked; |
| const response = await fetch('/sidebar_mode_action', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ action, use_patterns: usePatterns }) |
| }); |
| |
| const data = await response.json(); |
| showStatus(`Action: ${data.status}`); |
| |
| } catch (error) { |
| showStatus('Failed to execute mode action.', true); |
| } |
| } |
| |
| |
| function setupBrowserControls() { |
| document.getElementById('browser-go').addEventListener('click', () => { |
| const url = document.getElementById('browser-url').value.trim(); |
| if (url) { |
| let fullUrl = url; |
| if (!url.startsWith('http://') && !url.startsWith('https://')) { |
| fullUrl = 'https://' + url; |
| } |
| document.getElementById('web-browser').src = fullUrl; |
| } |
| }); |
| |
| document.getElementById('browser-home').addEventListener('click', () => { |
| document.getElementById('web-browser').src = 'about:blank'; |
| document.getElementById('browser-url').value = ''; |
| }); |
| |
| document.getElementById('browser-refresh').addEventListener('click', () => { |
| const iframe = document.getElementById('web-browser'); |
| iframe.src = iframe.src; |
| }); |
| |
| document.getElementById('browser-url').addEventListener('keydown', (e) => { |
| if (e.key === 'Enter') { |
| document.getElementById('browser-go').click(); |
| } |
| }); |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| setupEventHandlers(); |
| setupBrowserControls(); |
| |
| |
| setInterval(pollMessages, 1000); |
| }); |
| |
| |
| function setupVisualizer() { |
| const canvas = document.getElementById('rhythm-canvas'); |
| const ctx = canvas.getContext('2d'); |
| |
| function resizeCanvas() { |
| canvas.width = canvas.offsetWidth; |
| canvas.height = canvas.offsetHeight; |
| } |
| |
| function animate() { |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| |
| const time = Date.now() * 0.002; |
| const pulse = (Math.sin(time) + 1) * 0.5; |
| |
| ctx.fillStyle = `rgba(139, 233, 253, ${pulse * 0.3})`; |
| ctx.fillRect(0, 0, canvas.width, canvas.height); |
| |
| requestAnimationFrame(animate); |
| } |
| |
| resizeCanvas(); |
| window.addEventListener('resize', resizeCanvas); |
| animate(); |
| } |
| |
| setTimeout(setupVisualizer, 1000); |
| </script> |
| </body> |
| </html> |
|
|