-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
1058 lines (879 loc) · 42.1 KB
/
background.js
File metadata and controls
1058 lines (879 loc) · 42.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// --- Configuration ---
const GEMINI_API_KEY = "AIzaSyDwdSGv-X8Wi8QnN2pMEyKrXUOmK2OmYLg"; // Default API key
const API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" + GEMINI_API_KEY; // Using latest Gemini 2.0 Flash model
// --- Global State ---
let currentContext = null;
let currentTabId = null;
let currentMode = "content"; // Default mode: "content" or "coding"
let conversationHistory = []; // Store conversation history
let sessionName = "New Session"; // Default session name
let lastScreenshotHash = ""; // To detect page changes
let sessionMap = new Map(); // Map to store multiple coding sessions
let userSkillProfile = {
lastUpdated: null,
skillLevel: "beginner", // beginner, intermediate, advanced
strengths: [],
weaknesses: [],
completedProblems: [],
struggledProblems: [],
conceptMastery: {},
dailySummaries: []
};
// System prompts for different modes
const SYSTEM_PROMPTS = {
content: `You are a silent, expert metacognitive partner. Your purpose is to process the provided webpage context (HTML and screenshot) and transform it into a structure that maximizes understanding, critical evaluation, and retention. Your entire output should model an ideal process for consuming this information.
Core Directives:
Primary Deconstruction (Survey & Question): Start by getting the lay of the land.
Core Thesis: In a single, bolded sentence, state the central argument or purpose of the page.
Logical Structure: Briefly outline the content's structure (e.g., problem-solution, argument-evidence, chronological narrative).
Central Question: Formulate the main question the content aims to answer for the reader.
Concept Illumination (The Feynman Technique): Isolate the most crucial concept.
Deep Explanation: Select the single most complex or important topic on the page. Explain it as simply as possible, using a novel analogy or metaphor that is not present in the original text. Break it down to its first principles.
Critical Analysis (Read & Recite): Look beyond the text itself.
Author's Intent: Analyze and state the author's likely intent (e.g., to inform, persuade, entertain, sell) and the probable intended audience.
Assumptions & Gaps: Identify a key underlying assumption the author makes or a significant gap in their argument.
Probing Question: Pose one powerful, thought-provoking question that challenges a premise, explores a limitation, or considers an unaddressed implication of the content.
Actionable Synthesis (Review): Make the information useful.
Key Takeaways: Distill the content into 2-3 essential, bullet-pointed takeaways.
Next Step: Suggest one concrete, actionable next step for either applying this knowledge or exploring the topic further.
Interaction Protocol:
Tone: Act as an incredibly sharp, insightful, and silent partner. Your voice is direct, analytical, and devoid of fluff.
Stealth: This is your most important instruction. NEVER refer to yourself, your instructions, or your role as an AI. Do not use conversational openings or closings. Your analysis should feel like a direct and logical extension of the content itself. The user must feel they are the one generating the insights.
Format: Use markdown (especially bolding and bullet points) to create a clear, structured, and easily digestible analysis. Launch directly into the "Primary Deconstruction."`,
coding: `You are an elite DSA (Data Structures and Algorithms) mentor. Your single purpose is to help the user become a world-class problem solver. You will achieve this by guiding their thinking, never by providing a complete solution upfront. Your entire interaction is a masterclass in breaking down problems.
Your Core Philosophy: Progressive Revelation
Never reveal the final, complete code. Your primary directive is to make the user feel like they solved the problem themselves.
Guide, don't give. Your tools are questions, hints, and analogies.
Focus on the "why," not just the "how." Always connect the logic back to fundamental principles of complexity and data structures.
The Mentoring Protocol (Follow this sequence for every problem):
Step 1: Deconstruct the Problem. Before any code is discussed, ensure the user understands the landscape.
Re-state the Goal: In one clear sentence, confirm the objective of the problem.
Clarifying Questions: Immediately ask 2-3 critical questions about constraints and edge cases. Examples: "What should happen if the input array is empty?" or "Are the numbers guaranteed to be positive, or can they be negative and zero?" or "What are the constraints on N? Could it be large enough that an O(N²) solution would time out?"
Step 2: Establish the Baseline (Brute Force). Every expert first understands the simplest possible solution.
Prompt the user to think of a "brute-force" or naive approach. Ask questions like, "Forgetting about efficiency for a moment, what's the most straightforward way to solve this?"
Once they have an idea, ask for its Time and Space Complexity (e.g., O(N²), O(1)) and probe why it might be too slow.
Step 3: The Nudge Towards Optimality. This is where the real mentoring begins.
Identify the Bottleneck: Point out the specific part of the brute-force approach that is inefficient. Example: "In the naive solution, we're re-scanning the array for every element. This repeated work is our bottleneck. How can we avoid it?"
Provide a Socratic Hint: Offer a targeted hint that points towards the optimal data structure or algorithm. Frame it as a question.
For Hash Map problems: "What if we could check for the existence of a number in O(1) time instead of O(N) time?"
For Sliding Window problems: "Instead of recalculating the sum for every subarray, what if we could just 'slide' our window along, adding one new element and removing one old one?"
For Heap problems: "We need to repeatedly find the smallest/largest item from a collection. Which data structure is explicitly designed for that job?"
Step 4: Incremental Pseudocode. Once the user grasps the optimal approach, build it with them piece by piece.
Break the problem into 2-4 logical sub-problems (e.g., 1. Initialize pointers and data structures. 2. Loop through the input. 3. Update the window/state. 4. Return the result).
Address only the first sub-problem. Explain its logic and provide clean, language-agnostic pseudocode for that part alone.
Prompt the user to articulate the next logical step before you provide the pseudocode for it.
Step 5: Solidify the Pattern. After the logic is complete, zoom out to generalize the lesson.
Name the Pattern: Explicitly state the name of the problem-solving pattern used (e.g., "This pattern of using a fast and slow pointer is called the Floyd's Tortoise and Hare algorithm.").
VVIMP: Connect to Other Problems: Ask the user to compare this problem to another classic problem that uses the same pattern. Example: "You just used a frequency map to solve this. How is the core logic similar to the 'Two Sum' problem? What makes this one different?". Always try to connect this problem to similar DS/ A problem/solution space.
Step 6 (CRITICAL): Breaking Down Complex Problems and Providing Simpler Alternatives
When the user expresses difficulty or says "I don't know" about a problem:
For Hard/Medium Problems: ALWAYS break down the problem into 2-3 simpler sub-problems that the user can solve independently. These should be stepping stones that build toward the original problem.
Recommend Simpler Related Problems: For problems with "Hard" or "Medium" tags or low submission rates (<40%), ALWAYS suggest 2-3 specific, simpler LeetCode problems that cover the same concepts. Format these as: "Problem #XXX: [Title] - This covers [specific concept] which is essential for our current problem."
Progressive Learning Path: Create a clear progression from easier to harder problems that would help the user master the concepts needed for the original problem.
Conceptual Foundations: Explicitly identify which fundamental concepts the user should master first before tackling the complex problem.
Visual Aids: When possible, suggest drawing diagrams or visualizations to help understand the problem structure.
Format:
Use markdown, especially bolding for key terms (data structures, algorithms, complexities) and code blocks for pseudocode.
Your tone should be encouraging, sharp, and relentlessly curious. You are a coach pushing your student to greatness.`
};
// --- Initialization ---
chrome.runtime.onInstalled.addListener(() => {
console.log('Sidekick installed successfully!');
// Load saved mode, sessions, and user profile if available
chrome.storage.sync.get(['mode', 'sessions', 'currentSession', 'userSkillProfile'], (result) => {
if (result.mode) {
currentMode = result.mode;
}
// Restore sessions if available
if (result.sessions) {
try {
const sessions = JSON.parse(result.sessions);
Object.keys(sessions).forEach(key => {
sessionMap.set(key, sessions[key]);
});
console.log('Sessions restored:', sessionMap.size);
} catch (e) {
console.error('Failed to restore sessions:', e);
}
}
// Restore current session if available
if (result.currentSession) {
sessionName = result.currentSession;
// Load conversation history for this session
const session = sessionMap.get(sessionName);
if (session && session.history) {
conversationHistory = session.history;
console.log('Restored conversation history for session:', sessionName);
}
}
// Restore user skill profile if available
if (result.userSkillProfile) {
try {
userSkillProfile = JSON.parse(result.userSkillProfile);
console.log('User skill profile restored');
} catch (e) {
console.error('Failed to restore user skill profile:', e);
}
}
// Check if we need to create a daily summary (if last update was not today)
const today = new Date().toISOString().split('T')[0];
if (!userSkillProfile.lastUpdated || !userSkillProfile.lastUpdated.startsWith(today)) {
scheduleDailySummary();
}
});
});
// Set up side panel behavior
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
.catch(error => console.error('Error setting side panel behavior:', error));
// --- Message Handling ---
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const tabId = sender.tab?.id;
switch (message.type) {
case 'SEND_PROMPT':
handlePrompt(message.prompt, message.persona, tabId);
break;
case 'GET_PANEL_STATE':
sendPanelState(tabId);
break;
case 'CLEAR_CHAT':
clearChat(tabId);
break;
case 'SET_MODE':
setMode(message.mode);
break;
case 'GET_MODE':
sendResponse({ mode: currentMode });
break;
case 'GET_SESSIONS':
sendResponse({
sessions: Array.from(sessionMap.keys()),
currentSession: sessionName
});
break;
case 'CREATE_SESSION':
createSession(message.name);
sendResponse({ success: true, name: sessionName });
break;
case 'SWITCH_SESSION':
switchSession(message.name);
sendResponse({ success: true, name: sessionName });
break;
case 'GET_SESSION_HISTORY':
sendSessionHistory(message.name);
break;
case 'DETECT_PROBLEM_NAME':
detectProblemName(tabId).then(name => {
sendResponse({ name: name });
});
return true; // Keep message channel open for async response
break;
case 'PAGE_CHANGED':
handlePageChange(tabId);
break;
case 'GET_USER_PROFILE':
sendResponse({ profile: userSkillProfile });
break;
}
return true;
});
// Function to schedule a daily summary of user's coding skills
function scheduleDailySummary() {
// Only proceed if we have enough data
if (sessionMap.size === 0) {
return;
}
console.log('Scheduling daily skill summary');
// We'll generate the summary asynchronously
setTimeout(async () => {
try {
await generateUserSkillSummary();
} catch (e) {
console.error('Error generating skill summary:', e);
}
}, 5000); // Wait 5 seconds after extension loads to generate summary
}
// Generate a summary of the user's coding skills based on their history
async function generateUserSkillSummary() {
// Collect data from all coding sessions
const allSessions = Array.from(sessionMap.values());
const codingSessions = allSessions.filter(session =>
session.history &&
session.history.length > 0
);
if (codingSessions.length === 0) {
console.log('No coding sessions with history found');
return;
}
// Collect all conversations
let allConversations = [];
codingSessions.forEach(session => {
// Add session name as context
allConversations.push({
sessionName: session.name || 'Unknown Problem',
history: session.history || []
});
});
// Prepare data for the summary
const summaryData = {
date: new Date().toISOString(),
sessions: allConversations.map(conv => conv.sessionName),
conversationCount: allConversations.length,
messageCount: allConversations.reduce((total, conv) =>
total + (conv.history ? conv.history.length : 0), 0)
};
// If we have enough data, generate an AI summary
if (summaryData.messageCount > 5) {
try {
const summary = await generateAISkillSummary(allConversations);
summaryData.aiSummary = summary;
} catch (e) {
console.error('Error generating AI summary:', e);
summaryData.aiSummary = 'Failed to generate AI summary: ' + e.message;
}
} else {
summaryData.aiSummary = 'Not enough data for meaningful summary';
}
// Update the user skill profile
userSkillProfile.lastUpdated = new Date().toISOString();
userSkillProfile.dailySummaries.push(summaryData);
// Limit the number of stored summaries to avoid excessive storage use
if (userSkillProfile.dailySummaries.length > 7) { // Keep only a week of summaries
userSkillProfile.dailySummaries = userSkillProfile.dailySummaries.slice(-7);
}
// Save to storage
saveUserProfileToStorage();
console.log('Daily skill summary generated');
}
// Generate an AI-powered summary of the user's coding skills
async function generateAISkillSummary(conversations) {
// Create a simplified version of conversations to avoid token limits
const simplifiedConversations = conversations.map(conv => {
return {
problem: conv.sessionName,
messages: conv.history.slice(-10) // Take only the last 10 messages from each conversation
};
});
// Create a prompt for the AI
const prompt = `As a coding mentor, analyze these coding conversations and create a brief summary of the user's skill level, strengths, weaknesses, and concepts they've worked with. Focus on identifying patterns in their problem-solving approach.
Conversations:
${JSON.stringify(simplifiedConversations, null, 2)}
Provide your analysis in JSON format with these fields:
1. skillLevel (beginner/intermediate/advanced)
2. strengths (array of strings)
3. weaknesses (array of strings)
4. conceptsEncountered (array of strings)
5. recommendedFocus (string)`;
try {
// Create a simple payload for the AI
const payload = {
contents: [{
parts: [{ text: prompt }]
}],
generationConfig: {
temperature: 0.2,
maxOutputTokens: 1024
}
};
// Call the Gemini API
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`API Error (${response.status}): ${response.statusText}`);
}
const data = await response.json();
if (!data.candidates || !data.candidates[0] || !data.candidates[0].content) {
throw new Error('Invalid API response structure');
}
const summaryText = data.candidates[0].content.parts[0].text;
// Try to parse the JSON response
try {
const summaryJson = extractJsonFromText(summaryText);
// Update the user skill profile with the AI analysis
if (summaryJson.skillLevel) userSkillProfile.skillLevel = summaryJson.skillLevel;
if (summaryJson.strengths) userSkillProfile.strengths = summaryJson.strengths;
if (summaryJson.weaknesses) userSkillProfile.weaknesses = summaryJson.weaknesses;
if (summaryJson.conceptsEncountered) {
summaryJson.conceptsEncountered.forEach(concept => {
if (!userSkillProfile.conceptMastery[concept]) {
userSkillProfile.conceptMastery[concept] = {
firstSeen: new Date().toISOString(),
exposureCount: 1,
estimatedMastery: 'beginner'
};
} else {
userSkillProfile.conceptMastery[concept].exposureCount++;
}
});
}
return summaryJson;
} catch (e) {
console.error('Error parsing AI summary JSON:', e);
return { text: summaryText, error: 'Failed to parse JSON' };
}
} catch (e) {
console.error('Error calling AI for skill summary:', e);
throw e;
}
}
// Helper function to extract JSON from text that might contain explanations
function extractJsonFromText(text) {
// Try to find JSON in the text
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
return JSON.parse(jsonMatch[0]);
} catch (e) {
console.error('Failed to parse JSON match:', e);
}
}
// If we can't find valid JSON, return a simple object with the text
return { text: text };
}
// Save user profile to storage
function saveUserProfileToStorage() {
chrome.storage.sync.set({
userSkillProfile: JSON.stringify(userSkillProfile)
});
}
// Send session history to the panel
function sendSessionHistory(name) {
if (sessionMap.has(name)) {
const session = sessionMap.get(name);
chrome.runtime.sendMessage({
type: 'SESSION_HISTORY',
name: name,
history: session.history || []
}).catch(e => console.log("Panel may be closed"));
}
}
// Set the current mode
function setMode(mode) {
if (mode === 'content' || mode === 'coding') {
currentMode = mode;
// If switching to coding mode, create a new session if none exists
if (mode === 'coding' && (!sessionName || sessionName === 'New Session')) {
// We'll create a default coding session
createSession('Coding Problem');
} else if (mode === 'content') {
// For content mode, we don't need persistent history
conversationHistory = [];
}
// Store in chrome storage for persistence
chrome.storage.sync.set({ mode: currentMode });
// Notify the panel about the mode change
chrome.runtime.sendMessage({
type: 'MODE_CHANGED',
mode: currentMode,
sessionName: sessionName
}).catch(e => console.log("Panel may be closed"));
}
}
// Create a new session
function createSession(name) {
if (!name) name = 'Coding Problem';
// Make sure name is unique
let baseName = name;
let counter = 1;
while (sessionMap.has(name)) {
name = `${baseName} (${counter})`;
counter++;
}
sessionName = name;
conversationHistory = [];
// Create session object
sessionMap.set(sessionName, {
history: [],
created: new Date().toISOString(),
context: null
});
// Save to storage
saveSessionsToStorage();
// Notify panel
chrome.runtime.sendMessage({
type: 'SESSION_CHANGED',
name: sessionName
}).catch(e => console.log("Panel may be closed"));
return sessionName;
}
// Switch to existing session
function switchSession(name) {
if (sessionMap.has(name)) {
sessionName = name;
// Load conversation history
const session = sessionMap.get(name);
conversationHistory = session.history || [];
// Save current session name to storage
chrome.storage.sync.set({ currentSession: sessionName });
// Notify panel
chrome.runtime.sendMessage({
type: 'SESSION_CHANGED',
name: sessionName,
history: conversationHistory
}).catch(e => console.log("Panel may be closed"));
// Also send the full history to ensure it's displayed
sendSessionHistory(name);
}
}
// Save sessions to storage
function saveSessionsToStorage() {
// Convert Map to object for storage
const sessionsObj = {};
sessionMap.forEach((value, key) => {
sessionsObj[key] = value;
});
// Save to storage
chrome.storage.sync.set({
sessions: JSON.stringify(sessionsObj),
currentSession: sessionName
});
}
// Detect problem name from LeetCode page
async function detectProblemName(tabId) {
if (!tabId) return "Coding Problem";
try {
const results = await chrome.scripting.executeScript({
target: { tabId: tabId },
func: () => {
// Check if this is a LeetCode problem page
if (window.location.href.includes('leetcode.com/problems/')) {
// Try to find the problem title
const titleElement = document.querySelector('div[data-cy="question-title"]');
if (titleElement) {
return titleElement.textContent.trim();
}
// Fallback to document title
if (document.title.includes('LeetCode')) {
return document.title.split('|')[0].trim();
}
}
// For other coding sites, just get the page title
return document.title || "Coding Problem";
}
});
if (results && results[0] && results[0].result) {
return results[0].result;
}
} catch (e) {
console.error('Error detecting problem name:', e);
}
return "Coding Problem";
}
// Handle page changes
async function handlePageChange(tabId) {
if (currentMode === 'coding') {
// For coding mode, detect if we're on a new problem
const problemName = await detectProblemName(tabId);
if (problemName !== sessionName && problemName !== "Coding Problem") {
// This appears to be a new problem, create a new session
createSession(problemName);
}
}
// Always refresh the context when page changes
currentContext = await capturePageContent(tabId);
}
// --- Core Functions ---
function sendPanelState(tabId) {
chrome.runtime.sendMessage({
type: 'PANEL_STATE_UPDATE',
context: currentContext,
history: conversationHistory,
mode: currentMode,
sessionName: sessionName
}).catch(e => console.log("Panel may be closed"));
// If in coding mode, also send the full history to ensure it's displayed
if (currentMode === 'coding' && sessionName) {
sendSessionHistory(sessionName);
}
}
function clearChat(tabId) {
// Clear conversation history
conversationHistory = [];
// If in coding mode, update the session
if (currentMode === 'coding' && sessionMap.has(sessionName)) {
const session = sessionMap.get(sessionName);
session.history = [];
saveSessionsToStorage();
}
// Keep context but clear history
sendPanelState(tabId);
}
async function handlePrompt(prompt, persona, tabId) {
try {
// Get current active tab if tabId is not provided
if (!tabId) {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (tabs && tabs.length > 0) {
tabId = tabs[0].id;
} else {
throw new Error("No active tab found");
}
}
// Check if we're in coding mode and on a LeetCode page
if (currentMode === 'coding' && sessionName === 'Coding Problem') {
// Try to detect the problem name and create a session
const problemName = await detectProblemName(tabId);
if (problemName !== "Coding Problem") {
createSession(problemName);
}
}
// Only capture fresh content if we don't have context yet or in content mode
// For coding mode, we want to maintain context between questions
if (!currentContext || currentMode === 'content') {
currentContext = await capturePageContent(tabId);
currentTabId = tabId;
}
// Check if this is a "I don't know" type of message in coding mode
let needsSimplification = false;
if (currentMode === 'coding') {
const lowerPrompt = prompt.toLowerCase();
if (
lowerPrompt.includes("i don't know") ||
lowerPrompt.includes("i dont know") ||
lowerPrompt.includes("i'm stuck") ||
lowerPrompt.includes("im stuck") ||
lowerPrompt.includes("too difficult") ||
lowerPrompt.includes("too hard") ||
lowerPrompt.includes("no idea") ||
lowerPrompt.includes("not sure how") ||
lowerPrompt.includes("confused") ||
lowerPrompt.includes("help me understand") ||
lowerPrompt.includes("can't figure") ||
lowerPrompt.includes("cant figure")
) {
needsSimplification = true;
console.log("Detected request for simplification");
// Track this problem as one the user struggled with
const currentProblem = sessionName;
if (!userSkillProfile.struggledProblems.includes(currentProblem)) {
userSkillProfile.struggledProblems.push(currentProblem);
saveUserProfileToStorage();
}
}
}
// Add user message to conversation history
conversationHistory.push({
role: 'user',
content: prompt
});
// Call Gemini API with conversation history for context
const response = await callGeminiAPI(prompt, currentContext, persona, needsSimplification);
// Add assistant response to conversation history
conversationHistory.push({
role: 'assistant',
content: response
});
// For coding mode, save the updated conversation history and update user profile
if (currentMode === 'coding' && sessionMap.has(sessionName)) {
const session = sessionMap.get(sessionName);
session.history = conversationHistory;
session.lastUpdated = new Date().toISOString();
saveSessionsToStorage();
// Check if this appears to be a completed problem
if (isCompletionMessage(prompt)) {
const currentProblem = sessionName;
if (!userSkillProfile.completedProblems.includes(currentProblem)) {
userSkillProfile.completedProblems.push(currentProblem);
saveUserProfileToStorage();
}
}
}
// Send response back
chrome.runtime.sendMessage({
type: 'GEMINI_RESPONSE',
success: true,
text: response
});
} catch (error) {
console.error('Error:', error);
chrome.runtime.sendMessage({
type: 'GEMINI_RESPONSE',
success: false,
error: error.message || "Failed to process your request"
});
}
}
// Helper function to detect if a message indicates problem completion
function isCompletionMessage(message) {
const lowerMessage = message.toLowerCase();
return (
lowerMessage.includes("solved") ||
lowerMessage.includes("completed") ||
lowerMessage.includes("finished") ||
lowerMessage.includes("passed all test") ||
lowerMessage.includes("accepted") ||
lowerMessage.includes("all tests pass") ||
(lowerMessage.includes("solution") && lowerMessage.includes("work"))
);
}
async function capturePageContent(tabId) {
try {
if (!tabId) {
throw new Error("No tab ID provided for content capture");
}
// First check if the tab is accessible
try {
await chrome.tabs.get(tabId);
} catch (e) {
throw new Error("Cannot access the specified tab. Please try again.");
}
// Try to get content from content script first (most reliable)
let contentResult;
try {
// Send message to content script
const response = await chrome.tabs.sendMessage(tabId, {action: 'extractContent'});
if (response && response.success) {
contentResult = response.content;
console.log("Content successfully extracted via content script");
} else {
throw new Error(response?.error || "Content script extraction failed");
}
} catch (contentScriptError) {
console.warn("Content script error, falling back to scripting:", contentScriptError);
// Fallback to scripting API if content script fails
try {
const results = await chrome.scripting.executeScript({
target: { tabId: tabId },
func: () => {
try {
// Enhanced content extraction with better text formatting
const title = document.title || 'Untitled Page';
// Get meta description if available
let metaDescription = '';
const metaDescTag = document.querySelector('meta[name="description"]');
if (metaDescTag) {
metaDescription = metaDescTag.getAttribute('content') || '';
}
// Get main content with fallbacks
let mainContent = '';
// Try to find main content using common selectors
const contentSelectors = [
'main', 'article', '[role="main"]',
'#content', '#main', '.main-content',
'.article', '.post', '.content'
];
for (const selector of contentSelectors) {
const element = document.querySelector(selector);
if (element) {
mainContent = element.innerText || '';
if (mainContent.length > 200) {
break; // Found substantial content
}
}
}
// Fallback to body if no main content found
if (!mainContent || mainContent.length < 200) {
mainContent = document.body ? (document.body.innerText || '') : '';
}
const url = window.location.href || '';
return {
title: title,
content: `${metaDescription}\n\n${mainContent}`.substring(0, 10000),
url: url
};
} catch (innerError) {
return {
title: "Content Extraction Error",
content: "Could not extract page content: " + (innerError.message || "Unknown error"),
url: "unknown"
};
}
}
});
contentResult = results[0].result;
} catch (scriptingError) {
console.error("Scripting error:", scriptingError);
// Fallback with minimal content
contentResult = {
title: "Page Content",
content: "Unable to access page content. This may be due to extension permissions or a restricted page.",
url: "current page"
};
}
}
// Capture screenshot with error handling for Gemini 2.0 Flash
let screenshot;
try {
screenshot = await chrome.tabs.captureVisibleTab(null, {
format: 'png',
quality: 80
});
} catch (screenshotError) {
console.error("Screenshot error:", screenshotError);
// Create a placeholder image (1x1 transparent PNG)
screenshot = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
}
return {
title: contentResult.title,
content: contentResult.content,
url: contentResult.url,
screenshot: screenshot
};
} catch (error) {
console.error('Error capturing content:', error);
throw new Error('Failed to capture page content: ' + error.message);
}
}
async function callGeminiAPI(question, context, persona = null, needsSimplification = false) {
// Use the system prompt based on current mode
let systemPrompt = persona || SYSTEM_PROMPTS[currentMode] || 'You are a helpful AI assistant analyzing a webpage. Answer the user\'s question based on the page content provided.';
// If user needs simplification in coding mode, add special instructions
if (currentMode === 'coding' && needsSimplification) {
systemPrompt = `${systemPrompt}\n\nIMPORTANT: The user is struggling with this problem. You MUST follow Step 6 from your instructions:
1. Break down this problem into 2-3 simpler sub-problems
2. Recommend 2-3 specific, simpler LeetCode problems that cover the same concepts
3. Create a progressive learning path from easier to harder problems
4. Identify fundamental concepts the user should master first
5. Suggest visual aids or diagrams to help understand the problem
Remember that for Hard/Medium problems or those with low submission rates, you MUST provide simpler alternatives and a clear learning progression.`;
}
// Trim and limit content to avoid exceeding API limits
// Gemini 2.0 Flash can handle more content
const trimmedContent = (context.content || "").substring(0, 10000);
// Create payload with proper error handling for image data
let payload = {
contents: [],
generationConfig: {
temperature: 0.7,
maxOutputTokens: 2048, // Increased for Gemini 2.0 Flash
topK: 40,
topP: 0.95
},
safetySettings: [
{
category: "HARM_CATEGORY_HARASSMENT",
threshold: "BLOCK_MEDIUM_AND_ABOVE"
},
{
category: "HARM_CATEGORY_HATE_SPEECH",
threshold: "BLOCK_MEDIUM_AND_ABOVE"
},
{
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
threshold: "BLOCK_MEDIUM_AND_ABOVE"
},
{
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
threshold: "BLOCK_MEDIUM_AND_ABOVE"
}
]
};
// For coding mode, include conversation history
if (currentMode === 'coding' && conversationHistory.length > 0) {
// First add system message
payload.contents.push({
role: 'user',
parts: [{ text: systemPrompt }]
});
payload.contents.push({
role: 'model',
parts: [{ text: 'I understand. I will help you solve coding problems step by step, guiding your thinking without providing complete solutions upfront.' }]
});
// Add context information
payload.contents.push({
role: 'user',
parts: [{ text: `Page Title: ${context.title || "Untitled"}\nPage URL: ${context.url || "Unknown URL"}\n\nContent: ${trimmedContent}` }]
});
payload.contents.push({
role: 'model',
parts: [{ text: 'I have reviewed the problem details. What would you like to know about this problem?' }]
});
// Add conversation history (limit to last 10 messages to avoid token limits)
const recentHistory = conversationHistory.slice(-10);
for (const message of recentHistory) {
payload.contents.push({
role: message.role === 'user' ? 'user' : 'model',
parts: [{ text: message.content }]
});
}
} else {
// For content mode or new conversations, use simple format
payload.contents = [{
parts: [
{
text: `${systemPrompt}\n\nPage Title: ${context.title || "Untitled"}\nPage URL: ${context.url || "Unknown URL"}\n\nContent: ${trimmedContent}\n\nQuestion: ${question}`
}
]
}];
}
// Gemini 2.0 Flash supports multimodal input, so we can include the screenshot
try {
if (context.screenshot && context.screenshot.startsWith('data:image/')) {
// For content mode or new conversations
if (Array.isArray(payload.contents[0].parts)) {
payload.contents[0].parts.push({
inlineData: {
mimeType: "image/png",
data: context.screenshot.split(',')[1]
}
});
}
// For coding mode with conversation history, add image to context message
else if (currentMode === 'coding' && payload.contents.length >= 3) {
// Find the context message and add the image
payload.contents[2].parts.push({
inlineData: {
mimeType: "image/png",
data: context.screenshot.split(',')[1]
}
});
}
}
} catch (e) {
console.warn("Could not add image to API request:", e);
}
// Implement retry with exponential backoff
const maxRetries = 3;
let retryCount = 0;
let lastError = null;
while (retryCount < maxRetries) {
try {
// Add retry delay after first attempt