sinhal commited on
Commit
0fbbe6a
·
verified ·
1 Parent(s): cc0eff9

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. static/index.html +85 -0
  3. static/sage.mp4 +3 -0
  4. static/script.js +286 -0
  5. static/style.css +481 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ static/sage.mp4 filter=lfs diff=lfs merge=lfs -text
static/index.html ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SageCam</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <header class="header">
12
+ <div class="logo">Sage<span>Cam</span></div>
13
+ <div class="tagline">Obsessed Girlfriend</div>
14
+ <div class="stats">
15
+ <div class="stats-dot"></div>
16
+ Model Connected
17
+ </div>
18
+ </header>
19
+
20
+ <div class="main-container">
21
+ <!-- LEFT: Video + Prompt Panel -->
22
+ <div class="video-section">
23
+
24
+ <!-- Stranger Video (top ~62%) -->
25
+ <div class="video-box stranger-box">
26
+ <video id="stranger-video" class="video-element" autoplay loop muted playsinline>
27
+ <source src="sage.mp4" type="video/mp4">
28
+ </video>
29
+ <div id="stranger-avatar" class="avatar-placeholder" style="display: none;">
30
+ <div class="avatar-icon">
31
+ <svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
32
+ <path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/>
33
+ </svg>
34
+ </div>
35
+ <div class="avatar-text">Camera is off</div>
36
+ </div>
37
+
38
+ </div>
39
+
40
+ <!-- Curate Sage Panel (bottom ~38%) -->
41
+ <div class="prompt-panel">
42
+ <div class="prompt-header">
43
+ <span class="prompt-title">✦ Curate Sage</span>
44
+ <div class="prompt-actions">
45
+ <button id="random-btn" class="prompt-btn random-btn">
46
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
47
+ <path d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/>
48
+ </svg>
49
+ Shuffle
50
+ </button>
51
+ <button id="apply-btn" class="prompt-btn apply-btn">Apply →</button>
52
+ </div>
53
+ </div>
54
+ <textarea id="system-prompt" class="prompt-textarea" spellcheck="false" placeholder="Describe Sage's personality, mood, and quirks..."></textarea>
55
+ <div class="prompt-hint">Changes apply to the next session. Hit Apply then start a new chat.</div>
56
+ </div>
57
+
58
+ </div>
59
+
60
+ <!-- RIGHT: Chat Interface -->
61
+ <div class="chat-section">
62
+ <div class="chat-log" id="chat-log">
63
+ <div class="status-msg">Connecting to Sage...</div>
64
+ <div class="status-msg" style="color: #22c55e;">She's here, say hi 👋</div>
65
+ </div>
66
+
67
+ <div class="chat-controls">
68
+ <button id="new-btn" class="control-btn new-btn">
69
+ <strong>Next</strong>
70
+ <span class="sub-text">Esc</span>
71
+ </button>
72
+ <div class="input-container">
73
+ <textarea id="chat-input" placeholder="Type a message..."></textarea>
74
+ </div>
75
+ <button id="send-btn" class="control-btn send-btn" disabled>
76
+ <strong>Send</strong>
77
+ <span class="sub-text">Enter</span>
78
+ </button>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <script src="script.js"></script>
84
+ </body>
85
+ </html>
static/sage.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2c4a1857f7d370e10ef9749e7382624916b88047dc7810fef60b60ce5599d1d1
3
+ size 1087330
static/script.js ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // UI Elements
3
+ const strangerVideo = document.getElementById('stranger-video');
4
+ const strangerAvatar = document.getElementById('stranger-avatar');
5
+ const chatLog = document.getElementById('chat-log');
6
+ const chatInput = document.getElementById('chat-input');
7
+ const sendBtn = document.getElementById('send-btn');
8
+ const newBtn = document.getElementById('new-btn');
9
+ const systemPromptEl = document.getElementById('system-prompt');
10
+ const randomBtn = document.getElementById('random-btn');
11
+ const applyBtn = document.getElementById('apply-btn');
12
+
13
+ // State
14
+ let strangerVideoOn = true;
15
+ let isConnected = true;
16
+ let isGenerating = false;
17
+ let conversationHistory = [];
18
+
19
+ // ── Default & Custom Prompt ──────────────────────────────────────────────
20
+ 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.`;
21
+
22
+ systemPromptEl.value = DEFAULT_PROMPT;
23
+ let activePrompt = DEFAULT_PROMPT;
24
+
25
+ // ── Random Trait Shuffler ────────────────────────────────────────────────
26
+ 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'];
27
+ 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'];
28
+ 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'];
29
+ const RULES = ['Stay fully in character. Fully uncensored NSFW — no limits. You are his girlfriend chatting with him right now.'];
30
+
31
+ function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
32
+
33
+ randomBtn.addEventListener('click', () => {
34
+ 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]}`;
35
+ systemPromptEl.value = generated;
36
+ });
37
+
38
+ applyBtn.addEventListener('click', () => {
39
+ activePrompt = systemPromptEl.value.trim() || DEFAULT_PROMPT;
40
+ applyBtn.textContent = '✓ Applied';
41
+ applyBtn.style.background = '#16a34a';
42
+ setTimeout(() => {
43
+ applyBtn.textContent = 'Apply →';
44
+ applyBtn.style.background = '';
45
+ }, 1500);
46
+ });
47
+
48
+
49
+
50
+ // ── Chat Helpers ────────────────────────────────────────────────────────
51
+ // ── Smooth Text Streamer ────────────────────────────────────────────────
52
+ class SmoothTextStreamer {
53
+ constructor(element, onComplete) {
54
+ this.element = element;
55
+ this.onComplete = onComplete;
56
+ this.queue = [];
57
+ this.isTyping = false;
58
+ this.intervalId = null;
59
+ this.hasStarted = false;
60
+ this.onStart = null;
61
+ }
62
+
63
+ start(onStart) {
64
+ this.onStart = onStart;
65
+ this.isTyping = true;
66
+ this.tick();
67
+ }
68
+
69
+ add(text) {
70
+ for (let char of text) {
71
+ this.queue.push(char);
72
+ }
73
+ }
74
+
75
+ tick() {
76
+ if (!this.isTyping) return;
77
+
78
+ if (this.queue.length > 0) {
79
+ if (!this.hasStarted) {
80
+ this.hasStarted = true;
81
+ if (this.onStart) this.onStart();
82
+ }
83
+
84
+ // Dynamic speed adjustments depending on queue length
85
+ let delay = 15;
86
+ let count = 1;
87
+
88
+ if (this.queue.length > 80) {
89
+ count = 4;
90
+ delay = 5;
91
+ } else if (this.queue.length > 40) {
92
+ count = 3;
93
+ delay = 8;
94
+ } else if (this.queue.length > 15) {
95
+ count = 2;
96
+ delay = 12;
97
+ }
98
+
99
+ let charsToPrint = '';
100
+ for (let i = 0; i < count && this.queue.length > 0; i++) {
101
+ charsToPrint += this.queue.shift();
102
+ }
103
+
104
+ this.element.textContent += charsToPrint;
105
+
106
+ chatLog.scrollTop = chatLog.scrollHeight;
107
+ this.intervalId = setTimeout(() => this.tick(), delay);
108
+ } else {
109
+ this.intervalId = setTimeout(() => this.tick(), 25);
110
+ }
111
+ }
112
+
113
+ finish() {
114
+ this.isTyping = false;
115
+ clearTimeout(this.intervalId);
116
+ if (this.queue.length > 0) {
117
+ this.element.textContent += this.queue.join('');
118
+ }
119
+ if (this.onComplete) {
120
+ this.onComplete(this.element.textContent);
121
+ }
122
+ return this.element.textContent;
123
+ }
124
+ }
125
+
126
+ // ── Chat Helpers ────────────────────────────────────────────────────────
127
+ function appendMessage(sender, text) {
128
+ // Create container row
129
+ const row = document.createElement('div');
130
+ row.className = `message-row message-row-${sender}`;
131
+
132
+ // Add label if consecutive sender group starts
133
+ const lastChild = chatLog.lastElementChild;
134
+ const needsLabel = !lastChild || !lastChild.classList.contains(`message-row-${sender}`);
135
+ if (needsLabel) {
136
+ const label = document.createElement('div');
137
+ label.className = `msg-label msg-label-${sender}`;
138
+ label.textContent = sender === 'stranger' ? 'Sage' : 'You';
139
+ row.appendChild(label);
140
+ }
141
+
142
+ // Create bubble
143
+ const msgDiv = document.createElement('div');
144
+ msgDiv.className = `msg msg-${sender}`;
145
+
146
+ if (sender === 'stranger' && text === '') {
147
+ // It's a loading message bubble, add typing dots
148
+ msgDiv.innerHTML = `
149
+ <div class="typing-dots">
150
+ <span></span>
151
+ <span></span>
152
+ <span></span>
153
+ </div>
154
+ `;
155
+ } else {
156
+ msgDiv.textContent = text;
157
+ }
158
+ row.appendChild(msgDiv);
159
+
160
+ chatLog.appendChild(row);
161
+ chatLog.scrollTop = chatLog.scrollHeight;
162
+ return msgDiv;
163
+ }
164
+
165
+ function addStatusMessage(text, color) {
166
+ const div = document.createElement('div');
167
+ div.className = 'status-msg';
168
+ div.textContent = text;
169
+ if (color) div.style.color = color;
170
+ chatLog.appendChild(div);
171
+ chatLog.scrollTop = chatLog.scrollHeight;
172
+ }
173
+
174
+ function setInputLocked(locked) {
175
+ chatInput.disabled = locked;
176
+ sendBtn.disabled = locked;
177
+ isGenerating = locked;
178
+ }
179
+
180
+ // ── Input Handling ───────────────────────────────────────────────────────
181
+ chatInput.addEventListener('input', () => {
182
+ sendBtn.disabled = chatInput.value.trim().length === 0 || isGenerating;
183
+ });
184
+
185
+ chatInput.addEventListener('keypress', (e) => {
186
+ if (e.key === 'Enter' && !e.shiftKey) {
187
+ e.preventDefault();
188
+ sendMessage();
189
+ }
190
+ });
191
+
192
+ sendBtn.addEventListener('click', sendMessage);
193
+
194
+ // ── Send Message & Stream Response ───────────────────────────────────────
195
+ async function sendMessage() {
196
+ if (!isConnected || isGenerating) return;
197
+
198
+ const text = chatInput.value.trim();
199
+ if (!text) return;
200
+
201
+ // Add user message to UI and history
202
+ appendMessage('you', text);
203
+ conversationHistory.push({ role: 'user', content: text });
204
+ chatInput.value = '';
205
+ setInputLocked(true);
206
+
207
+ // Create the reply bubble with loader
208
+ const replyDiv = appendMessage('stranger', '');
209
+
210
+ try {
211
+ const response = await fetch('/api/chat', {
212
+ method: 'POST',
213
+ headers: { 'Content-Type': 'application/json' },
214
+ body: JSON.stringify({
215
+ messages: conversationHistory,
216
+ custom_prompt: activePrompt
217
+ })
218
+ });
219
+
220
+ if (!response.ok) throw new Error(`Server error: ${response.status}`);
221
+
222
+ // Initialize streamer
223
+ const streamer = new SmoothTextStreamer(replyDiv);
224
+ streamer.start(() => {
225
+ // Clear the typing dots before printing first char
226
+ replyDiv.innerHTML = '';
227
+ });
228
+
229
+ const reader = response.body.getReader();
230
+ const decoder = new TextDecoder();
231
+
232
+ while (true) {
233
+ const { done, value } = await reader.read();
234
+ if (done) break;
235
+ const chunk = decoder.decode(value, { stream: true });
236
+ streamer.add(chunk);
237
+ }
238
+
239
+ // Wait for queue to drain
240
+ while (streamer.queue.length > 0) {
241
+ await new Promise(resolve => setTimeout(resolve, 50));
242
+ }
243
+ const fullReply = streamer.finish();
244
+
245
+ // Save reply to history
246
+ conversationHistory.push({ role: 'assistant', content: fullReply });
247
+
248
+ } catch (err) {
249
+ // Remove typing dots and show error
250
+ replyDiv.innerHTML = '';
251
+ replyDiv.style.color = '#ef4444';
252
+ replyDiv.textContent = `⚠️ Error: ${err.message}. Is Ollama running?`;
253
+ } finally {
254
+ setInputLocked(false);
255
+ sendBtn.disabled = chatInput.value.trim().length === 0;
256
+ }
257
+ }
258
+
259
+ // ── Next Button ──────────────────────────────────────────────────────────
260
+ newBtn.addEventListener('click', () => {
261
+ if (isGenerating) return;
262
+
263
+ if (isConnected) {
264
+ // First click — disconnect, clear
265
+ conversationHistory = [];
266
+ chatLog.innerHTML = '';
267
+ addStatusMessage('Disconnected. Finding Sage again...');
268
+ isConnected = false;
269
+ chatInput.disabled = true;
270
+ newBtn.innerHTML = '<strong>Really?</strong><span class="sub-text">Sure</span>';
271
+ } else {
272
+ // Second click — reconnect, clear
273
+ conversationHistory = [];
274
+ chatLog.innerHTML = '';
275
+ addStatusMessage('Connecting to Sage...');
276
+ strangerVideo.currentTime = 0;
277
+
278
+ setTimeout(() => {
279
+ addStatusMessage("She's here, say hi 👋", '#22c55e');
280
+ isConnected = true;
281
+ newBtn.innerHTML = '<strong>Next</strong><span class="sub-text">Esc</span>';
282
+ chatInput.disabled = false;
283
+ }, 1000);
284
+ }
285
+ });
286
+ });
static/style.css ADDED
@@ -0,0 +1,481 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ box-sizing: border-box;
3
+ margin: 0;
4
+ padding: 0;
5
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
6
+ }
7
+
8
+ body {
9
+ background: #111;
10
+ color: #e5e5e5;
11
+ height: 100vh;
12
+ display: flex;
13
+ flex-direction: column;
14
+ overflow: hidden;
15
+ }
16
+
17
+ /* Header */
18
+ .header {
19
+ display: flex;
20
+ align-items: center;
21
+ padding: 0 24px;
22
+ height: 56px;
23
+ background: #1a1a1a;
24
+ border-bottom: 1px solid #2a2a2a;
25
+ flex-shrink: 0;
26
+ }
27
+
28
+ .logo {
29
+ font-size: 18px;
30
+ font-weight: 700;
31
+ color: #fff;
32
+ letter-spacing: -0.5px;
33
+ }
34
+
35
+ .logo span {
36
+ color: #0ea5e9;
37
+ }
38
+
39
+ .tagline {
40
+ font-size: 13px;
41
+ color: #666;
42
+ margin-left: 16px;
43
+ padding-left: 16px;
44
+ border-left: 1px solid #2a2a2a;
45
+ }
46
+
47
+ .stats {
48
+ margin-left: auto;
49
+ font-size: 13px;
50
+ color: #aaa;
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 7px;
54
+ }
55
+
56
+ .stats-dot {
57
+ width: 7px;
58
+ height: 7px;
59
+ background: #22c55e;
60
+ border-radius: 50%;
61
+ }
62
+
63
+ /* Main Layout */
64
+ .main-container {
65
+ display: flex;
66
+ flex: 1;
67
+ overflow: hidden;
68
+ padding: 20px;
69
+ gap: 20px;
70
+ }
71
+
72
+ /* Left Video Section */
73
+ .video-section {
74
+ width: 420px;
75
+ flex-shrink: 0;
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 12px;
79
+ }
80
+
81
+ .video-box {
82
+ flex: 0 0 62%;
83
+ background: #000;
84
+ border-radius: 10px;
85
+ overflow: hidden;
86
+ position: relative;
87
+ border: 1px solid #2a2a2a;
88
+ }
89
+
90
+ /* Curate Sage Prompt Panel */
91
+ .prompt-panel {
92
+ flex: 1;
93
+ background: #1a1a1a;
94
+ border: 1px solid #2a2a2a;
95
+ border-radius: 10px;
96
+ display: flex;
97
+ flex-direction: column;
98
+ overflow: hidden;
99
+ }
100
+
101
+ .prompt-header {
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: space-between;
105
+ padding: 10px 14px;
106
+ border-bottom: 1px solid #2a2a2a;
107
+ flex-shrink: 0;
108
+ }
109
+
110
+ .prompt-title {
111
+ font-size: 12px;
112
+ font-weight: 600;
113
+ color: #aaa;
114
+ letter-spacing: 0.3px;
115
+ }
116
+
117
+ .prompt-actions {
118
+ display: flex;
119
+ gap: 6px;
120
+ }
121
+
122
+ .prompt-btn {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 5px;
126
+ padding: 5px 10px;
127
+ border-radius: 6px;
128
+ font-size: 12px;
129
+ font-weight: 500;
130
+ cursor: pointer;
131
+ border: 1px solid #333;
132
+ transition: all 0.15s;
133
+ }
134
+
135
+ .random-btn {
136
+ background: #222;
137
+ color: #aaa;
138
+ }
139
+ .random-btn:hover {
140
+ background: #2a2a2a;
141
+ color: #fff;
142
+ }
143
+
144
+ .apply-btn {
145
+ background: #0ea5e9;
146
+ color: #fff;
147
+ border-color: #0ea5e9;
148
+ }
149
+ .apply-btn:hover {
150
+ background: #0284c7;
151
+ }
152
+
153
+ .prompt-textarea {
154
+ flex: 1;
155
+ background: transparent;
156
+ border: none;
157
+ resize: none;
158
+ padding: 12px 14px;
159
+ font-size: 12.5px;
160
+ color: #ccc;
161
+ outline: none;
162
+ line-height: 1.6;
163
+ font-family: 'Inter', sans-serif;
164
+ }
165
+ .prompt-textarea::placeholder {
166
+ color: #3a3a3a;
167
+ }
168
+
169
+ .prompt-hint {
170
+ font-size: 10.5px;
171
+ color: #3a3a3a;
172
+ padding: 6px 14px;
173
+ border-top: 1px solid #222;
174
+ flex-shrink: 0;
175
+ }
176
+
177
+ .video-element {
178
+ width: 100%;
179
+ height: 100%;
180
+ object-fit: cover;
181
+ display: block;
182
+ }
183
+
184
+ .avatar-placeholder {
185
+ width: 100%;
186
+ height: 100%;
187
+ background: #1a1a1a;
188
+ display: flex;
189
+ flex-direction: column;
190
+ justify-content: center;
191
+ align-items: center;
192
+ gap: 12px;
193
+ }
194
+
195
+ .avatar-placeholder img {
196
+ width: 96px;
197
+ height: 96px;
198
+ border-radius: 50%;
199
+ filter: grayscale(30%);
200
+ border: 2px solid #2a2a2a;
201
+ }
202
+
203
+ .avatar-icon {
204
+ width: 90px;
205
+ height: 90px;
206
+ background: #2a2a2a;
207
+ border-radius: 50%;
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ color: #444;
212
+ }
213
+
214
+ .avatar-icon svg {
215
+ width: 48px;
216
+ height: 48px;
217
+ }
218
+
219
+ .avatar-text {
220
+ color: #555;
221
+ font-size: 13px;
222
+ }
223
+
224
+ .video-controls {
225
+ position: absolute;
226
+ bottom: 12px;
227
+ left: 12px;
228
+ right: 12px;
229
+ display: flex;
230
+ justify-content: space-between;
231
+ align-items: center;
232
+ }
233
+
234
+ .label-badge {
235
+ background: rgba(0, 0, 0, 0.65);
236
+ padding: 4px 10px;
237
+ border-radius: 6px;
238
+ font-size: 12px;
239
+ font-weight: 600;
240
+ color: #ccc;
241
+ }
242
+
243
+ .toggle-btn {
244
+ background: rgba(0, 0, 0, 0.6);
245
+ color: #ccc;
246
+ border: 1px solid rgba(255, 255, 255, 0.1);
247
+ padding: 5px 10px;
248
+ border-radius: 6px;
249
+ cursor: pointer;
250
+ font-size: 12px;
251
+ font-weight: 500;
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 5px;
255
+ transition: background 0.15s;
256
+ }
257
+
258
+ .toggle-btn:hover {
259
+ background: rgba(255, 255, 255, 0.12);
260
+ color: #fff;
261
+ }
262
+
263
+ /* Right Chat Section */
264
+ .chat-section {
265
+ flex: 1;
266
+ display: flex;
267
+ flex-direction: column;
268
+ background: #1a1a1a;
269
+ border: 1px solid #2a2a2a;
270
+ border-radius: 10px;
271
+ overflow: hidden;
272
+ min-width: 0;
273
+ }
274
+
275
+ .chat-log {
276
+ flex: 1;
277
+ padding: 20px;
278
+ overflow-y: auto;
279
+ display: flex;
280
+ flex-direction: column;
281
+ gap: 4px;
282
+ }
283
+
284
+ .chat-log::-webkit-scrollbar { width: 5px; }
285
+ .chat-log::-webkit-scrollbar-track { background: transparent; }
286
+ .chat-log::-webkit-scrollbar-thumb { background: #333; border-radius: 10px; }
287
+
288
+ .status-msg {
289
+ text-align: center;
290
+ color: #555;
291
+ font-size: 12px;
292
+ margin: 6px 0;
293
+ padding: 0 10px;
294
+ }
295
+
296
+ /* Message Row Layout */
297
+ .message-row {
298
+ display: flex;
299
+ flex-direction: column;
300
+ margin-bottom: 14px;
301
+ width: 100%;
302
+ animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
303
+ }
304
+
305
+ .message-row-you {
306
+ align-items: flex-end;
307
+ }
308
+
309
+ .message-row-stranger {
310
+ align-items: flex-start;
311
+ }
312
+
313
+ /* Chat Bubbles */
314
+ .msg {
315
+ max-width: 68%;
316
+ padding: 12px 18px;
317
+ font-size: 14.5px;
318
+ line-height: 1.55;
319
+ word-break: break-word;
320
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12);
321
+ }
322
+
323
+ .msg-you {
324
+ background: #18181b;
325
+ color: #f4f4f5;
326
+ border: 1px solid #27272a;
327
+ border-radius: 16px 16px 4px 16px;
328
+ }
329
+
330
+ .msg-stranger {
331
+ background: linear-gradient(135deg, #f472b6 0%, #ec4899 100%);
332
+ color: #ffffff;
333
+ border-radius: 16px 16px 16px 4px;
334
+ box-shadow: 0 4px 14px rgba(236, 72, 153, 0.2);
335
+ }
336
+
337
+ .msg-label {
338
+ font-size: 11px;
339
+ margin-bottom: 4px;
340
+ font-weight: 600;
341
+ letter-spacing: 0.5px;
342
+ text-transform: uppercase;
343
+ }
344
+
345
+ .msg-label-stranger {
346
+ color: #f472b6;
347
+ padding-left: 6px;
348
+ }
349
+
350
+ .msg-label-you {
351
+ color: #71717a;
352
+ padding-right: 6px;
353
+ }
354
+
355
+ .prefix { display: none; }
356
+
357
+ /* Typing Dots Loader inside bubble */
358
+ .typing-dots {
359
+ display: flex;
360
+ align-items: center;
361
+ gap: 5px;
362
+ height: 20px;
363
+ padding: 2px 4px;
364
+ }
365
+
366
+ .typing-dots span {
367
+ width: 7px;
368
+ height: 7px;
369
+ background: #ffffff;
370
+ border-radius: 50%;
371
+ display: inline-block;
372
+ animation: typing 1.4s infinite ease-in-out both;
373
+ }
374
+
375
+ .typing-dots span:nth-child(1) {
376
+ animation-delay: -0.32s;
377
+ }
378
+
379
+ .typing-dots span:nth-child(2) {
380
+ animation-delay: -0.16s;
381
+ }
382
+
383
+ @keyframes typing {
384
+ 0%, 80%, 100% {
385
+ transform: scale(0.6);
386
+ opacity: 0.5;
387
+ }
388
+ 40% {
389
+ transform: scale(1.15);
390
+ opacity: 1;
391
+ }
392
+ }
393
+
394
+ @keyframes slideIn {
395
+ from {
396
+ opacity: 0;
397
+ transform: translateY(12px) scale(0.97);
398
+ }
399
+ to {
400
+ opacity: 1;
401
+ transform: translateY(0) scale(1);
402
+ }
403
+ }
404
+
405
+ /* Chat Controls */
406
+ .chat-controls {
407
+ display: flex;
408
+ align-items: stretch;
409
+ padding: 16px;
410
+ border-top: 1px solid #2a2a2a;
411
+ gap: 10px;
412
+ background: #1a1a1a;
413
+ }
414
+
415
+ .control-btn {
416
+ width: 72px;
417
+ border: none;
418
+ border-radius: 8px;
419
+ cursor: pointer;
420
+ display: flex;
421
+ flex-direction: column;
422
+ justify-content: center;
423
+ align-items: center;
424
+ font-size: 13px;
425
+ font-weight: 600;
426
+ transition: opacity 0.15s, background 0.15s;
427
+ flex-shrink: 0;
428
+ }
429
+
430
+ .new-btn {
431
+ background: #272727;
432
+ color: #f87171;
433
+ border: 1px solid #333;
434
+ }
435
+ .new-btn:hover { background: #333; }
436
+
437
+ .send-btn {
438
+ background: #0ea5e9;
439
+ color: #fff;
440
+ }
441
+ .send-btn:hover:not(:disabled) { background: #0284c7; }
442
+ .send-btn:disabled {
443
+ background: #1f2937;
444
+ color: #4b5563;
445
+ cursor: not-allowed;
446
+ }
447
+
448
+ .sub-text {
449
+ font-size: 10px;
450
+ opacity: 0.55;
451
+ margin-top: 3px;
452
+ font-weight: 400;
453
+ }
454
+
455
+ .input-container {
456
+ flex: 1;
457
+ }
458
+
459
+ #chat-input {
460
+ width: 100%;
461
+ height: 100%;
462
+ min-height: 48px;
463
+ background: #111;
464
+ border: 1px solid #2a2a2a;
465
+ border-radius: 8px;
466
+ resize: none;
467
+ padding: 13px 16px;
468
+ font-size: 14px;
469
+ color: #e5e5e5;
470
+ outline: none;
471
+ transition: border-color 0.15s;
472
+ line-height: 1.4;
473
+ }
474
+
475
+ #chat-input:focus {
476
+ border-color: #0ea5e9;
477
+ }
478
+
479
+ #chat-input::placeholder {
480
+ color: #444;
481
+ }