browser-assistant 0.0.0-stage → 2.8.2

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.
@@ -0,0 +1,313 @@
1
+ // Serialized by chrome.scripting into the page's MAIN world to access editor APIs.
2
+ function browserAssistantEditor(operation, selector, expected, value) {
3
+ try {
4
+ if (operation === 'readProject' || operation === 'writeProject') {
5
+ const files = { html: '#htmlEditor', css: '#cssEditor', javascript: '#javascriptEditor' };
6
+ const editors = {};
7
+ for (const [name, id] of Object.entries(files)) {
8
+ const matches = [...document.querySelectorAll(id)];
9
+ if (matches.length !== 1 || !matches[0].matches('.ace_editor') || matches[0].closest('#assistant-root'))
10
+ throw new Error('Web project editor missing: ' + name);
11
+ const editor = matches[0].env?.editor;
12
+ if (!editor || editor.getReadOnly())
13
+ throw new Error('Web project editor is unavailable or read-only: ' + name);
14
+ editors[name] = editor;
15
+ }
16
+ const readFiles = () => Object.fromEntries(Object.entries(editors).map(([name, editor]) => [name, editor.getValue()]));
17
+ const before = readFiles();
18
+ if (operation === 'writeProject') {
19
+ const previous = JSON.parse(expected);
20
+ const replacement = JSON.parse(value);
21
+ for (const name of Object.keys(files)) {
22
+ if (typeof replacement[name] !== 'string' || previous[name] !== before[name])
23
+ throw new Error('A project file changed while generating code: ' + name);
24
+ }
25
+ try {
26
+ for (const name of Object.keys(files))
27
+ editors[name].setValue(replacement[name], -1);
28
+ for (const name of Object.keys(files))
29
+ if (editors[name].getValue() !== replacement[name])
30
+ throw new Error('Project write verification failed: ' + name);
31
+ }
32
+ catch (error) {
33
+ for (const name of Object.keys(files))
34
+ editors[name].setValue(before[name], -1);
35
+ throw error;
36
+ }
37
+ }
38
+ return { ok: true, value: JSON.stringify(readFiles()) };
39
+ }
40
+ const nodes = [...document.querySelectorAll(selector)].filter(node => node.getClientRects().length > 0 && !node.closest('#assistant-root'));
41
+ if (nodes.length !== 1)
42
+ throw new Error('Expected one visible code editor.');
43
+ const node = nodes[0];
44
+ let read, write;
45
+ if (node.matches('.monaco-editor')) {
46
+ const editors = window.monaco?.editor?.getEditors?.() || [];
47
+ const editor = editors.find(item => item.getDomNode() === node);
48
+ if (!editor || editor.getOption(window.monaco.editor.EditorOption.readOnly))
49
+ throw new Error('Monaco editor API is unavailable or read-only.');
50
+ const model = editor.getModel();
51
+ read = () => model.getValue();
52
+ write = code => { editor.pushUndoStop(); editor.executeEdits('browser-assistant', [{ range: model.getFullModelRange(), text: code }]); editor.pushUndoStop(); };
53
+ }
54
+ else if (node.matches('.ace_editor')) {
55
+ const editor = node.env?.editor;
56
+ if (!editor || editor.getReadOnly())
57
+ throw new Error('Ace editor is unavailable or read-only.');
58
+ read = () => editor.getValue();
59
+ write = code => editor.setValue(code, -1);
60
+ }
61
+ else if (node.matches('.CodeMirror')) {
62
+ const editor = node.CodeMirror;
63
+ if (!editor || editor.getOption('readOnly'))
64
+ throw new Error('CodeMirror is unavailable or read-only.');
65
+ read = () => editor.getValue();
66
+ write = code => editor.setValue(code);
67
+ }
68
+ else {
69
+ if (node.disabled || node.readOnly)
70
+ throw new Error('Code editor is read-only.');
71
+ read = () => node.value;
72
+ write = code => {
73
+ Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(node, code);
74
+ node.dispatchEvent(new Event('input', { bubbles: true }));
75
+ node.dispatchEvent(new Event('change', { bubbles: true }));
76
+ };
77
+ }
78
+ if (operation === 'write') {
79
+ if (typeof value !== 'string' || read() !== expected)
80
+ throw new Error('Editor changed while generating code.');
81
+ write(value);
82
+ if (read() !== value)
83
+ throw new Error('Editor did not retain the generated code.');
84
+ }
85
+ return { ok: true, value: read() };
86
+ }
87
+ catch (error) {
88
+ return { ok: false, error: error.message };
89
+ }
90
+ }
91
+ // Runs in the portal IDE frame only. The terminal command is built here from
92
+ // fixed operations and encoded source, never from model-generated shell text.
93
+ async function browserAssistantOnlineIdeTerminal(action, workspace, questionId, token, source) {
94
+ if (location.origin !== 'https://ide.ccbp.in' || location.pathname.replace(/\/$/, '') !== `/playground/${workspace}`)
95
+ return null;
96
+ const visible = node => node.getClientRects().length > 0;
97
+ const inputs = [...document.querySelectorAll('.xterm-helper-textarea[aria-label="Terminal input"]')].filter(visible);
98
+ if (inputs.length !== 1)
99
+ return { ok: false, retry: inputs.length === 0, error: 'A unique IDE terminal is required.' };
100
+ const input = inputs[0];
101
+ const rowContainer = input.closest('.xterm')?.querySelector('.xterm-rows');
102
+ const rows = () => [...(rowContainer?.children || [])].map(row => row.textContent.trim()).filter(Boolean);
103
+ const finishMarker = `BA_RESULT_${token}`;
104
+ const reportPrefix = `https://learning.ccbp.in/ide-coding-submission/${questionId}/`;
105
+ const reports = lines => {
106
+ const text = lines.join('');
107
+ return text.split(reportPrefix).slice(1).map(fragment => fragment.match(/^[a-f0-9-]{36}/i)?.[0]).filter(Boolean);
108
+ };
109
+ const key = '__browserAssistantOnlineIdeRun';
110
+ const previous = window[key];
111
+ if (action === 'onlineIdeStatus') {
112
+ if (previous?.token !== token)
113
+ return { ok: false, error: 'The IDE reloaded or its run could not be identified.' };
114
+ const lines = rows();
115
+ const result = lines.find(line => line.startsWith(`${finishMarker}:`));
116
+ if (!result)
117
+ return { ok: true, done: false };
118
+ const index = lines.findLastIndex(line => line.includes('Open Submission URL'));
119
+ const report = index >= 0 && reports(lines.slice(index)).find(id => !previous.reports.includes(id));
120
+ const passed = result === `${finishMarker}:0` && Boolean(report) && index > 0
121
+ && lines.slice(Math.max(0, index - 3), index).includes('All test cases passed');
122
+ return { ok: true, done: true, passed, submitted: Boolean(report), url: report ? reportPrefix + report : null };
123
+ }
124
+ if (previous?.token === token)
125
+ return { ok: false, error: 'This IDE run was already started. Check the terminal.' };
126
+ if (document.visibilityState !== 'visible')
127
+ return { ok: false, error: 'Keep the matching IDE tab active to submit.' };
128
+ const before = rows();
129
+ const prompt = before.at(-1)?.match(/^nxtwave@([a-z0-9]+):~\/nodejs\/coding-practices\/([a-z0-9_-]+)\$$/i);
130
+ if (!prompt || prompt[1] !== workspace)
131
+ return { ok: false, retry: true, error: 'Waiting for the correct workspace shell prompt.' };
132
+ if (reports(before).length && before.includes('All test cases passed'))
133
+ return { ok: true, alreadyPassed: true };
134
+ const findFile = () => [...document.querySelectorAll('div[title="app.js"]')].filter(visible);
135
+ let files = findFile();
136
+ if (!files.length) {
137
+ const folders = [...document.querySelectorAll('div[title=""] span')]
138
+ .filter(node => visible(node) && node.textContent.trim() === prompt[2]);
139
+ if (folders.length === 1) {
140
+ folders[0].click();
141
+ await new Promise(resolve => setTimeout(resolve, 150));
142
+ files = findFile();
143
+ }
144
+ }
145
+ if (files.length !== 1)
146
+ return { ok: false, retry: true, error: 'Waiting for the workspace app.js file in Explorer.' };
147
+ files[0].click();
148
+ await new Promise(resolve => setTimeout(resolve, 200));
149
+ const editors = [...document.querySelectorAll('.monaco-editor[data-uri]')].filter(visible);
150
+ const editor = editors.length === 1 && editors[0];
151
+ if (!editor || !editor.getAttribute('data-uri').endsWith(`/${prompt[2]}/app.js`)) {
152
+ return { ok: false, retry: true, error: 'Waiting for the app.js editor.' };
153
+ }
154
+ const renderedCode = editor.querySelector('.view-lines');
155
+ if (!renderedCode || renderedCode.textContent.trim())
156
+ return { ok: false, error: 'app.js already contains code. Review the prepared result before replacing it.' };
157
+ const bytes = new TextEncoder().encode(source);
158
+ const encoded = btoa(Array.from(bytes, byte => String.fromCharCode(byte)).join(''));
159
+ const digest = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), byte => byte.toString(16).padStart(2, '0')).join('');
160
+ const writer = [
161
+ 'const fs = require("node:fs")',
162
+ 'const [encoded, digest, folder] = process.argv.slice(1)',
163
+ 'const code = Buffer.from(encoded, "base64")',
164
+ 'if (require("node:crypto").createHash("sha256").update(code).digest("hex") !== digest) throw Error("Incomplete source received")',
165
+ 'if (!process.cwd().endsWith("/nodejs/coding-practices/" + folder)) throw Error("Unexpected project directory")',
166
+ 'const stat = fs.lstatSync("app.js")',
167
+ 'if (!stat.isFile() || fs.readFileSync("app.js", "utf8").trim()) throw Error("app.js is not an empty regular file")',
168
+ 'const check = require("node:child_process").spawnSync(process.execPath, ["--check"], {input: code})',
169
+ 'if (check.status !== 0) throw Error(String(check.stderr || "JavaScript syntax check failed"))',
170
+ 'fs.writeFileSync("app.js", code)',
171
+ 'if (!fs.readFileSync("app.js").equals(code)) throw Error("Could not verify the saved file")',
172
+ ].join('; ');
173
+ // All arguments are restricted to letters, digits, or base64 by construction.
174
+ const echoMarker = `BA_COMMAND_${token}`;
175
+ const command = `node -e '${writer}' '${encoded}' '${digest}' '${prompt[2]}' && ccbp submit ${workspace}; printf '\\n${finishMarker}:%s\\n' "$?" #${echoMarker}`;
176
+ if (rows().at(-1) !== before.at(-1))
177
+ return { ok: false, error: 'The IDE terminal changed before submission.' };
178
+ input.focus();
179
+ if (document.activeElement !== input)
180
+ return { ok: false, error: 'The IDE terminal could not be focused.' };
181
+ const data = new DataTransfer();
182
+ data.setData('text/plain', command);
183
+ window[key] = { token, reports: reports(before) };
184
+ input.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }));
185
+ const deadline = Date.now() + 2500;
186
+ while (Date.now() < deadline && document.activeElement === input && document.visibilityState === 'visible') {
187
+ if (rows().join('').endsWith(`#${echoMarker}`)) {
188
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true }));
189
+ return { ok: true, started: true };
190
+ }
191
+ await new Promise(resolve => setTimeout(resolve, 50));
192
+ }
193
+ return { ok: false, error: 'The IDE did not confirm terminal input. Check the terminal before running any command.' };
194
+ }
195
+ // Keep complete models on their DOM nodes in the content script's world. Ace
196
+ // recycles viewport rows when scrolling, so rendered text is not an option ID.
197
+ var browserAssistantMcqSnippetCache = new WeakMap();
198
+ function browserAssistantCacheMcqSnippets(snapshot) {
199
+ const nodes = [...document.querySelectorAll('.ace_editor,.monaco-editor,.CodeMirror')]
200
+ .filter(node => node.getClientRects().length && !node.closest('#assistant-root'));
201
+ if (!Array.isArray(snapshot.editorSources) || nodes.length !== snapshot.editorSources.length
202
+ || snapshot.editorSources.some(value => typeof value !== 'string'))
203
+ return;
204
+ nodes.forEach((node, index) => browserAssistantMcqSnippetCache.set(node, snapshot.editorSources[index]));
205
+ }
206
+ function browserAssistantCachedMcqSnippet(node) {
207
+ return browserAssistantMcqSnippetCache.get(node);
208
+ }
209
+ // Read-only snapshot of complete editor models, including offscreen lines.
210
+ // DOM .ace_line/.view-line nodes contain only the current viewport.
211
+ function browserAssistantReadMcqSnippets() {
212
+ try {
213
+ const completeAceDom = node => {
214
+ const lines = [...node.querySelectorAll('.ace_line')];
215
+ const numbers = [...node.querySelectorAll('.ace_gutter-cell')].map(item => Number((item.textContent || '').trim()));
216
+ const bar = node.querySelector('.ace_scrollbar-v .ace_scrollbar-inner');
217
+ if (!lines.length || numbers.length !== lines.length || !numbers.every((number, index) => number === index + 1))
218
+ return null;
219
+ const rowHeight = lines[0].getBoundingClientRect().height;
220
+ const totalHeight = bar ? Math.max(bar.scrollHeight || 0, parseFloat(bar.style.height) || 0) : 0;
221
+ // No scrollbar measurement means the visible rows cannot prove
222
+ // completeness. Never guess from the first screen of long code.
223
+ if (!(rowHeight > 0 && totalHeight > 0) || totalHeight > rowHeight * lines.length + 1)
224
+ return null;
225
+ return lines.map(line => line.textContent || '').join('\n');
226
+ };
227
+ const nodes = [...document.querySelectorAll('.ace_editor,.monaco-editor,.CodeMirror')]
228
+ // MCQ pages sometimes reuse the coding editor's ID for a read-only
229
+ // pseudocode panel. This function only reads; the background origin
230
+ // check limits it to the learning portal, including SPA routes.
231
+ .filter(node => node.getClientRects().length && !node.closest('#assistant-root'));
232
+ const sources = [], editorSources = [];
233
+ for (const node of nodes) {
234
+ let value;
235
+ if (node.matches('.ace_editor'))
236
+ value = node.env?.editor?.getValue()
237
+ ?? node.env?.editor?.session?.getValue() ?? completeAceDom(node);
238
+ else if (node.matches('.CodeMirror'))
239
+ value = node.CodeMirror?.getValue();
240
+ else {
241
+ const editor = window.monaco?.editor?.getEditors?.().find(item => item.getDomNode() === node);
242
+ value = editor?.getModel()?.getValue();
243
+ if (typeof value !== 'string' && nodes.length === 1) {
244
+ const models = window.monaco?.editor?.getModels?.() || [];
245
+ if (models.length === 1)
246
+ value = models[0].getValue();
247
+ }
248
+ }
249
+ if (typeof value !== 'string')
250
+ throw new Error('Full snippet editor model is unavailable; refusing to solve from partial lines.');
251
+ editorSources.push(value);
252
+ if (value.trim() && !sources.includes(value))
253
+ sources.push(value);
254
+ }
255
+ if (editorSources.join('\n').length > 50000)
256
+ throw new Error('Code snippets exceed the 50000-character limit; no code was truncated.');
257
+ return { ok: true, sources, editorSources };
258
+ }
259
+ catch (error) {
260
+ return { ok: false, error: error.message };
261
+ }
262
+ }
263
+ // Runs in the page's main world. CCBP's React drag listeners receive the
264
+ // native DataTransfer object only when these events originate in this world.
265
+ async function browserAssistantOrderingDrag(prompt, optionTexts, from, to) {
266
+ try {
267
+ const normalize = value => (value || '').replace(/\s+/g, ' ').trim();
268
+ const question = document.querySelector('#generic-question-container');
269
+ const actualPrompt = normalize(question?.querySelector('#generic-question-wrapper-id')?.textContent);
270
+ if (!question || actualPrompt !== normalize(prompt) || !Array.isArray(optionTexts)
271
+ || !Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < 0)
272
+ return { ok: false };
273
+ const rows = () => [...question.querySelectorAll('.drag-and-drop-option-label')]
274
+ .map(label => ({ label, text: normalize(label.textContent), handle: label.parentElement?.querySelector('.drag-icon-container[draggable="true"]') }))
275
+ .filter(row => row.handle);
276
+ const before = rows();
277
+ if (before.length !== optionTexts.length || before.some((row, index) => row.text !== normalize(optionTexts[index]))
278
+ || from >= before.length || to >= before.length)
279
+ return { ok: false };
280
+ if (from === to)
281
+ return { ok: true };
282
+ const source = before[from].handle;
283
+ const target = before[to].handle;
284
+ const dataTransfer = new DataTransfer();
285
+ dataTransfer.effectAllowed = 'move';
286
+ const send = (node, type) => {
287
+ const rect = node.getBoundingClientRect();
288
+ node.dispatchEvent(new DragEvent(type, {
289
+ bubbles: true, cancelable: true, dataTransfer,
290
+ clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2,
291
+ }));
292
+ };
293
+ send(source, 'dragstart');
294
+ await new Promise(resolve => setTimeout(resolve, 220));
295
+ send(target, 'dragenter');
296
+ send(target, 'dragover');
297
+ await new Promise(resolve => setTimeout(resolve, 220));
298
+ send(target, 'drop');
299
+ send(source, 'dragend');
300
+ for (let attempt = 0; attempt < 10; attempt++) {
301
+ await new Promise(resolve => setTimeout(resolve, 160));
302
+ const after = rows();
303
+ if (after.length !== before.length)
304
+ return { ok: false };
305
+ if (after[to]?.text === before[from].text)
306
+ return { ok: true };
307
+ }
308
+ return { ok: false };
309
+ }
310
+ catch (error) {
311
+ return { ok: false, error: error.message };
312
+ }
313
+ }