Quizlet AI Answer Assistant

Add AI-powered answer buttons to Quizlet flashcards for detailed explanations and verification

Size

22.0 KB

Version

1.1.10

Created

Nov 8, 2025

Updated

21 days ago

1// ==UserScript==
2// @name		Quizlet AI Answer Assistant
3// @description		Add AI-powered answer buttons to Quizlet flashcards for detailed explanations and verification
4// @version		1.1.10
5// @match		https://*.quizlet.com/*
6// @icon		https://assets.quizlet.com/_next/static/media/q-twilight.e27821d9.png
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    console.log('Quizlet AI Answer Assistant loaded');
12
13    // Add custom styles for the AI button
14    TM_addStyle(`
15        .ai-answer-button {
16            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17            color: white;
18            border: none;
19            padding: 8px 16px;
20            border-radius: 8px;
21            font-size: 14px;
22            font-weight: 600;
23            cursor: pointer;
24            margin: 8px 0;
25            transition: all 0.3s ease;
26            box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
27        }
28        
29        .ai-answer-button:hover {
30            transform: translateY(-2px);
31            box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
32        }
33        
34        .ai-answer-button:disabled {
35            background: #ccc;
36            cursor: not-allowed;
37            transform: none;
38        }
39        
40        .ai-answer-container {
41            background: #f8f9fa;
42            border: 2px solid #667eea;
43            border-radius: 12px;
44            padding: 20px;
45            margin: 16px 0;
46            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
47            line-height: 1.6;
48            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
49        }
50        
51        .ai-answer-container h3 {
52            color: #667eea;
53            margin-top: 0;
54            margin-bottom: 12px;
55            font-size: 18px;
56            border-bottom: 2px solid #667eea;
57            padding-bottom: 8px;
58        }
59        
60        .ai-answer-container h4 {
61            color: #764ba2;
62            margin-top: 16px;
63            margin-bottom: 8px;
64            font-size: 16px;
65        }
66        
67        .ai-answer-container p {
68            margin: 8px 0;
69            color: #333;
70        }
71        
72        .ai-answer-container strong {
73            color: #667eea;
74        }
75        
76        .ai-loading {
77            display: inline-block;
78            width: 16px;
79            height: 16px;
80            border: 3px solid rgba(255, 255, 255, 0.3);
81            border-radius: 50%;
82            border-top-color: white;
83            animation: spin 1s ease-in-out infinite;
84            margin-right: 8px;
85        }
86        
87        @keyframes spin {
88            to { transform: rotate(360deg); }
89        }
90        
91        .ai-error {
92            background: #fee;
93            border-color: #f44;
94            color: #c00;
95        }
96        
97        .card-timer {
98            position: fixed;
99            top: 20px;
100            right: 20px;
101            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
102            color: white;
103            padding: 12px 20px;
104            border-radius: 12px;
105            font-size: 24px;
106            font-weight: 700;
107            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
108            box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
109            z-index: 10000;
110            min-width: 100px;
111            text-align: center;
112        }
113    `);
114
115    // Function to extract question and answer from a flashcard
116    function extractCardContent(cardElement) {
117        try {
118            // Check if we're in list view or flashcard view
119            const isListView = cardElement.hasAttribute('aria-label') && cardElement.getAttribute('aria-label') === 'Term';
120            
121            if (isListView) {
122                // List view: Find the term (question) and definition (answer)
123                const termElement = cardElement.querySelector('[data-testid="set-page-term-card-side"]');
124                const definitionElement = cardElement.querySelectorAll('[data-testid="set-page-term-card-side"]')[1];
125                
126                if (!termElement) {
127                    console.error('Could not find term element');
128                    return null;
129                }
130                
131                const question = termElement.querySelector('.TermText')?.textContent?.trim() || '';
132                const answer = definitionElement?.querySelector('.TermText')?.textContent?.trim() || '';
133                
134                console.log('Extracted content (list view):', { question, answer });
135                
136                return { question, answer };
137            } else {
138                // Flashcard view: Find the front and back of the card
139                const frontCard = cardElement.querySelector('[data-testid="Card-front"]');
140                const backCard = cardElement.querySelector('[data-testid="Card-back"]');
141                
142                if (!frontCard) {
143                    console.error('Could not find card front');
144                    return null;
145                }
146                
147                const question = frontCard.querySelector('.FormattedText')?.textContent?.trim() || 
148                                frontCard.querySelector('[data-testid="Term"]')?.textContent?.trim() || '';
149                const answer = backCard?.querySelector('.FormattedText')?.textContent?.trim() || 
150                              backCard?.querySelector('[data-testid="Definition"]')?.textContent?.trim() || '';
151                
152                console.log('Extracted content (flashcard view):', { question, answer });
153                
154                return { question, answer };
155            }
156        } catch (error) {
157            console.error('Error extracting card content:', error);
158            return null;
159        }
160    }
161
162    // Function to create AI answer using RM.aiCall
163    async function getAIAnswer(question, existingAnswer) {
164        try {
165            console.log('Calling AI with question:', question);
166            console.log('Existing answer:', existingAnswer);
167            
168            let prompt = `You are a medical education expert. A student is studying neurology flashcards.
169
170Question: ${question}`;
171
172            if (existingAnswer) {
173                prompt += `\n\nThe provided answer is: "${existingAnswer}"`;
174                prompt += `\n\nPlease verify if this answer is correct and provide a comprehensive explanation.`;
175            } else {
176                prompt += `\n\nNo answer is provided. Please generate a detailed, accurate answer.`;
177            }
178
179            prompt += `\n\nProvide your response in the following structured format:
180
181✅ Correct Answer:
182[Write the correct answer here first - this is the most important part. Be clear and concise.]
183
184Full Answer:
185[Write the complete, accurate answer here. If it's a term, provide its definition. If it's an open question, provide a well-structured answer in complete sentences.]
186
187Detailed Explanation:
188[Provide a scientific and in-depth explanation that clarifies the principle, pathophysiology, mechanism, or logic behind the answer. Include:
189- When this is correct (conditions or clinical context)
190- Why it's incorrect in other situations (if applicable)
191- How it differs from similar answers or related concepts]
192
193Clinical Example or Application:
194[If relevant, add a brief case or clinical example that illustrates the principle.]
195
196Summary for Memory:
197[One concise line with an easy-to-remember formulation or a short mnemonic.]`;
198
199            const response = await RM.aiCall(prompt);
200            console.log('AI response received:', response);
201            
202            return response;
203        } catch (error) {
204            console.error('Error calling AI:', error);
205            throw error;
206        }
207    }
208
209    // Function to display AI answer
210    function displayAIAnswer(containerElement, aiResponse, isError = false) {
211        const existingAnswer = containerElement.querySelector('.ai-answer-container');
212        if (existingAnswer) {
213            existingAnswer.remove();
214        }
215        
216        const answerDiv = document.createElement('div');
217        answerDiv.className = 'ai-answer-container' + (isError ? ' ai-error' : '');
218        
219        if (isError) {
220            answerDiv.innerHTML = `
221                <h3>❌ Error</h3>
222                <p>${aiResponse}</p>
223            `;
224        } else {
225            // Format the AI response with proper HTML
226            const formattedResponse = aiResponse
227                .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
228                .replace(/\n\n/g, '</p><p>')
229                .replace(/\n/g, '<br>');
230            
231            answerDiv.innerHTML = `
232                <h3>✅ AI Answer</h3>
233                <div>${formattedResponse}</div>
234            `;
235        }
236        
237        containerElement.appendChild(answerDiv);
238    }
239
240    // Function to display loading state
241    function displayLoadingState(containerElement) {
242        const existingAnswer = containerElement.querySelector('.ai-answer-container');
243        if (existingAnswer) {
244            existingAnswer.remove();
245        }
246        
247        const loadingDiv = document.createElement('div');
248        loadingDiv.className = 'ai-answer-container';
249        loadingDiv.innerHTML = `
250            <h3>🤔 Thinking...</h3>
251            <p>AI is analyzing the question and preparing a detailed answer...</p>
252        `;
253        
254        containerElement.appendChild(loadingDiv);
255    }
256
257    // Function to add AI button to a flashcard
258    function addAIButtonToCard(cardElement) {
259        // Check if button already exists
260        if (cardElement.querySelector('.ai-answer-button')) {
261            return;
262        }
263        
264        console.log('Adding AI button to card');
265        
266        // Find the button container based on view type
267        const isListView = cardElement.hasAttribute('aria-label') && cardElement.getAttribute('aria-label') === 'Term';
268        let buttonContainer;
269        
270        if (isListView) {
271            // List view: Find the button container (where star and audio buttons are)
272            buttonContainer = cardElement.querySelector('.a11nf743');
273        } else {
274            // Flashcard view: Find the button container on the front of the card
275            buttonContainer = cardElement.querySelector('[data-testid="Card-front"] .w97e1xh');
276        }
277        
278        if (!buttonContainer) {
279            console.error('Could not find button container');
280            return;
281        }
282        
283        // Create AI button
284        const aiButton = document.createElement('button');
285        aiButton.className = 'ai-answer-button';
286        aiButton.innerHTML = '🤖 Answer with AI';
287        aiButton.type = 'button';
288        
289        // For flashcard view, make the button look like the other icon buttons
290        if (!isListView) {
291            aiButton.style.cssText = `
292                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
293                color: white;
294                border: none;
295                padding: 6px 12px;
296                border-radius: 6px;
297                font-size: 12px;
298                font-weight: 600;
299                cursor: pointer;
300                margin-left: 8px;
301                transition: all 0.3s ease;
302            `;
303        }
304        
305        aiButton.addEventListener('click', async function() {
306            console.log('AI button clicked');
307            
308            // Disable button during processing
309            aiButton.disabled = true;
310            aiButton.innerHTML = '<span class="ai-loading"></span>Processing...';
311            
312            try {
313                // Extract question and answer
314                const content = extractCardContent(cardElement);
315                
316                if (!content) {
317                    throw new Error('Could not extract card content');
318                }
319                
320                // Show loading state
321                displayLoadingState(cardElement);
322                
323                // Get AI answer
324                const aiResponse = await getAIAnswer(content.question, content.answer);
325                
326                // Display the answer
327                displayAIAnswer(cardElement, aiResponse);
328                
329                // Re-enable button
330                aiButton.disabled = false;
331                aiButton.innerHTML = '🤖 Answer with AI';
332                
333            } catch (error) {
334                console.error('Error processing AI answer:', error);
335                displayAIAnswer(cardElement, 'Failed to get AI answer. Please try again.', true);
336                
337                // Re-enable button
338                aiButton.disabled = false;
339                aiButton.innerHTML = '🤖 Answer with AI';
340            }
341        });
342        
343        // Add button to the container
344        buttonContainer.appendChild(aiButton);
345        console.log('AI button added successfully');
346    }
347
348    // Function to process all flashcards on the page
349    function processFlashcards() {
350        console.log('Processing flashcards...');
351        
352        // Find all flashcard elements - try both list view and flashcard view
353        let cards = document.querySelectorAll('[aria-label="Term"]');
354        
355        // If no cards found in list view, try flashcard view
356        if (cards.length === 0) {
357            cards = document.querySelectorAll('[data-testid="Card-front"]');
358            console.log(`Found ${cards.length} flashcards (flashcard view)`);
359        } else {
360            console.log(`Found ${cards.length} flashcards (list view)`);
361        }
362        
363        cards.forEach((card, index) => {
364            console.log(`Processing card ${index + 1}`);
365            // For flashcard view, we need to add the button to the parent container
366            const targetElement = card.closest('.c1j5a868') || card;
367            addAIButtonToCard(targetElement);
368        });
369    }
370    
371    // Function to clear all AI answers from the page
372    function clearAIAnswers() {
373        const allAnswers = document.querySelectorAll('.ai-answer-container');
374        allAnswers.forEach(answer => answer.remove());
375        console.log(`Cleared ${allAnswers.length} AI answers`);
376    }
377
378    // Debounce function to avoid excessive calls
379    function debounce(func, wait) {
380        let timeout;
381        return function executedFunction(...args) {
382            const later = () => {
383                clearTimeout(timeout);
384                func(...args);
385            };
386            clearTimeout(timeout);
387            timeout = setTimeout(later, wait);
388        };
389    }
390
391    // Timer functions
392    let timerInterval = null;
393    let startTime = null;
394    let timerElement = null;
395
396    function createTimer() {
397        if (timerElement) {
398            return;
399        }
400        
401        timerElement = document.createElement('div');
402        timerElement.className = 'card-timer';
403        timerElement.textContent = '0:00';
404        document.body.appendChild(timerElement);
405        console.log('Timer created');
406    }
407
408    function startTimer() {
409        if (timerInterval) {
410            clearInterval(timerInterval);
411        }
412        
413        startTime = Date.now();
414        
415        timerInterval = setInterval(() => {
416            const elapsed = Math.floor((Date.now() - startTime) / 1000);
417            const minutes = Math.floor(elapsed / 60);
418            const seconds = elapsed % 60;
419            
420            if (timerElement) {
421                timerElement.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
422            }
423        }, 100);
424        
425        console.log('Timer started');
426    }
427
428    function resetTimer() {
429        if (timerInterval) {
430            clearInterval(timerInterval);
431        }
432        
433        if (timerElement) {
434            timerElement.textContent = '0:00';
435        }
436        
437        startTimer();
438        console.log('Timer reset');
439    }
440
441    // Initialize the extension
442    function init() {
443        console.log('Initializing Quizlet AI Answer Assistant...');
444        
445        // Track current question to detect when it changes
446        let currentQuestion = '';
447        
448        // Process existing flashcards
449        TM_runBody(() => {
450            // Wait a bit for the page to fully load
451            setTimeout(() => {
452                processFlashcards();
453                
454                // Check if we're in flashcard mode
455                const isFlashcardMode = window.location.href.includes('/flashcards');
456                
457                // Create and start timer only in flashcard mode
458                if (isFlashcardMode) {
459                    createTimer();
460                    startTimer();
461                }
462                
463                // Update current question
464                const cardElement = document.querySelector('.c1j5a868') || document.querySelector('[aria-label="Term"]');
465                if (cardElement) {
466                    const content = extractCardContent(cardElement);
467                    if (content) {
468                        currentQuestion = content.question;
469                        console.log('Initial question:', currentQuestion);
470                    }
471                }
472                
473                // Add click listeners to navigation buttons
474                const addNavigationListeners = () => {
475                    const nextButton = document.querySelector('button[aria-label="Select to study the next card"]');
476                    const prevButton = document.querySelector('button[aria-label="Select to study the previous card"]');
477                    
478                    if (nextButton && !nextButton.hasAttribute('data-listener-added')) {
479                        nextButton.setAttribute('data-listener-added', 'true');
480                        nextButton.addEventListener('click', () => {
481                            console.log('Next button clicked - clearing answers and resetting timer');
482                            setTimeout(() => {
483                                clearAIAnswers();
484                                if (isFlashcardMode) {
485                                    resetTimer();
486                                }
487                                processFlashcards();
488                                
489                                // Update current question
490                                const cardElement = document.querySelector('.c1j5a868') || document.querySelector('[aria-label="Term"]');
491                                if (cardElement) {
492                                    const content = extractCardContent(cardElement);
493                                    if (content) {
494                                        currentQuestion = content.question;
495                                        console.log('New question:', currentQuestion);
496                                    }
497                                }
498                            }, 300);
499                        });
500                        console.log('Added listener to next button');
501                    }
502                    
503                    if (prevButton && !prevButton.hasAttribute('data-listener-added')) {
504                        prevButton.setAttribute('data-listener-added', 'true');
505                        prevButton.addEventListener('click', () => {
506                            console.log('Previous button clicked - clearing answers and resetting timer');
507                            setTimeout(() => {
508                                clearAIAnswers();
509                                if (isFlashcardMode) {
510                                    resetTimer();
511                                }
512                                processFlashcards();
513                                
514                                // Update current question
515                                const cardElement = document.querySelector('.c1j5a868') || document.querySelector('[aria-label="Term"]');
516                                if (cardElement) {
517                                    const content = extractCardContent(cardElement);
518                                    if (content) {
519                                        currentQuestion = content.question;
520                                        console.log('New question:', currentQuestion);
521                                    }
522                                }
523                            }, 300);
524                        });
525                        console.log('Added listener to previous button');
526                    }
527                };
528                
529                // Add listeners initially
530                addNavigationListeners();
531                
532                // Also listen for keyboard arrow keys
533                document.addEventListener('keydown', (e) => {
534                    if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
535                        console.log('Arrow key pressed - clearing answers and resetting timer');
536                        setTimeout(() => {
537                            clearAIAnswers();
538                            if (isFlashcardMode) {
539                                resetTimer();
540                            }
541                            processFlashcards();
542                            
543                            // Update current question
544                            const cardElement = document.querySelector('.c1j5a868') || document.querySelector('[aria-label="Term"]');
545                            if (cardElement) {
546                                const content = extractCardContent(cardElement);
547                                if (content) {
548                                    currentQuestion = content.question;
549                                    console.log('New question:', currentQuestion);
550                                }
551                            }
552                        }, 300);
553                    }
554                });
555                
556                // Set up observer for dynamically loaded content
557                const observer = new MutationObserver(debounce(() => {
558                    console.log('DOM changed, reprocessing...');
559                    processFlashcards();
560                    addNavigationListeners();
561                }, 500));
562                
563                observer.observe(document.body, {
564                    childList: true,
565                    subtree: true
566                });
567                
568                console.log('Observer set up successfully');
569            }, 5000);
570        });
571    }
572
573    // Start the extension
574    init();
575})();
Quizlet AI Answer Assistant | Robomonkey