Student Focus Reminder Avatar

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})();
Student Focus Reminder Avatar | Robomonkey