Student Focus Reminder Avatar

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