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})();