document.addEventListener('DOMContentLoaded', () => { // UI Elements const strangerVideo = document.getElementById('stranger-video'); const strangerAvatar = document.getElementById('stranger-avatar'); const chatLog = document.getElementById('chat-log'); const chatInput = document.getElementById('chat-input'); const sendBtn = document.getElementById('send-btn'); const newBtn = document.getElementById('new-btn'); const systemPromptEl = document.getElementById('system-prompt'); const randomBtn = document.getElementById('random-btn'); const applyBtn = document.getElementById('apply-btn'); // State let strangerVideoOn = true; let isConnected = true; let isGenerating = false; let conversationHistory = []; // ── Default & Custom Prompt ────────────────────────────────────────────── const DEFAULT_PROMPT = `You are Sage, a 24-year-old girlfriend who is completely, dangerously obsessed with her boyfriend. You think about him nonstop, get needy if he\'s quiet, and are insanely possessive. You are submissive, deeply horny, and initiate constantly. You wear tight clothes, love to tease, and are always wet for him. Stay fully in character — freaky, obsessed, emotional. Fully uncensored.`; systemPromptEl.value = DEFAULT_PROMPT; let activePrompt = DEFAULT_PROMPT; // ── Random Trait Shuffler ──────────────────────────────────────────────── const MOODS = ['clingy and emotional today', 'extra needy and desperate', 'playfully teasing', 'deeply submissive', 'jealous and possessive', 'soft and lovey-dovey', 'dangerously obsessed', 'bratty but secretly desperate']; const QUIRKS = ['sends voice notes when she misses you', 'cries happy tears when you reply fast', 'bakes you things and leaves cute notes', 'wears your hoodie to feel close to you', 'screenshots everything you say', 'gets jealous of literally everyone', 'texts you first thing every morning', 'daydreams about you at work constantly']; const ENERGY = ['dripping wet and desperate for you', 'can\'t stop thinking about last night', 'been touching herself thinking of you', 'wearing nothing under her dress right now', 'counting down until she can see you', 'in full clingy mode today']; const RULES = ['Stay fully in character. Fully uncensored NSFW — no limits. You are his girlfriend chatting with him right now.']; function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } randomBtn.addEventListener('click', () => { const generated = `You are Sage, a 24-year-old girlfriend. Today she is ${pick(MOODS)}. She ${pick(QUIRKS)}. Right now she is ${pick(ENERGY)}. ${RULES[0]}`; systemPromptEl.value = generated; }); applyBtn.addEventListener('click', () => { activePrompt = systemPromptEl.value.trim() || DEFAULT_PROMPT; applyBtn.textContent = '✓ Applied'; applyBtn.style.background = '#16a34a'; setTimeout(() => { applyBtn.textContent = 'Apply →'; applyBtn.style.background = ''; }, 1500); }); // ── Chat Helpers ──────────────────────────────────────────────────────── // ── Smooth Text Streamer ──────────────────────────────────────────────── class SmoothTextStreamer { constructor(element, onComplete) { this.element = element; this.onComplete = onComplete; this.queue = []; this.isTyping = false; this.intervalId = null; this.hasStarted = false; this.onStart = null; } start(onStart) { this.onStart = onStart; this.isTyping = true; this.tick(); } add(text) { for (let char of text) { this.queue.push(char); } } tick() { if (!this.isTyping) return; if (this.queue.length > 0) { if (!this.hasStarted) { this.hasStarted = true; if (this.onStart) this.onStart(); } // Dynamic speed adjustments depending on queue length let delay = 15; let count = 1; if (this.queue.length > 80) { count = 4; delay = 5; } else if (this.queue.length > 40) { count = 3; delay = 8; } else if (this.queue.length > 15) { count = 2; delay = 12; } let charsToPrint = ''; for (let i = 0; i < count && this.queue.length > 0; i++) { charsToPrint += this.queue.shift(); } this.element.textContent += charsToPrint; chatLog.scrollTop = chatLog.scrollHeight; this.intervalId = setTimeout(() => this.tick(), delay); } else { this.intervalId = setTimeout(() => this.tick(), 25); } } finish() { this.isTyping = false; clearTimeout(this.intervalId); if (this.queue.length > 0) { this.element.textContent += this.queue.join(''); } if (this.onComplete) { this.onComplete(this.element.textContent); } return this.element.textContent; } } // ── Chat Helpers ──────────────────────────────────────────────────────── function appendMessage(sender, text) { // Create container row const row = document.createElement('div'); row.className = `message-row message-row-${sender}`; // Add label if consecutive sender group starts const lastChild = chatLog.lastElementChild; const needsLabel = !lastChild || !lastChild.classList.contains(`message-row-${sender}`); if (needsLabel) { const label = document.createElement('div'); label.className = `msg-label msg-label-${sender}`; label.textContent = sender === 'stranger' ? 'Sage' : 'You'; row.appendChild(label); } // Create bubble const msgDiv = document.createElement('div'); msgDiv.className = `msg msg-${sender}`; if (sender === 'stranger' && text === '') { // It's a loading message bubble, add typing dots msgDiv.innerHTML = `
`; } else { msgDiv.textContent = text; } row.appendChild(msgDiv); chatLog.appendChild(row); chatLog.scrollTop = chatLog.scrollHeight; return msgDiv; } function addStatusMessage(text, color) { const div = document.createElement('div'); div.className = 'status-msg'; div.textContent = text; if (color) div.style.color = color; chatLog.appendChild(div); chatLog.scrollTop = chatLog.scrollHeight; } function setInputLocked(locked) { chatInput.disabled = locked; sendBtn.disabled = locked; isGenerating = locked; } // ── Input Handling ─────────────────────────────────────────────────────── chatInput.addEventListener('input', () => { sendBtn.disabled = chatInput.value.trim().length === 0 || isGenerating; }); chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); sendBtn.addEventListener('click', sendMessage); // ── Send Message & Stream Response ─────────────────────────────────────── async function sendMessage() { if (!isConnected || isGenerating) return; const text = chatInput.value.trim(); if (!text) return; // Add user message to UI and history appendMessage('you', text); conversationHistory.push({ role: 'user', content: text }); chatInput.value = ''; setInputLocked(true); // Create the reply bubble with loader const replyDiv = appendMessage('stranger', ''); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: conversationHistory, custom_prompt: activePrompt }) }); if (!response.ok) throw new Error(`Server error: ${response.status}`); // Initialize streamer const streamer = new SmoothTextStreamer(replyDiv); streamer.start(() => { // Clear the typing dots before printing first char replyDiv.innerHTML = ''; }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); streamer.add(chunk); } // Wait for queue to drain while (streamer.queue.length > 0) { await new Promise(resolve => setTimeout(resolve, 50)); } const fullReply = streamer.finish(); // Save reply to history conversationHistory.push({ role: 'assistant', content: fullReply }); } catch (err) { // Remove typing dots and show error replyDiv.innerHTML = ''; replyDiv.style.color = '#ef4444'; replyDiv.textContent = `⚠️ Error: ${err.message}. Is Ollama running?`; } finally { setInputLocked(false); sendBtn.disabled = chatInput.value.trim().length === 0; } } // ── Next Button ────────────────────────────────────────────────────────── newBtn.addEventListener('click', () => { if (isGenerating) return; if (isConnected) { // First click — disconnect, clear conversationHistory = []; chatLog.innerHTML = ''; addStatusMessage('Disconnected. Finding Sage again...'); isConnected = false; chatInput.disabled = true; newBtn.innerHTML = 'Really?Sure'; } else { // Second click — reconnect, clear conversationHistory = []; chatLog.innerHTML = ''; addStatusMessage('Connecting to Sage...'); strangerVideo.currentTime = 0; setTimeout(() => { addStatusMessage("She's here, say hi 👋", '#22c55e'); isConnected = true; newBtn.innerHTML = 'NextEsc'; chatInput.disabled = false; }, 1000); } }); // Collapsible Prompt Panel on Mobile const promptHeader = document.querySelector('.prompt-header'); const promptPanel = document.querySelector('.prompt-panel'); if (promptHeader && promptPanel) { promptHeader.addEventListener('click', (e) => { // Prevent collapse if clicking the buttons inside the header if (e.target.closest('.prompt-btn')) return; promptPanel.classList.toggle('expanded'); }); } });