ddeeds / index.html
sudotheworld's picture
Upload 56 files
70a50c3 verified
<!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;
}
/* Mobile-first responsive design */
@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; /* Prevents zoom on iOS */
}
.setting-section {
padding: 15px;
}
.input-text, .select-box {
font-size: 16px; /* Prevents zoom on iOS */
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">&laquo;</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>
<!-- Setup Overlay -->
<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>
// ─── GLOBAL STATE ─────────────────────────────────────────────────────────────────
let currentAIName = "BOT";
let currentPersona = "";
let currentHandyKey = "";
let currentElevenLabsKey = "";
let audioEnabled = false;
let sidebarCollapsed = false;
let isSetupComplete = false;
let isPolling = false;
let pendingAudioRequests = [];
// ─── UTILITY FUNCTIONS ────────────────────────────────────────────────────────────
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) {
// Clean HTML tags from text for audio
const cleanText = text.replace(/<[^>]+>/g, '').trim();
if (!cleanText || cleanText.startsWith('(') || cleanText.startsWith('[')) return;
// Simple rate limiting
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); // Small delay to allow server processing
}
// ─── API FUNCTIONS ────────────────────────────────────────────────────────────────
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);
// Update button states based on mode
const edgeBtn = document.getElementById('edge-signal-btn');
if (data.auto_mode_name === 'edging') {
edgeBtn.style.display = 'block';
} else {
edgeBtn.style.display = 'none';
}
// Update auto mode button text
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;
}
}
// ─── EVENT HANDLERS ───────────────────────────────────────────────────────────────
function setupEventHandlers() {
// Splash screen - handle both keyboard and touch events
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();
}
});
// Add touch/click support for mobile devices
document.getElementById('splash-screen').addEventListener('click', hideSplashScreen);
document.getElementById('splash-screen').addEventListener('touchstart', hideSplashScreen);
// Chat input
document.getElementById('user-chat-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
sendChatMessage();
}
});
document.getElementById('send-chat-btn').addEventListener('click', sendChatMessage);
// Sidebar toggle
document.getElementById('toggle-sidebar-btn').addEventListener('click', () => {
sidebarCollapsed = !sidebarCollapsed;
document.body.classList.toggle('sidebar-collapsed', sidebarCollapsed);
});
// AI Name setting
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);
}
});
// Persona setting
document.getElementById('set-persona-btn').addEventListener('click', () => {
currentPersona = document.getElementById('persona-input').value.trim();
if (currentPersona) {
showStatus('Persona updated successfully.');
}
});
// Profile picture upload
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);
}
});
// Hardware key setting
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);
}
});
// ElevenLabs settings
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);
}
});
// Timing settings
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);
}
});
// Control mode buttons
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);
}
});
// Setup overlay
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';
// Update UI with setup values
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);
}
}
// ─── WEB BROWSER FUNCTIONALITY ───────────────────────────────────────────────────
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();
}
});
}
// ─── INITIALIZATION ───────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
setupEventHandlers();
setupBrowserControls();
// Start polling for messages
setInterval(pollMessages, 1000);
});
// Visual rhythm indicator (simple animation)
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);
// Simple pulsing effect
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>