Displays an avatar that reminds students to stay focused with customizable prompts at adjustable intervals
Size
16.9 KB
Version
1.1.20
Created
Oct 28, 2025
Updated
17 days ago
1// ==UserScript==
2// @name Student Focus Reminder Avatar
3// @description Displays an avatar that reminds students to stay focused with customizable prompts at adjustable intervals
4// @version 1.1.20
5// @match https://*.google.com/*
6// @icon https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 // Array of on-task prompts
12 const prompts = [
13 'Keep your eyes on the teacher! š',
14 'Stay focused on the lesson! š',
15 'Are you paying attention? šÆ',
16 'Listen carefully to your teacher! š',
17 'Focus on what\'s being taught! š”',
18 'Eyes up front, please! šļø',
19 'Stay engaged with the lesson! āØ',
20 'Remember to concentrate! š§ ',
21 'Keep your attention on the teacher! š',
22 'Stay on task! ā'
23 ];
24
25 let reminderInterval;
26 let currentIntervalMinutes = 2; // Default 2 minutes
27 let customAvatarImage = null; // Store custom avatar
28 let textToSpeechEnabled = true; // Enable text-to-speech by default
29 let avatarVerticalPosition = 15; // Default vertical position in pixels
30 let isShowingPrompt = false; // Prevent overlapping prompts
31 let isInitialized = false; // Prevent double initialization
32
33 // Create the avatar container
34 function createAvatar() {
35 const avatarContainer = document.createElement('div');
36 avatarContainer.id = 'focus-reminder-avatar';
37 avatarContainer.style.cssText = `
38 position: fixed;
39 bottom: 20px;
40 right: 20px;
41 z-index: 999999;
42 font-family: Arial, sans-serif;
43 `;
44
45 // Avatar circle
46 const avatar = document.createElement('div');
47 avatar.id = 'avatar-circle';
48 avatar.style.cssText = `
49 width: 60px;
50 height: 60px;
51 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
52 border-radius: 50%;
53 display: flex;
54 align-items: center;
55 justify-content: center;
56 font-size: 30px;
57 cursor: pointer;
58 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
59 transition: transform 0.3s ease;
60 overflow: hidden;
61 `;
62
63 updateAvatarDisplay(avatar);
64
65 avatar.addEventListener('mouseenter', () => {
66 avatar.style.transform = 'scale(1.1)';
67 });
68
69 avatar.addEventListener('mouseleave', () => {
70 avatar.style.transform = 'scale(1)';
71 });
72
73 avatar.addEventListener('click', () => {
74 showSettingsPanel();
75 });
76
77 // Message bubble (hidden by default)
78 const messageBubble = document.createElement('div');
79 messageBubble.id = 'message-bubble';
80 messageBubble.style.cssText = `
81 position: absolute;
82 bottom: 70px;
83 right: 0;
84 background: white;
85 color: #333;
86 padding: 15px 20px;
87 border-radius: 15px;
88 box-shadow: 0 4px 12px rgba(0,0,0,0.2);
89 max-width: 250px;
90 display: none;
91 font-size: 16px;
92 font-weight: 600;
93 text-align: center;
94 border: 3px solid #667eea;
95 `;
96
97 avatarContainer.appendChild(messageBubble);
98 avatarContainer.appendChild(avatar);
99 document.body.appendChild(avatarContainer);
100
101 console.log('Focus reminder avatar created');
102 }
103
104 // Update avatar display with custom image or emoji
105 function updateAvatarDisplay(avatar) {
106 if (!avatar) avatar = document.getElementById('avatar-circle');
107 if (!avatar) return;
108
109 if (customAvatarImage) {
110 const img = document.createElement('img');
111 img.src = customAvatarImage;
112 img.style.width = '100%';
113 img.style.height = '100%';
114 img.style.objectFit = 'cover';
115 img.style.borderRadius = '50%';
116 img.style.transform = `translateY(${avatarVerticalPosition}px)`;
117 avatar.innerHTML = '';
118 avatar.appendChild(img);
119 console.log('Custom avatar displayed with position:', avatarVerticalPosition);
120 } else {
121 // Use Google Drive image with proper positioning
122 const img = document.createElement('img');
123 img.src = 'https://drive.google.com/uc?export=view&id=1yVtZLFNhkWHfaay5nDEJg60iEjhooJ-h';
124 img.style.width = '100%';
125 img.style.height = '100%';
126 img.style.objectFit = 'cover';
127 img.style.borderRadius = '50%';
128 img.style.transform = `translateY(${avatarVerticalPosition}px)`;
129 img.onerror = function() {
130 console.error('Failed to load avatar image from Google Drive');
131 avatar.innerHTML = 'šØāš«';
132 };
133 img.onload = function() {
134 console.log('Avatar image loaded successfully with position:', avatarVerticalPosition);
135 };
136 avatar.innerHTML = '';
137 avatar.appendChild(img);
138 }
139 }
140
141 // Show a random prompt
142 function showPrompt() {
143 if (isShowingPrompt) {
144 console.log('Prompt blocked - already showing a prompt');
145 return; // Prevent overlapping prompts
146 }
147
148 const messageBubble = document.getElementById('message-bubble');
149 if (!messageBubble) {
150 console.log('Message bubble not found');
151 return;
152 }
153
154 isShowingPrompt = true;
155 const randomPrompt = prompts[Math.floor(Math.random() * prompts.length)];
156 messageBubble.textContent = randomPrompt;
157 messageBubble.style.display = 'block';
158
159 // Animate the avatar
160 const avatar = document.getElementById('avatar-circle');
161 avatar.style.animation = 'bounce 0.5s ease';
162
163 console.log('Showing and speaking prompt:', randomPrompt);
164
165 // Text-to-speech
166 if (textToSpeechEnabled) {
167 speakPrompt(randomPrompt);
168 }
169
170 // Hide the message after 10 seconds
171 setTimeout(() => {
172 messageBubble.style.display = 'none';
173 avatar.style.animation = '';
174 isShowingPrompt = false;
175 console.log('Prompt hidden, ready for next prompt');
176 }, 10000);
177 }
178
179 // Text-to-speech function
180 function speakPrompt(text) {
181 try {
182 // Cancel any ongoing speech
183 window.speechSynthesis.cancel();
184
185 // Remove all emojis and special characters for better speech
186 const cleanText = text.replace(/[\u{1F000}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '').trim();
187
188 const utterance = new SpeechSynthesisUtterance(cleanText);
189 utterance.rate = 0.9; // Slightly slower for clarity
190 utterance.pitch = 1.0;
191 utterance.volume = 1.0;
192
193 window.speechSynthesis.speak(utterance);
194 console.log('TTS speaking:', cleanText);
195 } catch (error) {
196 console.error('Text-to-speech error:', error);
197 }
198 }
199
200 // Create settings panel
201 function showSettingsPanel() {
202 // Remove existing panel if any
203 const existingPanel = document.getElementById('focus-settings-panel');
204 if (existingPanel) {
205 existingPanel.remove();
206 return;
207 }
208
209 const panel = document.createElement('div');
210 panel.id = 'focus-settings-panel';
211 panel.style.cssText = `
212 position: fixed;
213 bottom: 90px;
214 right: 20px;
215 background: white;
216 color: #333;
217 padding: 20px;
218 border-radius: 10px;
219 box-shadow: 0 4px 20px rgba(0,0,0,0.3);
220 z-index: 999998;
221 min-width: 280px;
222 max-height: 500px;
223 overflow-y: auto;
224 border: 2px solid #667eea;
225 `;
226
227 panel.innerHTML = `
228 <h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 18px;">āļø Reminder Settings</h3>
229
230 <label style="display: block; margin-bottom: 10px; font-weight: 600; color: #333;">
231 Custom Avatar Image:
232 </label>
233 <input type="file" id="avatar-upload" accept="image/*"
234 style="width: 100%; padding: 8px; border: 2px solid #667eea; border-radius: 5px; font-size: 14px; margin-bottom: 10px;">
235 <button id="reset-avatar-btn" style="
236 width: 100%;
237 padding: 8px;
238 background: #dc3545;
239 color: white;
240 border: none;
241 border-radius: 5px;
242 font-size: 14px;
243 font-weight: 600;
244 cursor: pointer;
245 margin-bottom: 15px;
246 ">Reset to Default Avatar</button>
247
248 <label style="display: block; margin-bottom: 10px; font-weight: 600; color: #333;">
249 Avatar Vertical Position:
250 </label>
251 <input type="range" id="avatar-position-slider" min="-20" max="40" step="1" value="${avatarVerticalPosition}"
252 style="width: 100%; margin-bottom: 5px;">
253 <div style="text-align: center; font-size: 12px; color: #666; margin-bottom: 15px;">
254 Position: <span id="position-value">${avatarVerticalPosition}</span>px
255 </div>
256
257 <label style="display: block; margin-bottom: 10px; font-weight: 600; color: #333;">
258 <input type="checkbox" id="tts-toggle" ${textToSpeechEnabled ? 'checked' : ''}
259 style="margin-right: 8px; width: 18px; height: 18px; vertical-align: middle; cursor: pointer;">
260 Enable Text-to-Speech
261 </label>
262
263 <label style="display: block; margin-bottom: 10px; margin-top: 15px; font-weight: 600; color: #333;">
264 Reminder Interval (minutes):
265 </label>
266 <input type="number" id="interval-input" min="0.5" max="60" step="0.5" value="${currentIntervalMinutes}"
267 style="width: 100%; padding: 8px; border: 2px solid #667eea; border-radius: 5px; font-size: 16px; margin-bottom: 15px;">
268 <button id="save-interval-btn" style="
269 width: 100%;
270 padding: 10px;
271 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
272 color: white;
273 border: none;
274 border-radius: 5px;
275 font-size: 16px;
276 font-weight: 600;
277 cursor: pointer;
278 margin-bottom: 10px;
279 ">Save Changes</button>
280 <button id="test-prompt-btn" style="
281 width: 100%;
282 padding: 10px;
283 background: #28a745;
284 color: white;
285 border: none;
286 border-radius: 5px;
287 font-size: 16px;
288 font-weight: 600;
289 cursor: pointer;
290 ">Test Prompt Now</button>
291 <p style="margin: 15px 0 0 0; font-size: 12px; color: #666;">
292 Current: Every ${currentIntervalMinutes} minute(s)
293 </p>
294 `;
295
296 document.body.appendChild(panel);
297
298 // Avatar position slider handler
299 const positionSlider = document.getElementById('avatar-position-slider');
300 const positionValue = document.getElementById('position-value');
301 positionSlider.addEventListener('input', (e) => {
302 avatarVerticalPosition = parseInt(e.target.value);
303 positionValue.textContent = avatarVerticalPosition;
304 updateAvatarDisplay();
305 });
306
307 // Avatar upload handler
308 const avatarUpload = document.getElementById('avatar-upload');
309 avatarUpload.addEventListener('change', async (e) => {
310 const file = e.target.files[0];
311 if (file && file.type.startsWith('image/')) {
312 const reader = new FileReader();
313 reader.onload = async (event) => {
314 customAvatarImage = event.target.result;
315 await GM.setValue('customAvatarImage', customAvatarImage);
316 updateAvatarDisplay();
317 console.log('Custom avatar uploaded successfully');
318 alert('Avatar uploaded successfully! Now adjust the position and click Save Changes.');
319 };
320 reader.onerror = (error) => {
321 console.error('Error reading file:', error);
322 alert('Error uploading avatar. Please try again.');
323 };
324 reader.readAsDataURL(file);
325 } else {
326 alert('Please select a valid image file.');
327 }
328 });
329
330 // Reset avatar handler
331 document.getElementById('reset-avatar-btn').addEventListener('click', async () => {
332 customAvatarImage = null;
333 await GM.deleteValue('customAvatarImage');
334 updateAvatarDisplay();
335 console.log('Avatar reset to default');
336 alert('Avatar reset to default!');
337 });
338
339 // Text-to-speech toggle handler
340 document.getElementById('tts-toggle').addEventListener('change', async (e) => {
341 textToSpeechEnabled = e.target.checked;
342 await GM.setValue('textToSpeechEnabled', textToSpeechEnabled);
343 console.log('Text-to-speech:', textToSpeechEnabled ? 'enabled' : 'disabled');
344 });
345
346 // Save button handler
347 document.getElementById('save-interval-btn').addEventListener('click', async () => {
348 const newInterval = parseFloat(document.getElementById('interval-input').value);
349 if (newInterval > 0) {
350 currentIntervalMinutes = newInterval;
351 await GM.setValue('focusReminderInterval', currentIntervalMinutes);
352 await GM.setValue('avatarVerticalPosition', avatarVerticalPosition);
353 startReminderInterval();
354 panel.remove();
355 console.log('Interval updated to:', currentIntervalMinutes, 'minutes');
356 console.log('Avatar position saved:', avatarVerticalPosition, 'px');
357 }
358 });
359
360 // Test button handler
361 document.getElementById('test-prompt-btn').addEventListener('click', () => {
362 showPrompt();
363 panel.remove();
364 });
365
366 // Close panel when clicking outside
367 setTimeout(() => {
368 document.addEventListener('click', function closePanel(e) {
369 if (!panel.contains(e.target) && !document.getElementById('avatar-circle').contains(e.target)) {
370 panel.remove();
371 document.removeEventListener('click', closePanel);
372 }
373 });
374 }, 100);
375 }
376
377 // Start the reminder interval
378 function startReminderInterval() {
379 // Clear existing interval
380 if (reminderInterval) {
381 clearInterval(reminderInterval);
382 }
383
384 // Convert minutes to milliseconds
385 const intervalMs = currentIntervalMinutes * 60 * 1000;
386
387 // Set new interval
388 reminderInterval = setInterval(() => {
389 showPrompt();
390 }, intervalMs);
391
392 console.log('Reminder interval started:', currentIntervalMinutes, 'minutes');
393 }
394
395 // Add CSS animations
396 function addStyles() {
397 const style = document.createElement('style');
398 style.textContent = `
399 @keyframes bounce {
400 0%, 100% { transform: translateY(0); }
401 50% { transform: translateY(-10px); }
402 }
403 `;
404 document.head.appendChild(style);
405 }
406
407 // Initialize the extension
408 async function init() {
409 if (isInitialized) {
410 console.log('Extension already initialized, skipping...');
411 return;
412 }
413 isInitialized = true;
414
415 console.log('Student Focus Reminder Avatar initializing...');
416
417 // Load saved interval
418 const savedInterval = await GM.getValue('focusReminderInterval', 2);
419 currentIntervalMinutes = savedInterval;
420
421 // Load saved custom avatar
422 const savedAvatar = await GM.getValue('customAvatarImage', null);
423 customAvatarImage = savedAvatar;
424
425 // Load text-to-speech preference
426 const savedTTS = await GM.getValue('textToSpeechEnabled', true);
427 textToSpeechEnabled = savedTTS;
428
429 // Load saved avatar position
430 const savedPosition = await GM.getValue('avatarVerticalPosition', 15);
431 avatarVerticalPosition = savedPosition;
432
433 addStyles();
434 createAvatar();
435 startReminderInterval();
436
437 // Show initial prompt after 5 seconds
438 setTimeout(() => {
439 showPrompt();
440 }, 5000);
441
442 console.log('Student Focus Reminder Avatar initialized successfully');
443 }
444
445 // Wait for page to load
446 if (document.readyState === 'loading') {
447 document.addEventListener('DOMContentLoaded', init);
448 } else {
449 init();
450 }
451})();