Skip to content

Commit 29ce1c9

Browse files
committed
Added All Files: LeetCode Learning Assistant Chrome extension
0 parents  commit 29ce1c9

File tree

10 files changed

+849
-0
lines changed

10 files changed

+849
-0
lines changed

background.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Main Content Generation
2+
3+
// Listen messages from the content_script.js
4+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
5+
if (request.type === 'getAnalysis') {
6+
7+
chrome.storage.sync.get(['geminiApiKey'], function (result) {
8+
if (!result.geminiApiKey) {
9+
chrome.tabs.sendMessage(sender.tab.id, {
10+
type: 'analysisResult',
11+
stage: request.stage,
12+
content: 'ERROR: API Key not set. Please set it in the extension popup.'
13+
});
14+
return;
15+
}
16+
callGeminiAPI(request.problem, request.stage, result.geminiApiKey, sender.tab.id);
17+
});
18+
return true;
19+
}
20+
});
21+
22+
23+
// delay for loading the content
24+
function sleep(ms) {
25+
return new Promise(resolve => setTimeout(resolve, ms));
26+
}
27+
28+
// --- Gemini API Call ---
29+
async function callGeminiAPI(problem, stage, apiKey, tabId) {
30+
const model = 'gemini-2.5-flash';
31+
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
32+
const prompt = generatePrompt(problem, stage);
33+
34+
const maxRetries = 3;
35+
let lastError = null;
36+
37+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
38+
try {
39+
const response = await fetch(apiUrl, {
40+
method: 'POST',
41+
headers: {
42+
'Content-Type': 'application/json',
43+
},
44+
body: JSON.stringify({
45+
"contents": [{ "parts": [{ "text": prompt }] }],
46+
"safetySettings": [
47+
{ "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE" },
48+
{ "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE" },
49+
{ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE" },
50+
{ "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE" }
51+
]
52+
})
53+
});
54+
55+
56+
// Errors:
57+
if (response.status === 429) {
58+
chrome.tabs.sendMessage(tabId, {
59+
type: 'analysisResult',
60+
stage: stage,
61+
content: 'You have exceeded the daily request limit for this model. Please try again tomorrow.'
62+
});
63+
return;
64+
}
65+
if (response.status === 503) {
66+
lastError = new Error(`Service Unavailable (503). The server might be temporarily overloaded.`);
67+
if (attempt < maxRetries) {
68+
console.warn(`Attempt ${attempt} failed. Retrying in 2 seconds...`);
69+
await sleep(2000);
70+
continue;
71+
}
72+
}
73+
if (!response.ok) {
74+
throw new Error(`API request failed with status ${response.status}`);
75+
}
76+
77+
78+
// response
79+
const data = await response.json();
80+
if (!data.candidates || !data.candidates[0] || !data.candidates[0].content || !data.candidates[0].content.parts || !data.candidates[0].content.parts[0].text) {
81+
if (data.candidates && data.candidates[0].finishReason === 'SAFETY') {
82+
throw new Error('Response was blocked for safety reasons.');
83+
}
84+
throw new Error('Invalid response structure from Gemini API.');
85+
}
86+
87+
// validContent response
88+
const validContent = data.candidates[0].content.parts[0].text;
89+
chrome.tabs.sendMessage(tabId, {
90+
type: 'analysisResult',
91+
stage: stage,
92+
content: validContent
93+
});
94+
return;
95+
96+
} catch (error) {
97+
lastError = error;
98+
console.error(`Gemini API call attempt ${attempt} failed:`, error);
99+
}
100+
}
101+
102+
chrome.tabs.sendMessage(tabId, {
103+
type: 'analysisResult',
104+
stage: stage,
105+
content: `Error: Failed to get analysis after ${maxRetries} attempts. ${lastError.message}`
106+
});
107+
}
108+
109+
110+
// ----- Prompts ------
111+
function generatePrompt(problem, stage) {
112+
const baseInstruction = `You are an expert DSA teacher helping a student understand a LeetCode problem.
113+
Always follow these rules:
114+
- Be beginner-friendly and short.
115+
- Do NOT over explain.
116+
- Use markdown format (### Headings, bullet points, numbered lists).
117+
- Focus only on the **Optimal Approach**.
118+
- Keep answers to-the-point and 4-6 lines max.
119+
- Avoid long paragraphs and stories.
120+
LeetCode Problem: ${problem}
121+
`;
122+
switch (stage) {
123+
case 'hints':
124+
return baseInstruction + ` ### 💡 Quick Hints
125+
Give only **2-3 hints**, each in **1 short sentence**:
126+
- Hint should guide the user without revealing the solution.
127+
- Avoid mentioning any algorithm or data structure directly.
128+
- Just help the user "think in the right direction".
129+
`;
130+
case 'approach':
131+
return baseInstruction + ` ### 🧠 Optimal Approach (Only)
132+
Describe the **optimal algorithm in 4 points max**:
133+
- What is the idea?
134+
- Why is this the best approach?
135+
- Which technique does it use (like two-pointer, hashmap etc)?
136+
- Why is it efficient?
137+
✅ Keep it under 4-5 lines.
138+
❌ Don't write code.
139+
`;
140+
case 'pseudo':
141+
return baseInstruction + ` ### 📋 Pseudo Code (Optimal Only)
142+
Write **simple pseudo-code** using English + code-style keywords.
143+
- Use plain steps like:
144+
- \`function()\`
145+
- \`if condition:\`
146+
- \`for each element in array:\`
147+
- Keep it **under 6 lines**, don't explain each line.
148+
✅ Format it using a markdown code block.
149+
`;
150+
case 'solution':
151+
return baseInstruction + ` ### ✅ JavaScript Code (Optimal Only)
152+
Give clean, **simple** JavaScript code in 10-15 lines max.
153+
- Add only **1-2 important comments**, no over commenting.
154+
- Use good variable names.
155+
- After code, write **a short 3-step explanation**.
156+
✅ Format the code in a markdown block \`\`\`js
157+
`;
158+
case 'complexity':
159+
return baseInstruction + ` ### ⏱ Complexity (Time & Space)
160+
Tell just:
161+
- **Time Complexity:** O(...)
162+
- **Space Complexity:** O(...)
163+
Then give a **2-3 line reason** for each. Don’t go into depth.
164+
✅ Keep it point wise, no paragraphs.
165+
`;
166+
default:
167+
return baseInstruction + "Give a short, helpful, pointwise explanation to guide the user.";
168+
}
169+
}

