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.
- package/LICENSE +21 -0
- package/README.md +120 -2
- 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,187 @@
|
|
|
1
|
+
// Problem-list navigation is deliberately separate from solving/editor access.
|
|
2
|
+
(function () {
|
|
3
|
+
const visible = node => node.getClientRects().length && !node.closest('#assistant-root');
|
|
4
|
+
const text = node => (node.innerText || node.textContent || '').replace(/\s+/g, ' ').trim();
|
|
5
|
+
const statusPattern = /^(not attempted|in progress|solved)$/i;
|
|
6
|
+
function courseFilterStep(doc) {
|
|
7
|
+
const courses = /^(programming foundations|data structures\s*(?:&|and)\s*algorithms|build your own static website|build your own responsive website|modern responsive web design|build your own dynamic web application)$/i;
|
|
8
|
+
const choices = [];
|
|
9
|
+
for (const input of doc.querySelectorAll('input[type="checkbox"],[role="checkbox"]')) {
|
|
10
|
+
const label = input.labels?.[0] || (input.id ? [...doc.querySelectorAll('label')].find(node => node.htmlFor === input.id) : null)
|
|
11
|
+
|| input.closest('label') || input.parentElement;
|
|
12
|
+
if (!label || !visible(label) || !courses.test(text(label)))
|
|
13
|
+
continue;
|
|
14
|
+
choices.push({ input, name: text(label), selected: Boolean(input.checked || input.getAttribute('aria-checked') === 'true') });
|
|
15
|
+
}
|
|
16
|
+
const excluded = choices.filter(item => /^data structures/i.test(item.name));
|
|
17
|
+
const included = choices.filter(item => !/^data structures/i.test(item.name));
|
|
18
|
+
if (excluded.length !== 1 || !included.length)
|
|
19
|
+
return { error: 'Course filters were not identified. Open the Course filters to exclude Data Structures & Algorithms.' };
|
|
20
|
+
// Enable the other courses first: some portals interpret zero selected
|
|
21
|
+
// filters as "all courses", which would temporarily include DSA.
|
|
22
|
+
const change = included.find(item => !item.selected) || excluded.find(item => item.selected);
|
|
23
|
+
if (change?.input.disabled || change?.input.getAttribute('aria-disabled') === 'true')
|
|
24
|
+
return { error: 'Course filter is disabled.' };
|
|
25
|
+
return change ? { control: change.input } : { ready: true };
|
|
26
|
+
}
|
|
27
|
+
function rows(doc) {
|
|
28
|
+
const statuses = [...doc.querySelectorAll('span,div,td,p')].filter(node => visible(node)
|
|
29
|
+
&& statusPattern.test(text(node)) && ![...node.children].some(child => statusPattern.test(text(child))));
|
|
30
|
+
const found = [];
|
|
31
|
+
for (const status of statuses) {
|
|
32
|
+
if (!/^(not attempted|in progress)$/i.test(text(status)))
|
|
33
|
+
continue;
|
|
34
|
+
for (let row = status.parentElement; row && row !== doc.body; row = row.parentElement) {
|
|
35
|
+
if (row.querySelector('input[type="checkbox"],[role="checkbox"]'))
|
|
36
|
+
break;
|
|
37
|
+
if (statuses.filter(item => row.contains(item)).length !== 1)
|
|
38
|
+
break;
|
|
39
|
+
const copy = row.cloneNode(true);
|
|
40
|
+
// CCBP displays '-' when difficulty is unavailable. It is
|
|
41
|
+
// metadata, not part of the title (Parsing Datetime-).
|
|
42
|
+
for (const item of copy.querySelectorAll('[data-testid="question-difficulty"]'))
|
|
43
|
+
item.remove();
|
|
44
|
+
for (const icon of copy.querySelectorAll('svg,img,[aria-hidden="true"]'))
|
|
45
|
+
icon.remove();
|
|
46
|
+
for (const item of [...copy.querySelectorAll('*')].reverse()) {
|
|
47
|
+
if (/^(not attempted|in progress|easy|medium|hard|→|›|»|open|view)$/i.test(text(item)))
|
|
48
|
+
item.remove();
|
|
49
|
+
}
|
|
50
|
+
const portalTitle = row.matches('[data-testid="question-list-question"]')
|
|
51
|
+
? row.querySelector('span[title]')?.getAttribute('title') : null;
|
|
52
|
+
const title = portalTitle?.trim() || text(copy);
|
|
53
|
+
const actions = [...row.querySelectorAll('a[href],button,[role="button"]')].filter(visible);
|
|
54
|
+
if (title && (actions.length || row.querySelector('svg') || row.matches('[role="row"],tr'))) {
|
|
55
|
+
const action = actions.length === 1 ? actions[0] : row;
|
|
56
|
+
const link = row.querySelector('a[href*="/question/"]');
|
|
57
|
+
found.push({ title, action, href: link?.getAttribute('href') || null });
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
function matchesQuestion(question, batch, href) {
|
|
65
|
+
if (!question || question.type !== 'code')
|
|
66
|
+
return false;
|
|
67
|
+
if (batch.questionUrl) {
|
|
68
|
+
const expected = new URL(batch.questionUrl, batch.listUrl);
|
|
69
|
+
const current = new URL(href);
|
|
70
|
+
if (expected.origin !== current.origin || expected.pathname !== current.pathname)
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const normalize = value => (value || '').normalize('NFKC').toLowerCase()
|
|
74
|
+
.replace(/(?:\s|\u00a0)*(?:easy|medium|hard|not attempted|in progress|solved)\s*$/i, '')
|
|
75
|
+
.replace(/[\s\u200b-\u200d\ufeff]+/g, '').replace(/[–—−]/g, '-');
|
|
76
|
+
const expectedTitle = normalize(batch.title);
|
|
77
|
+
if (!expectedTitle)
|
|
78
|
+
return true;
|
|
79
|
+
return question.title ? normalize(question.title) === expectedTitle : normalize(question.text).includes(expectedTitle);
|
|
80
|
+
}
|
|
81
|
+
function nextPage(doc) {
|
|
82
|
+
const range = pageRange(doc);
|
|
83
|
+
if (range && range.end >= range.total)
|
|
84
|
+
return null;
|
|
85
|
+
const buttons = [...doc.querySelectorAll('button,a,[role="button"]')].filter(node => visible(node)
|
|
86
|
+
&& !node.disabled && node.getAttribute('aria-disabled') !== 'true');
|
|
87
|
+
const named = buttons.filter(node => /^(next|next page)$/i.test(node.getAttribute('aria-label') || text(node)));
|
|
88
|
+
if (named.length === 1)
|
|
89
|
+
return named[0];
|
|
90
|
+
const current = doc.querySelector('[aria-current="page"],[data-active-page="true"]');
|
|
91
|
+
if (current && /^\d+$/.test(text(current))) {
|
|
92
|
+
const next = buttons.filter(node => text(node) === String(Number(text(current)) + 1));
|
|
93
|
+
if (next.length === 1)
|
|
94
|
+
return next[0];
|
|
95
|
+
}
|
|
96
|
+
if (range && range.end < range.total) {
|
|
97
|
+
const number = Math.floor((range.start - 1) / (range.end - range.start + 1)) + 2;
|
|
98
|
+
const next = buttons.filter(node => text(node) === String(number));
|
|
99
|
+
if (next.length === 1)
|
|
100
|
+
return next[0];
|
|
101
|
+
// React pagers may use plain div/li/span controls with delegated
|
|
102
|
+
// click handlers. Restrict fallback clicks to a numeric pager group.
|
|
103
|
+
const leaves = [...doc.querySelectorAll('div,span,li,a,button')].filter(node => visible(node)
|
|
104
|
+
&& !node.children.length && /^\d+$/.test(text(node)));
|
|
105
|
+
const targets = leaves.filter(node => text(node) === String(number)).filter(node => {
|
|
106
|
+
for (let scope = node.parentElement; scope && scope !== doc.body; scope = scope.parentElement) {
|
|
107
|
+
if (scope.querySelector('input,textarea,.ace_editor,.monaco-editor,#assistant-root'))
|
|
108
|
+
break;
|
|
109
|
+
const numbers = leaves.filter(item => scope.contains(item)).map(text);
|
|
110
|
+
if (numbers.includes(String(number - 1)) && new Set(numbers).size >= 2) {
|
|
111
|
+
// A pager contains numbers, ellipses and arrows, not question prose.
|
|
112
|
+
return !/[a-z]/i.test(text(scope).replace(/next|previous|page/ig, ''));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}).filter(node => !node.closest('[aria-disabled="true"],[disabled],.disabled'));
|
|
117
|
+
if (targets.length === 1)
|
|
118
|
+
return targets[0];
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function getBackControl(doc, listUrl) {
|
|
123
|
+
const enabled = node => visible(node) && !node.closest('[disabled],[aria-disabled="true"]');
|
|
124
|
+
const links = [...doc.querySelectorAll('a[href]')].filter(enabled).filter(node => {
|
|
125
|
+
try {
|
|
126
|
+
const url = new URL(node.getAttribute('href'), listUrl);
|
|
127
|
+
return url.origin === new URL(listUrl).origin && url.pathname === new URL(listUrl).pathname;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
if (links.length === 1)
|
|
134
|
+
return links[0];
|
|
135
|
+
const headings = [...doc.querySelectorAll('h1,h2,h3,span,div,p')].filter(node => visible(node)
|
|
136
|
+
&& /^question bank$/i.test(text(node)) && ![...node.children].some(child => /^question bank$/i.test(text(child))));
|
|
137
|
+
for (const heading of headings) {
|
|
138
|
+
let scope = heading.parentElement;
|
|
139
|
+
for (let depth = 0; scope && scope !== doc.body && depth < 3; depth++, scope = scope.parentElement) {
|
|
140
|
+
if (scope.querySelector('.ace_editor,.monaco-editor,#assistant-root'))
|
|
141
|
+
break;
|
|
142
|
+
const buttons = [...scope.querySelectorAll('button,[role="button"],a')].filter(enabled);
|
|
143
|
+
const back = buttons.filter(node => /^(back|go back|back to question bank|question bank|←)$/i.test(node.getAttribute('aria-label') || node.getAttribute('title') || text(node)));
|
|
144
|
+
if (back.length === 1)
|
|
145
|
+
return back[0];
|
|
146
|
+
if (buttons.length === 1 && buttons[0].querySelector('svg'))
|
|
147
|
+
return buttons[0];
|
|
148
|
+
// Some portal headers attach the click handler to an SVG wrapper.
|
|
149
|
+
const icons = [...scope.querySelectorAll('svg')].filter(enabled);
|
|
150
|
+
if (icons.length === 1 && /^question bank$/i.test(text(scope)))
|
|
151
|
+
return icons[0].closest('button,a,div,span');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function getSaveBeforeLeaving(doc) {
|
|
157
|
+
const choices = new Set();
|
|
158
|
+
for (const prompt of doc.querySelectorAll('p,span,div,h2,h3')) {
|
|
159
|
+
if (!visible(prompt) || !/^do you want to save code before leaving\?$/i.test(text(prompt)))
|
|
160
|
+
continue;
|
|
161
|
+
for (let scope = prompt.parentElement; scope && scope !== doc.body; scope = scope.parentElement) {
|
|
162
|
+
if (scope.querySelector('#assistant-root,.ace_editor,.monaco-editor'))
|
|
163
|
+
break;
|
|
164
|
+
const buttons = [...scope.querySelectorAll('button,[role="button"]')].filter(node => visible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true');
|
|
165
|
+
const yes = buttons.filter(node => /^yes$/i.test(text(node)));
|
|
166
|
+
if (yes.length === 1 && buttons.some(node => /^no$/i.test(text(node)))) {
|
|
167
|
+
choices.add(yes[0]);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return choices.size === 1 ? [...choices][0] : null;
|
|
173
|
+
}
|
|
174
|
+
function pageRange(doc) {
|
|
175
|
+
const match = text(doc.body).match(/\b([\d,]+)\s+to\s+([\d,]+)\s+of\s+([\d,]+)\b/i);
|
|
176
|
+
if (!match)
|
|
177
|
+
return null;
|
|
178
|
+
const [start, end, total] = match.slice(1).map(value => Number(value.replace(/,/g, '')));
|
|
179
|
+
return { start, end, total };
|
|
180
|
+
}
|
|
181
|
+
function pageKey(doc) {
|
|
182
|
+
const copy = doc.body.cloneNode(true);
|
|
183
|
+
copy.querySelector('#assistant-root')?.remove();
|
|
184
|
+
return text(copy);
|
|
185
|
+
}
|
|
186
|
+
window.BrowserAssistantCodingBatch = { rows, nextPage, pageKey, pageRange, getBackControl, getSaveBeforeLeaving, matchesQuestion, courseFilterStep };
|
|
187
|
+
})();
|