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