content_script.js

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// --------------- This script is injected into the LeetCode problem page. ------------------
2+
3+
// isUIRunning helps to only inject UI once.
4+
let isUIRunning = false;
5+
let panel;
6+
7+
function main() {
8+
if (isUIRunning) return;
9+
10+
// find area to inject button
11+
const targetArea = document.querySelector('div.flex.items-center.gap-4');
12+
if (targetArea) {
13+
const analyzeBtn = document.createElement('button');
14+
analyzeBtn.textContent = 'Analyze Problem';
15+
analyzeBtn.id = 'analyze-btn';
16+
analyzeBtn.onclick = togglePanel;
17+
targetArea.appendChild(analyzeBtn);
18+
19+
// inject panel
20+
fetch(chrome.runtime.getURL('panel.html'))
21+
.then(response => response.text())
22+
.then(html => {
23+
document.body.insertAdjacentHTML('beforeend', html);
24+
panel = document.getElementById('panel');
25+
setupPanelBtn();
26+
}).catch(err => console.error('Failed to load panel HTML:', err));
27+
28+
isUIRunning = true;
29+
}
30+
}
31+
32+
// toggle panel
33+
function togglePanel() {
34+
if (!panel) return;
35+
panel.classList.toggle('visible');
36+
}
37+
38+
// setup panel button
39+
function setupPanelBtn() {
40+
const closeBtn = document.getElementById('close-btn');
41+
closeBtn.addEventListener('click', togglePanel);
42+
43+
const revealBtn = document.querySelectorAll('.btn');
44+
revealBtn.forEach(button => {
45+
button.addEventListener('click', handleRevealBtn);
46+
});
47+
48+
const retryBtns = document.querySelectorAll('.btn-retry');
49+
retryBtns.forEach(button => {
50+
button.addEventListener('click', handleRevealBtn);
51+
});
52+
}
53+
54+
55+
// handle reveal button click
56+
function handleRevealBtn(event) {
57+
const button = event.target;
58+
const stage = button.dataset.stage;
59+
60+
let problemTitle = '';
61+
let problemDescription = '';
62+
63+
// find title using different methods
64+
const titleSelectors = [
65+
'.text-title-large a', 'div[data-cy="question-title"]', '.mr-2.text-label-1'
66+
];
67+
let title = null;
68+
for (const selector of titleSelectors) {
69+
title = document.querySelector(selector);
70+
if (title) {
71+
problemTitle = title.innerText;
72+
break;
73+
}
74+
}
75+
76+
// find description element
77+
const descSelectors = [
78+
'div[data-track-load="description_content"]',
79+
'div[class*="elfjS"]',
80+
'div.prose',
81+
'div[class^="description__"]',
82+
'div[class*="question-content"]'
83+
];
84+
let description = null;
85+
for (const selector of descSelectors) {
86+
description = document.querySelector(selector);
87+
if (description) {
88+
problemDescription = description.innerText;
89+
break;
90+
}
91+
}
92+
93+
if (!problemTitle || !problemDescription) {
94+
console.error("Error Info:", {
95+
titleFound: !!problemTitle,
96+
descriptionFound: !!problemDescription,
97+
url: window.location.href
98+
});
99+
alert("LeetCode Assistant Error: Could not find the problem title or description on the page. LeetCode may have updated its layout. Please check the browser console for more details.");
100+
101+
// try again
102+
const revealBtn = document.querySelector(`.btn[data-stage="${stage}"]`);
103+
if(revealBtn) {
104+
revealBtn.textContent = `Reveal ${stage.charAt(0).toUpperCase() + stage.slice(1)}`;
105+
revealBtn.disabled = false;
106+
}
107+
return;
108+
}
109+
110+
111+
const problemText = `Title: ${problemTitle}\n\nDescription:\n${problemDescription}`;
112+
113+
// --- hide retry and show loading ---
114+
const retryBtn = document.querySelector(`.btn-retry[data-stage="${stage}"]`);
115+
if (retryBtn) {
116+
retryBtn.style.display = 'none';
117+
}
118+
const revealBtn = document.querySelector(`.btn[data-stage="${stage}"]`);
119+
if (revealBtn) {
120+
revealBtn.textContent = 'Loading...';
121+
revealBtn.disabled = true;
122+
revealBtn.style.display = 'inline-block';
123+
}
124+
125+
// send request to background.js
126+
chrome.runtime.sendMessage({
127+
type: 'getAnalysis',
128+
stage: stage,
129+
problem: problemText
130+
});
131+
}
132+
133+
134+
// receive response from background.js script
135+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
136+
if (message.type === 'analysisResult') {
137+
const { stage, content } = message;
138+
139+
const contentDiv = document.getElementById(`${stage}-content`);
140+
const button = document.querySelector(`.btn[data-stage="${stage}"]`);
141+
const retryBtn = document.querySelector(`.btn-retry[data-stage="${stage}"]`);
142+
143+
if (contentDiv && button && retryBtn) {
144+
if (content.startsWith('Error:')) {
145+
contentDiv.innerHTML = `<p class="error-message">${content}</p>`;
146+
contentDiv.style.display = 'block';
147+
button.style.display = 'none';
148+
retryBtn.style.display = 'inline-block';
149+
} else {
150+
contentDiv.innerHTML = formatMarkdown(content);
151+
contentDiv.style.display = 'block';
152+
button.style.display = 'none';
153+
retryBtn.style.display = 'none';
154+
}
155+
}
156+
}
157+
});
158+
159+
160+
// --- markdown formatting ---
161+
function formatMarkdown(text) {
162+
let Text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
163+
const language = lang || 'plaintext';
164+
return `<pre><code class="language-${language}">${code.trim()}</code></pre>`;
165+
});
166+
167+
// Headings
168+
Text = Text.replace(/^### (.*$)/gim, '<h3>$1</h3>');
169+
Text = Text.replace(/^## (.*$)/gim, '<h2>$1</h2>');
170+
Text = Text.replace(/^# (.*$)/gim, '<h1>$1</h1>');
171+
172+
// Bold text
173+
Text = Text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
174+
175+
// Unordered list (grouped)
176+
Text = Text.replace(/(?:^|\n)[*-] (.*?)(?=\n|$)/g, (_, item) => `<li>${item}</li>`);
177+
Text = Text.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
178+
Text = Text.replace(/<\/ul>\s*<ul>/g, '');
179+
180+
// Ordered list (grouped)
181+
Text = Text.replace(/(?:^|\n)\d+\. (.*?)(?=\n|$)/g, (_, item) => `<li>${item}</li>`);
182+
Text = Text.replace(/(<li>[\s\S]*?<\/li>)/g, '<ol>$1</ol>');
183+
Text = Text.replace(/<\/ol>\s*<ol>/g, '');
184+
185+
// Paragraphs
186+
Text = Text.split('\n').map(p => {
187+
if (p.trim() === '' || p.startsWith('<h') || p.startsWith('<ul') || p.startsWith('<ol') || p.startsWith('<pre')) {
188+
return p;
189+
}
190+
return `<p>${p}</p>`;
191+
}).join('');
192+
193+
return Text;
194+
}
195+
196+
197+
// detect changes in the page
198+
const detect = new MutationObserver((mutations) => {
199+
if (window.location.href.includes('/problems/') && !document.getElementById('analyze-btn')) {
200+
// delay for load completely
201+
setTimeout(main, 1000);
202+
}
203+
});
204+
detect.observe(document.body, { childList: true, subtree: true });
205+
206+
207+
// run Extension
208+
setTimeout(main, 1500);

icons/icon128.png

79.9 KB
Loading

icons/icon16.png

30.9 KB
Loading

icons/icon48.png

358 KB
Loading

0 commit comments

Comments
 (0)