Download static/script.js from sinhal/SageCam: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/spaces/sinhal/SageCam/resolve/main/static/script.js
- Command line
-
hf download hf://spaces/sinhal/SageCam/static/script.js
-
curl -L -o script.js https://huggingface.co/spaces/sinhal/SageCam/resolve/main/static/script.js
12.5 kB
| 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 = ` | |
| <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; | |
| } | |
| // ββ 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 = '<strong>Really?</strong><span class="sub-text">Sure</span>'; | |
| } 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 = '<strong>Next</strong><span class="sub-text">Esc</span>'; | |
| 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'); | |
| }); | |
| } | |
| }); | |