| | <!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> |
| |
|