| document.addEventListener('DOMContentLoaded', () => { |
| |
| 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'); |
|
|
| |
| let strangerVideoOn = true; |
| let isConnected = true; |
| let isGenerating = false; |
| let conversationHistory = []; |
|
|
| |
| 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; |
|
|
| |
| 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); |
| }); |
|
|
|
|
|
|
| |
| |
| 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(); |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| function appendMessage(sender, text) { |
| |
| const row = document.createElement('div'); |
| row.className = `message-row message-row-${sender}`; |
|
|
| |
| 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); |
| } |
|
|
| |
| const msgDiv = document.createElement('div'); |
| msgDiv.className = `msg msg-${sender}`; |
|
|
| if (sender === 'stranger' && text === '') { |
| |
| msgDiv.innerHTML = ` |
| <div class="typing-dots"> |
| <span></span> |
| <span></span> |
| <span></span> |
| </div> |
| `; |
| } 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; |
| } |
|
|
| |
| 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); |
|
|
| |
| async function sendMessage() { |
| if (!isConnected || isGenerating) return; |
|
|
| const text = chatInput.value.trim(); |
| if (!text) return; |
|
|
| |
| appendMessage('you', text); |
| conversationHistory.push({ role: 'user', content: text }); |
| chatInput.value = ''; |
| setInputLocked(true); |
|
|
| |
| 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}`); |
|
|
| |
| const streamer = new SmoothTextStreamer(replyDiv); |
| streamer.start(() => { |
| |
| 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); |
| } |
|
|
| |
| while (streamer.queue.length > 0) { |
| await new Promise(resolve => setTimeout(resolve, 50)); |
| } |
| const fullReply = streamer.finish(); |
|
|
| |
| conversationHistory.push({ role: 'assistant', content: fullReply }); |
|
|
| } catch (err) { |
| |
| replyDiv.innerHTML = ''; |
| replyDiv.style.color = '#ef4444'; |
| replyDiv.textContent = `β οΈ Error: ${err.message}. Is Ollama running?`; |
| } finally { |
| setInputLocked(false); |
| sendBtn.disabled = chatInput.value.trim().length === 0; |
| } |
| } |
|
|
| |
| newBtn.addEventListener('click', () => { |
| if (isGenerating) return; |
|
|
| if (isConnected) { |
| |
| conversationHistory = []; |
| chatLog.innerHTML = ''; |
| addStatusMessage('Disconnected. Finding Sage again...'); |
| isConnected = false; |
| chatInput.disabled = true; |
| newBtn.innerHTML = '<strong>Really?</strong><span class="sub-text">Sure</span>'; |
| } else { |
| |
| conversationHistory = []; |
| chatLog.innerHTML = ''; |
| addStatusMessage('Connecting to Sage...'); |
| strangerVideo.currentTime = 0; |
|
|
| setTimeout(() => { |
| addStatusMessage("She's here, say hi π", '#22c55e'); |
| isConnected = true; |
| newBtn.innerHTML = '<strong>Next</strong><span class="sub-text">Esc</span>'; |
| chatInput.disabled = false; |
| }, 1000); |
| } |
| }); |
|
|
| |
| const promptHeader = document.querySelector('.prompt-header'); |
| const promptPanel = document.querySelector('.prompt-panel'); |
| if (promptHeader && promptPanel) { |
| promptHeader.addEventListener('click', (e) => { |
| |
| if (e.target.closest('.prompt-btn')) return; |
| promptPanel.classList.toggle('expanded'); |
| }); |
| } |
| }); |
|
|