browser-assistant 0.0.0-stage → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -3
- package/extension/answer-requests.js +58 -0
- package/extension/automation-policy.js +617 -0
- package/extension/background.js +284 -0
- package/extension/coding-automation.js +209 -0
- package/extension/coding-batch.js +187 -0
- package/extension/content.js +2765 -0
- package/extension/editor-bridge.js +313 -0
- package/extension/journal-automation.js +559 -0
- package/extension/manifest.json +44 -0
- package/extension/popup.html +61 -0
- package/extension/popup.js +19 -0
- package/extension/question-visuals.js +82 -0
- package/extension/styles.css +617 -0
- package/package.json +49 -4
- package/server/.env.example +19 -0
- package/server/code_contract.py +136 -0
- package/server/journal.py +109 -0
- package/server/journal_prompt.md +425 -0
- package/server/journal_syllabus.json +80 -0
- package/server/local_security.py +47 -0
- package/server/main.py +1764 -0
- package/server/practice_quiz.html +56 -0
- package/server/requirements.lock.txt +423 -0
- package/server/requirements.txt +9 -0
- package/server/vision_images.py +132 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Background script for Browser Assistant
|
|
2
|
+
importScripts('editor-bridge.js');
|
|
3
|
+
// Local unpacked development updates: only reload when the files in Chrome's
|
|
4
|
+
// own extension folder match the workspace release, and every running task is idle.
|
|
5
|
+
let updateCheckRunning = false;
|
|
6
|
+
async function checkWorkspaceUpdate() {
|
|
7
|
+
if (updateCheckRunning)
|
|
8
|
+
return;
|
|
9
|
+
updateCheckRunning = true;
|
|
10
|
+
try {
|
|
11
|
+
const release = await fetch('http://127.0.0.1:8000/extension-release', { cache: 'no-store', signal: AbortSignal.timeout(4000) }).then(r => r.ok ? r.json() : null);
|
|
12
|
+
const current = chrome.runtime.getManifest().version;
|
|
13
|
+
if (!release?.version || release.version === current)
|
|
14
|
+
return;
|
|
15
|
+
const disk = await fetch(chrome.runtime.getURL('manifest.json'), { cache: 'no-store' }).then(r => r.json());
|
|
16
|
+
// Never loop reloading an old ZIP extraction. Load the workspace's
|
|
17
|
+
// extension directory once to enable local file updates.
|
|
18
|
+
if (disk.version !== release.version)
|
|
19
|
+
return;
|
|
20
|
+
const tabs = (await chrome.tabs.query({})).filter(tab => /^https?:\/\//.test(tab.url || ''));
|
|
21
|
+
const states = await Promise.all(tabs.map(async (tab) => {
|
|
22
|
+
try {
|
|
23
|
+
return { tab, state: await chrome.tabs.sendMessage(tab.id, { action: 'assistantUpdateStatus' }) };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { tab, state: null };
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
29
|
+
if (states.some(item => item.state?.busy))
|
|
30
|
+
return;
|
|
31
|
+
for (const { tab, state } of states) {
|
|
32
|
+
if (!state)
|
|
33
|
+
continue;
|
|
34
|
+
const prepared = await chrome.tabs.sendMessage(tab.id, { action: 'assistantPrepareUpdate' });
|
|
35
|
+
if (!prepared?.ready) {
|
|
36
|
+
await attachUpdatedPanels();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
chrome.runtime.reload();
|
|
41
|
+
}
|
|
42
|
+
catch { /* Offline server or restricted tab: retry on the next alarm. */ }
|
|
43
|
+
finally {
|
|
44
|
+
updateCheckRunning = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function attachUpdatedPanels() {
|
|
48
|
+
const manifest = chrome.runtime.getManifest();
|
|
49
|
+
const tabs = (await chrome.tabs.query({})).filter(tab => /^https?:\/\//.test(tab.url || ''));
|
|
50
|
+
for (const tab of tabs) {
|
|
51
|
+
try {
|
|
52
|
+
const existing = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => Boolean(document.getElementById('assistant-root')) });
|
|
53
|
+
if (existing[0]?.result)
|
|
54
|
+
continue;
|
|
55
|
+
for (const group of manifest.content_scripts) {
|
|
56
|
+
await chrome.scripting.insertCSS({ target: { tabId: tab.id }, files: group.css || [] });
|
|
57
|
+
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: group.js });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch { /* Chrome may restrict particular sites; leave them alone. */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
chrome.alarms.create('assistant-workspace-update', { periodInMinutes: 1 });
|
|
64
|
+
chrome.alarms.onAlarm.addListener(alarm => { if (alarm.name === 'assistant-workspace-update')
|
|
65
|
+
void checkWorkspaceUpdate(); });
|
|
66
|
+
chrome.runtime.onInstalled.addListener(() => { void attachUpdatedPanels(); });
|
|
67
|
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
68
|
+
if (!request || typeof request.action !== 'string') {
|
|
69
|
+
sendResponse({ error: 'Invalid extension message' });
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (request.action === 'questionImage') {
|
|
73
|
+
let url;
|
|
74
|
+
try {
|
|
75
|
+
url = new URL(request.url);
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
if (sender.id !== chrome.runtime.id || !sender.tab || !/^https?:/.test(sender.url || '')
|
|
79
|
+
|| !url || !['https:', 'http:'].includes(url.protocol) || url.username || url.password) {
|
|
80
|
+
sendResponse({ error: 'Question image access is not authorized.' });
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
(async () => {
|
|
84
|
+
const response = await fetch(url.href, { credentials: 'omit', signal: AbortSignal.timeout(10000) });
|
|
85
|
+
if (!response.ok || !response.headers.get('content-type')?.startsWith('image/'))
|
|
86
|
+
throw new Error('Question image download failed.');
|
|
87
|
+
if (Number(response.headers.get('content-length')) > 10000000)
|
|
88
|
+
throw new Error('Question image is too large.');
|
|
89
|
+
const reader = response.body.getReader();
|
|
90
|
+
const parts = [];
|
|
91
|
+
let size = 0;
|
|
92
|
+
while (true) {
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done)
|
|
95
|
+
break;
|
|
96
|
+
size += value.length;
|
|
97
|
+
if (size > 10000000) {
|
|
98
|
+
await reader.cancel();
|
|
99
|
+
throw new Error('Question image is too large.');
|
|
100
|
+
}
|
|
101
|
+
parts.push(value);
|
|
102
|
+
}
|
|
103
|
+
let binary = '';
|
|
104
|
+
for (const part of parts)
|
|
105
|
+
for (let i = 0; i < part.length; i += 8192)
|
|
106
|
+
binary += String.fromCharCode(...part.subarray(i, i + 8192));
|
|
107
|
+
return { dataUrl: `data:${response.headers.get('content-type').split(';')[0]};base64,${btoa(binary)}` };
|
|
108
|
+
})().then(sendResponse).catch(error => sendResponse({ error: error.message }));
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (request.action === 'mcqSnippets') {
|
|
112
|
+
let allowed = false;
|
|
113
|
+
// Chrome's document URL may retain the route where this SPA first
|
|
114
|
+
// loaded. This read-only bridge must also work after in-page navigation
|
|
115
|
+
// and on review routes. Keep the exact portal origin and sender checks;
|
|
116
|
+
// use the tab URL only for a top frame with no document URL.
|
|
117
|
+
try {
|
|
118
|
+
const topFrame = sender.frameId === undefined || sender.frameId === 0;
|
|
119
|
+
const url = new URL(sender.url || (topFrame ? sender.tab?.url : ''));
|
|
120
|
+
allowed = url.origin === 'https://learning.ccbp.in';
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
if (!sender.tab || sender.id !== chrome.runtime.id || !allowed) {
|
|
124
|
+
sendResponse({ ok: false, error: 'Full snippet access is unavailable on this page/frame.' });
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
chrome.scripting.executeScript({ target: { tabId: sender.tab.id, frameIds: [sender.frameId || 0] }, world: 'MAIN', func: browserAssistantReadMcqSnippets })
|
|
128
|
+
.then(results => sendResponse(results[0]?.result || { ok: false, error: results[0]?.error?.message || 'The snippet reader returned no result. The page may still be loading.' }))
|
|
129
|
+
.catch(error => sendResponse({ ok: false, error: error.message }));
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if (request.action === 'orderingDrag') {
|
|
133
|
+
if (!sender.tab || sender.id !== chrome.runtime.id || !sender.url?.startsWith('https://learning.ccbp.in/')) {
|
|
134
|
+
sendResponse({ ok: false });
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
chrome.scripting.executeScript({
|
|
138
|
+
target: { tabId: sender.tab.id, frameIds: [sender.frameId || 0] }, world: 'MAIN',
|
|
139
|
+
func: browserAssistantOrderingDrag, args: [request.prompt, request.options, request.from, request.to],
|
|
140
|
+
}).then(results => sendResponse(results[0]?.result || { ok: false }))
|
|
141
|
+
.catch(error => sendResponse({ ok: false, error: error.message }));
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (['focusOnlineIde', 'runOnlineIde', 'onlineIdeStatus'].includes(request.action)) {
|
|
145
|
+
const questionPath = /^\/question\/([a-f0-9-]{36})\/?$/i;
|
|
146
|
+
let questionId;
|
|
147
|
+
try {
|
|
148
|
+
const page = new URL(sender.tab.url || sender.url);
|
|
149
|
+
const source = new URL(sender.url);
|
|
150
|
+
if (page.origin === 'https://learning.ccbp.in' && source.origin === page.origin) {
|
|
151
|
+
questionId = page.pathname.match(questionPath)?.[1];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch { /* Only a portal question tab can request its IDE. */ }
|
|
155
|
+
if (!questionId || sender.id !== chrome.runtime.id || sender.frameId > 0) {
|
|
156
|
+
sendResponse({ ok: false, error: 'Online IDE access requires the portal question tab.' });
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
(async () => {
|
|
160
|
+
const tabs = await chrome.tabs.query({ windowId: sender.tab.windowId });
|
|
161
|
+
const matches = tabs.filter(tab => {
|
|
162
|
+
try {
|
|
163
|
+
const url = new URL(tab.url || tab.pendingUrl);
|
|
164
|
+
return url.origin === 'https://learning.ccbp.in' && /^\/ide\/[a-z0-9]+\/?$/i.test(url.pathname)
|
|
165
|
+
&& url.searchParams.get('q_id') === questionId;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
const ide = matches.find(tab => tab.active) || matches[0];
|
|
172
|
+
if (request.action !== 'focusOnlineIde') {
|
|
173
|
+
if (!ide || matches.length !== 1)
|
|
174
|
+
return { ok: false, error: 'A unique IDE tab for this question is required.' };
|
|
175
|
+
const workspace = new URL(ide.url || ide.pendingUrl).pathname.split('/')[2];
|
|
176
|
+
if (request.workspace !== workspace || !/^[a-z0-9]{6,24}$/i.test(workspace)
|
|
177
|
+
|| !/^[a-f0-9-]{36}$/.test(request.token || ''))
|
|
178
|
+
return { ok: false, error: 'The portal submit command and IDE workspace do not match.' };
|
|
179
|
+
if (request.action === 'runOnlineIde' && (!ide.active || typeof request.code !== 'string'
|
|
180
|
+
|| !request.code.trim() || new TextEncoder().encode(request.code).length > 12000)) {
|
|
181
|
+
return { ok: false, error: 'The matching IDE must be active and the source must fit the terminal input limit.' };
|
|
182
|
+
}
|
|
183
|
+
const results = await chrome.scripting.executeScript({ target: { tabId: ide.id, allFrames: true }, world: 'MAIN',
|
|
184
|
+
func: browserAssistantOnlineIdeTerminal,
|
|
185
|
+
args: [request.action, workspace, questionId, request.token, request.action === 'runOnlineIde' ? request.code : null] });
|
|
186
|
+
const frameResults = results.map(entry => entry.result).filter(Boolean);
|
|
187
|
+
return frameResults.length === 1 ? frameResults[0] : { ok: false, retry: true, error: 'The IDE workspace is still opening.' };
|
|
188
|
+
}
|
|
189
|
+
if (ide && !ide.active)
|
|
190
|
+
await chrome.tabs.update(ide.id, { active: true });
|
|
191
|
+
return { ok: true, found: Boolean(ide) };
|
|
192
|
+
})().then(sendResponse).catch(error => sendResponse({ ok: false, error: error.message }));
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (request.action === 'codingEditor') {
|
|
196
|
+
let protocol = '';
|
|
197
|
+
try {
|
|
198
|
+
protocol = new URL(sender.url).protocol;
|
|
199
|
+
}
|
|
200
|
+
catch { /* Invalid senders are rejected below. */ }
|
|
201
|
+
if (!sender.tab || sender.id !== chrome.runtime.id || !['https:', 'http:'].includes(protocol)
|
|
202
|
+
|| !['read', 'write', 'readProject', 'writeProject'].includes(request.operation)
|
|
203
|
+
|| (request.operation.endsWith('Project') && new URL(sender.url).origin !== 'https://learning.ccbp.in')) {
|
|
204
|
+
sendResponse({ ok: false, error: 'Editor access is not authorized.' });
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
chrome.scripting.executeScript({
|
|
208
|
+
target: { tabId: sender.tab.id, frameIds: [sender.frameId || 0] }, world: 'MAIN',
|
|
209
|
+
func: browserAssistantEditor, args: [request.operation, request.selector, request.expected ?? null, request.value ?? null],
|
|
210
|
+
}).then(results => sendResponse(results[0]?.result || { ok: false, error: 'Editor unavailable.' }))
|
|
211
|
+
.catch(error => sendResponse({ ok: false, error: error.message }));
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (request.action === "captureVisibleTab") {
|
|
215
|
+
if (sender.id !== chrome.runtime.id || !sender.tab || !/^https?:\/\//.test(sender.url || sender.tab.url || '')) {
|
|
216
|
+
sendResponse({ error: 'Screenshot access is not authorized.' });
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
// A tab can lose focus while capture is queued. Never capture another
|
|
220
|
+
// tab/window and send that screenshot with the requesting page's URL.
|
|
221
|
+
chrome.tabs.query({ active: true, windowId: sender.tab.windowId }).then(active => {
|
|
222
|
+
if (active.length !== 1 || active[0].id !== sender.tab.id) {
|
|
223
|
+
sendResponse({ error: 'The requesting tab is no longer active. Focus it and try Vision Scan again.' });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
chrome.tabs.captureVisibleTab(sender.tab.windowId, { format: "png" }, (dataUrl) => {
|
|
227
|
+
if (chrome.runtime.lastError) {
|
|
228
|
+
console.error("Capture failed:", chrome.runtime.lastError.message);
|
|
229
|
+
sendResponse({ error: chrome.runtime.lastError.message });
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
sendResponse({ dataUrl: dataUrl });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}).catch(() => sendResponse({ error: 'Could not verify the active screenshot tab.' }));
|
|
236
|
+
return true; // Keep the message channel open for async response
|
|
237
|
+
}
|
|
238
|
+
if (request.action === "apiRequest") {
|
|
239
|
+
const allowedMethods = { '/health': ['GET'], '/solve': ['POST'], '/solve-vision': ['POST'],
|
|
240
|
+
'/history': ['GET', 'POST', 'DELETE'], '/journal': ['POST'] };
|
|
241
|
+
const method = request.method || 'POST';
|
|
242
|
+
if (sender.id !== chrome.runtime.id || !allowedMethods[request.path]?.includes(method)) {
|
|
243
|
+
sendResponse({ error: "Blocked API sender, path or method" });
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
let body;
|
|
247
|
+
try {
|
|
248
|
+
body = request.body === undefined ? undefined : JSON.stringify(request.body);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
sendResponse({ error: 'Invalid API request body' });
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
// Chrome documents an API pulse for bounded long-running operations.
|
|
255
|
+
// Keep only an active inference request alive; never run an idle timer.
|
|
256
|
+
const longRequest = ['/solve', '/solve-vision', '/journal'].includes(request.path);
|
|
257
|
+
const keepAlive = longRequest && typeof chrome.runtime.getPlatformInfo === 'function'
|
|
258
|
+
? setInterval(() => chrome.runtime.getPlatformInfo(() => { void chrome.runtime.lastError; }), 25000) : null;
|
|
259
|
+
fetch(`http://127.0.0.1:8000${request.path}`, {
|
|
260
|
+
signal: AbortSignal.timeout(['/solve', '/solve-vision', '/journal'].includes(request.path) ? 360000 : 8000),
|
|
261
|
+
method,
|
|
262
|
+
headers: { "Content-Type": "application/json" },
|
|
263
|
+
body,
|
|
264
|
+
})
|
|
265
|
+
.then(async (response) => {
|
|
266
|
+
const text = await response.text();
|
|
267
|
+
let data;
|
|
268
|
+
try {
|
|
269
|
+
data = text ? JSON.parse(text) : {};
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
data = { detail: text || "Unexpected server response" };
|
|
273
|
+
}
|
|
274
|
+
sendResponse({ ok: response.ok, status: response.status, data });
|
|
275
|
+
})
|
|
276
|
+
.catch(error => sendResponse({ error: error.name === 'TimeoutError' ? 'Local server request timed out.' : 'Local server is offline. Start it with: python main.py' }))
|
|
277
|
+
.finally(() => { if (keepAlive !== null)
|
|
278
|
+
clearInterval(keepAlive); });
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
// Unknown action — respond immediately so the sender doesn't hang
|
|
282
|
+
sendResponse({ error: `Unknown action: ${request.action}` });
|
|
283
|
+
return false;
|
|
284
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Coding detection is separate from the existing MCQ adapters.
|
|
2
|
+
(function () {
|
|
3
|
+
const visible = element => element.getClientRects().length > 0 && !element.closest('#assistant-root');
|
|
4
|
+
const text = element => (element?.innerText || element?.textContent || '').trim();
|
|
5
|
+
const editorSelector = '.monaco-editor,.ace_editor,.CodeMirror,textarea[data-code-editor],textarea[name="code"],textarea[id="code"]';
|
|
6
|
+
let detectionError = '';
|
|
7
|
+
let editorSequence = 0;
|
|
8
|
+
const editorIds = new WeakMap();
|
|
9
|
+
function unavailable(message) { detectionError = message; return null; }
|
|
10
|
+
function isReadOnly(editor) {
|
|
11
|
+
return editor.matches('[readonly],[disabled],[aria-readonly="true"],.ace_read-only,.cm-readonly')
|
|
12
|
+
|| Boolean(editor.querySelector('textarea[readonly],textarea[disabled],[contenteditable="false"][role="textbox"]'));
|
|
13
|
+
}
|
|
14
|
+
function chooseEditor(editors, doc) {
|
|
15
|
+
const editable = editors.filter(editor => !isReadOnly(editor));
|
|
16
|
+
if (editable.length === 1)
|
|
17
|
+
return editable[0];
|
|
18
|
+
// Example inputs/outputs can themselves be Ace editors. The solution
|
|
19
|
+
// editor has its own Run/Submit controls; never choose by size or order.
|
|
20
|
+
const candidates = editable.filter(editor => {
|
|
21
|
+
for (let scope = editor.parentElement; scope && scope !== doc.body; scope = scope.parentElement) {
|
|
22
|
+
if (editable.filter(candidate => scope.contains(candidate)).length !== 1)
|
|
23
|
+
break;
|
|
24
|
+
const actions = [...scope.querySelectorAll('button,[role="button"]')].filter(visible);
|
|
25
|
+
if (actions.some(action => /^(run|run code|submit|submit code|submit solution)$/i.test(text(action))))
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
});
|
|
30
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
31
|
+
}
|
|
32
|
+
function selectorFor(editor) {
|
|
33
|
+
if (!editorIds.has(editor))
|
|
34
|
+
editorIds.set(editor, `editor-${++editorSequence}`);
|
|
35
|
+
const id = editorIds.get(editor);
|
|
36
|
+
editor.setAttribute('data-browser-assistant-editor', id);
|
|
37
|
+
return `[data-browser-assistant-editor="${id}"]`;
|
|
38
|
+
}
|
|
39
|
+
// Some practice pages use generated CSS classes and plain text labels.
|
|
40
|
+
// Find the Description pane by its visible content, keeping editor/sidebar text out.
|
|
41
|
+
function findDescription(doc, editor) {
|
|
42
|
+
const anchors = [...doc.querySelectorAll('h1,h2,h3,h4,p,span,div,button,[role="tab"]')]
|
|
43
|
+
.filter(element => visible(element) && /^(description|(?:sample\s+)?output\s*:?|(?:sample\s+)?input\s*:?)$/i.test(text(element)));
|
|
44
|
+
for (const anchor of anchors) {
|
|
45
|
+
for (let region = anchor.parentElement; region && region !== doc.body; region = region.parentElement) {
|
|
46
|
+
if (region.contains(editor) || region.querySelector('#assistant-root'))
|
|
47
|
+
break;
|
|
48
|
+
const content = text(region);
|
|
49
|
+
const hasOutputHeading = [...region.querySelectorAll('h1,h2,h3,h4,h5,h6,p,span,div,strong,b')]
|
|
50
|
+
.some(node => /^(?:sample\s+)?output\s*:?$/i.test(text(node)));
|
|
51
|
+
if ((hasOutputHeading || /sample\s+output|output\s*:/i.test(content)) && /write a program|given\b|you are given|implement\b|print\b|return\b/i.test(content)
|
|
52
|
+
&& content.length >= 40 && content.length <= 30000)
|
|
53
|
+
return region;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const aliases = { python: 'python', python3: 'python', javascript: 'javascript', 'node.js': 'javascript', java: 'java', 'c++': 'cpp', cpp: 'cpp', c: 'c', 'c#': 'csharp', go: 'go', rust: 'rust', kotlin: 'kotlin', php: 'php', sql: 'sql' };
|
|
59
|
+
function parseLanguage(label) {
|
|
60
|
+
const value = String(label || '').normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim()
|
|
61
|
+
.replace(/^(?:selected )?language\s*:\s*/, '');
|
|
62
|
+
// Portal dropdowns use both "JavaScript" and versioned Node.js
|
|
63
|
+
// runtime names. Match the WHOLE label, not arbitrary page/code text.
|
|
64
|
+
if (/^(?:javascript|js|node\.?js)(?:\s+v?\d+(?:\.\d+)*|\s*\(\s*(?:node\.?js\s*)?v?\d+(?:\.\d+)*\s*\)|\s*\(\s*node\.?js\s*\))?$/.test(value))
|
|
65
|
+
return 'javascript';
|
|
66
|
+
if (/^javascript\s*\(\s*node\.?js\s+v?\d+(?:\.\d+)*\s*\)$/.test(value))
|
|
67
|
+
return 'javascript';
|
|
68
|
+
const name = value.replace(/\s*v?\d[\d.]*\s*$/, '').trim();
|
|
69
|
+
return aliases[name] || null;
|
|
70
|
+
}
|
|
71
|
+
function findLanguage(doc, editor) {
|
|
72
|
+
const controls = [...doc.querySelectorAll('[data-code-language],select,[role="combobox"]')].filter(node => visible(node)
|
|
73
|
+
&& !node.closest('[role="listbox"],[role="option"],#assistant-root'));
|
|
74
|
+
const selected = new Set(controls.map(control => parseLanguage(control.dataset.codeLanguage || control.selectedOptions?.[0]?.textContent || text(control))
|
|
75
|
+
|| parseLanguage(control.getAttribute('aria-label')) || parseLanguage(control.getAttribute('title'))).filter(Boolean));
|
|
76
|
+
if (selected.size === 1)
|
|
77
|
+
return [...selected][0];
|
|
78
|
+
if (selected.size > 1)
|
|
79
|
+
return null;
|
|
80
|
+
// Search the editor's nearest toolbar/container before considering wider ancestors.
|
|
81
|
+
for (let scope = editor.parentElement; scope && scope !== doc.body; scope = scope.parentElement) {
|
|
82
|
+
const labels = [...scope.querySelectorAll('span,div,button,p')].filter(element => visible(element)
|
|
83
|
+
&& !element.closest(editorSelector) && !element.closest('[role="option"],[role="listbox"],pre,code,option'));
|
|
84
|
+
const languages = new Set(labels.map(element => parseLanguage(text(element))).filter(Boolean));
|
|
85
|
+
if (languages.size)
|
|
86
|
+
return languages.size === 1 ? [...languages][0] : null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function getQuestion(doc) {
|
|
91
|
+
detectionError = '';
|
|
92
|
+
const editors = [...doc.querySelectorAll(editorSelector)].filter(visible);
|
|
93
|
+
if (!editors.length)
|
|
94
|
+
return unavailable(`CODE-EDITOR: No supported visible editor found (${doc.querySelectorAll('iframe').length} embedded frames).`);
|
|
95
|
+
const editor = chooseEditor(editors, doc);
|
|
96
|
+
if (!editor)
|
|
97
|
+
return unavailable(`CODE-EDITORS: Found ${editors.length} visible editors, but no unique editable solution editor with Run/Submit controls.`);
|
|
98
|
+
const regions = [...doc.querySelectorAll('#codingQuestionInfoContainer,[data-coding-question],.problem-statement,.problem-description,[class*="problemDescription"],[class*="ProblemDescription"],[class*="questionDescription"],[class*="QuestionDescription"]')].filter(visible);
|
|
99
|
+
const region = regions.find(element => !element.contains(editor)) || findDescription(doc, editor) || [...doc.querySelectorAll('main,article')].find(element => visible(element) && !element.contains(editor));
|
|
100
|
+
if (!region || text(region).length < 20)
|
|
101
|
+
return unavailable('CODE-DESCRIPTION: Editor found, but the problem description could not be identified.');
|
|
102
|
+
const webProject = ['htmlEditor', 'cssEditor', 'javascriptEditor'].every(id => doc.getElementById(id)?.matches('.ace_editor'))
|
|
103
|
+
&& ['HTML', 'CSS', 'JAVASCRIPT'].every(name => doc.querySelector(`[data-testid="${name}"]`));
|
|
104
|
+
const language = webProject ? 'web' : findLanguage(doc, editor);
|
|
105
|
+
// Require an explicit language; do not overwrite an editor using a guessed default.
|
|
106
|
+
if (!language)
|
|
107
|
+
return unavailable('CODE-LANGUAGE: Editor and description found, but the selected language is missing or ambiguous.');
|
|
108
|
+
const description = region.cloneNode(true);
|
|
109
|
+
for (const node of description.querySelectorAll('button,[role="tab"],script,style'))
|
|
110
|
+
node.remove();
|
|
111
|
+
for (const node of [...description.querySelectorAll('*')].reverse()) {
|
|
112
|
+
if (/^(solved|in progress|not attempted)$/i.test(text(node)))
|
|
113
|
+
node.remove();
|
|
114
|
+
}
|
|
115
|
+
const statuses = [...region.querySelectorAll('span,div,p')].filter(node => visible(node)
|
|
116
|
+
&& !node.children.length && /^(solved|in progress|not attempted)$/i.test(text(node)));
|
|
117
|
+
const problemStatus = statuses.length === 1 ? text(statuses[0]).toLowerCase() : null;
|
|
118
|
+
const sectionLabel = /^(input|output|explanation|description|submissions|tutorial|get help|note|topics|companies|easy|medium|hard|sample.*)$/i;
|
|
119
|
+
const heading = [...description.querySelectorAll('h1,h2,h3,h4,[role="heading"]')]
|
|
120
|
+
.find(node => text(node) && text(node).length < 200 && !sectionLabel.test(text(node)));
|
|
121
|
+
const portalTitle = region.querySelector('[class*="QuestionShortText-"]');
|
|
122
|
+
let title = text(portalTitle) || text(heading);
|
|
123
|
+
// CCBP often uses a styled div beside "In Progress", not h1/h2.
|
|
124
|
+
// Do not use the Input heading or the entire description as its title.
|
|
125
|
+
if (!title && statuses.length === 1) {
|
|
126
|
+
for (let branch = statuses[0], depth = 0; branch && region.contains(branch) && depth < 3 && !title; branch = branch.parentElement, depth++) {
|
|
127
|
+
for (let sibling = branch.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
128
|
+
const candidate = text(sibling);
|
|
129
|
+
if (candidate.length >= 3 && candidate.length < 200 && !sectionLabel.test(candidate)
|
|
130
|
+
&& !/description.*submissions/i.test(candidate)
|
|
131
|
+
&& !sibling.querySelector('input,textarea,pre,.ace_editor')) {
|
|
132
|
+
title = candidate;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { type: 'code', title, text: text(description), language, problemStatus, editorSelector: selectorFor(editor), options: [] };
|
|
139
|
+
}
|
|
140
|
+
function getOnlineIdeQuestion(doc, location) {
|
|
141
|
+
if (location.origin !== 'https://learning.ccbp.in'
|
|
142
|
+
|| !/^\/question\/[a-f0-9-]{36}\/?$/i.test(location.pathname))
|
|
143
|
+
return null;
|
|
144
|
+
const description = doc.querySelector('#codingQuestionInfoContainer #mark-down')
|
|
145
|
+
|| doc.querySelector('#codingQuestionInfoContainer');
|
|
146
|
+
const buttons = [...doc.querySelectorAll('button:not(:disabled),a[href],[role="button"]:not([aria-disabled="true"])')]
|
|
147
|
+
.filter(node => visible(node) && /^open ide$/i.test(text(node)));
|
|
148
|
+
if (!description || !visible(description) || buttons.length !== 1)
|
|
149
|
+
return null;
|
|
150
|
+
const requirements = text(description);
|
|
151
|
+
if (!/\bapp\.js\b/i.test(requirements) || !/\b(?:express|common\s*js|node\.?js)\b/i.test(requirements))
|
|
152
|
+
return null;
|
|
153
|
+
const title = text(description.querySelector('h1,h2')) || 'Node.js IDE assignment';
|
|
154
|
+
const panel = doc.querySelector('#codingQuestionInfoContainer');
|
|
155
|
+
const solved = [...panel.querySelectorAll('span,div')].some(node => !node.children.length && visible(node) && /^solved$/i.test(text(node)));
|
|
156
|
+
const commands = new Set([...doc.querySelectorAll('code,span,div,p,input')]
|
|
157
|
+
.filter(node => visible(node) && !description.contains(node))
|
|
158
|
+
.map(node => (node.value || text(node)).match(/^ccbp\s+submit\s+([a-z0-9]{6,24})$/i)?.[1]).filter(Boolean));
|
|
159
|
+
return { title, text: requirements, button: buttons[0], solved, workspace: commands.size === 1 ? [...commands][0] : null };
|
|
160
|
+
}
|
|
161
|
+
function getSubmit(doc) {
|
|
162
|
+
const buttons = [...doc.querySelectorAll('button:not(:disabled),[role="button"]:not([aria-disabled="true"])')]
|
|
163
|
+
.filter(visible).filter(button => /^(submit|submit code|submit solution)$/i.test(text(button)));
|
|
164
|
+
return buttons.length === 1 ? buttons[0] : null;
|
|
165
|
+
}
|
|
166
|
+
function getResult(doc) {
|
|
167
|
+
const webResult = doc.querySelector('#submitResultContainer');
|
|
168
|
+
if (webResult && visible(webResult)) {
|
|
169
|
+
const cards = [...webResult.querySelectorAll('[class*="TestCaseSummaryCard-"]')];
|
|
170
|
+
const counts = {};
|
|
171
|
+
for (const card of cards) {
|
|
172
|
+
const label = text(card.querySelector('[class*="TestCaseSummaryLabel-"]')).toLowerCase();
|
|
173
|
+
const value = text(card.querySelector('[class*="TestCaseSummaryCount-"]'));
|
|
174
|
+
if (/^\d+$/.test(value))
|
|
175
|
+
counts[label] = Number(value);
|
|
176
|
+
}
|
|
177
|
+
if (counts.total > 0 && counts.passed + counts.failed === counts.total) {
|
|
178
|
+
return counts.failed > 0 ? 'failed' : counts.passed === counts.total ? 'passed' : null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const resultNodes = [...doc.querySelectorAll('[data-coding-result],[role="status"],[role="alert"],.submission-result,.test-results')];
|
|
182
|
+
const labels = [...doc.querySelectorAll('span,p,div')].filter(node => !node.closest('table,[role="table"],tr,[role="row"]') && !node.children.length
|
|
183
|
+
&& /^(accepted|all test cases passed|all tests passed|correct answer|wrong answer|compilation error|runtime error|time limit exceeded|tests? failed)[.!\s]*$/i.test(text(node)));
|
|
184
|
+
const results = [...resultNodes, ...labels].filter(visible).map(text).join('\n');
|
|
185
|
+
const latest = labels.filter(visible)[0];
|
|
186
|
+
if (latest)
|
|
187
|
+
return /^(accepted|all test cases passed|all tests passed|correct answer)/i.test(text(latest)) ? 'passed' : 'failed';
|
|
188
|
+
if (/wrong answer|compilation error|runtime error|time limit exceeded|tests? failed/i.test(results))
|
|
189
|
+
return 'failed';
|
|
190
|
+
if (/^(accepted|all test cases passed|all tests passed|correct answer)[.!\s]*$/im.test(results))
|
|
191
|
+
return 'passed';
|
|
192
|
+
// Only the selected problem's status counts; sidebar/course completion does not.
|
|
193
|
+
if (getQuestion(doc)?.problemStatus === 'solved')
|
|
194
|
+
return 'passed';
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function getFailureFeedback(doc) {
|
|
198
|
+
const copy = doc.body.cloneNode(true);
|
|
199
|
+
for (const node of copy.querySelectorAll('#assistant-root,.ace_editor,.monaco-editor,.CodeMirror,script,style,table,[role="table"]'))
|
|
200
|
+
node.remove();
|
|
201
|
+
return text(copy).slice(0, 16000);
|
|
202
|
+
}
|
|
203
|
+
function getDescriptionTab(doc) {
|
|
204
|
+
const tabs = [...doc.querySelectorAll('button,[role="tab"],a,span,div')].filter(node => visible(node)
|
|
205
|
+
&& /^description$/i.test(text(node)) && ![...node.children].some(child => /^description$/i.test(text(child))));
|
|
206
|
+
return tabs.length === 1 ? tabs[0] : null;
|
|
207
|
+
}
|
|
208
|
+
window.BrowserAssistantCoding = { getQuestion, getOnlineIdeQuestion, getSubmit, getResult, getFailureFeedback, getDescriptionTab, getDetectionError: () => detectionError };
|
|
209
|
+
})();
|