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,559 @@
1
+ function browserAssistantJournal(global) {
2
+ 'use strict';
3
+ const keys = ['experience', 'feelings', 'learning', 'application', 'conclusion'];
4
+ const labels = ['Experience', 'Feelings', 'Learning', 'Application', 'Conclusion'];
5
+ function validate(sections) {
6
+ if (!sections || Object.keys(sections).length !== 5 || keys.some(k => typeof sections[k] !== 'string' || !sections[k].trim()))
7
+ throw Error('Five complete sections are required.');
8
+ const counts = keys.map(k => sections[k].trim().split(/\s+/u).length);
9
+ const count = counts.reduce((n, value) => n + value, 0);
10
+ if (counts.some(value => value !== 80))
11
+ throw Error(`Each section must have exactly 80 words (400 total). Actual counts: ${counts.join(', ')}.`);
12
+ return count;
13
+ }
14
+ function metadata(text) {
15
+ const number = text.match(/Reflective\s+Journal\s+(\d+)\b/i);
16
+ const subject = text.match(/Subject\s*:\s*([\s\S]*?)(?:Description\s*:|Due Date\s*:)/i);
17
+ if (!number || !subject)
18
+ return null;
19
+ return { number: Number(number[1]), subject: subject[1].replace(/\s+/g, ' ').trim() };
20
+ }
21
+ function readableText(root) {
22
+ const parts = [];
23
+ function visit(node) {
24
+ if (node.nodeType === 3) {
25
+ parts.push(node.textContent);
26
+ return;
27
+ }
28
+ if (node.nodeType !== 1 || node.matches('#assistant-root,#assistant-journal,script,style,textarea'))
29
+ return;
30
+ for (const child of node.childNodes)
31
+ visit(child);
32
+ }
33
+ visit(root);
34
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
35
+ }
36
+ function pageText(doc) { return readableText(doc.body); }
37
+ function fields(doc) {
38
+ const result = {};
39
+ for (const field of doc.querySelectorAll('textarea,[contenteditable="true"],[contenteditable=""],[role="textbox"]')) {
40
+ if (field.closest('#assistant-root, #assistant-journal') || field.disabled || field.readOnly || !field.getClientRects().length)
41
+ continue;
42
+ if (field.tagName !== 'TEXTAREA' && !field.hasAttribute('contenteditable') && !field.isContentEditable)
43
+ continue;
44
+ let node = field.parentElement;
45
+ for (let depth = 0; node && depth < 6; depth++, node = node.parentElement) {
46
+ const headings = [...node.querySelectorAll('h1,h2,h3,h4,h5,h6,label')];
47
+ const clone = node.cloneNode(true);
48
+ for (const input of clone.querySelectorAll('textarea,[contenteditable],[role="textbox"]'))
49
+ input.remove();
50
+ const text = headings.length ? headings.map(h => h.textContent).join(' ') : readableText(clone);
51
+ const matches = labels.filter(label => new RegExp(`\\b${label}\\b`, 'i').test(text));
52
+ if (matches.length === 1) {
53
+ const key = matches[0].toLowerCase();
54
+ if (result[key] && result[key] !== field)
55
+ throw Error(`Ambiguous ${key} field.`);
56
+ result[key] = field;
57
+ break;
58
+ }
59
+ }
60
+ }
61
+ return keys.every(k => result[k]) ? result : null;
62
+ }
63
+ function readField(field) {
64
+ return field.tagName === 'TEXTAREA' ? field.value : (field.innerText ?? field.textContent ?? '');
65
+ }
66
+ function remainingSeconds(doc) {
67
+ const excluded = node => node.closest('#assistant-root,#assistant-journal,textarea,[contenteditable],script,style');
68
+ const clock = value => {
69
+ const match = value.trim().match(/^(\d{1,3}):([0-5]\d)(?::([0-5]\d))?$/);
70
+ return match ? (match[3] === undefined ? Number(match[1]) * 60 + Number(match[2])
71
+ : Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3])) : null;
72
+ };
73
+ const labeled = value => {
74
+ const match = value.replace(/\s+/g, ' ').trim().match(/^(?:Time(?:\s+(?:Remaining|Left))?|Remaining\s+Time)\s*:?\s*(\d{1,3}:[0-5]\d(?::[0-5]\d)?)$/i);
75
+ return match ? clock(match[1]) : null;
76
+ };
77
+ const explicit = new Set(), values = new Set();
78
+ for (const node of doc.querySelectorAll('[role="timer"],[data-timer],[data-testid*="timer"]')) {
79
+ if (excluded(node) || !node.getClientRects().length)
80
+ continue;
81
+ const value = clock(node.textContent) ?? labeled(node.textContent);
82
+ if (value !== null)
83
+ explicit.add(value);
84
+ }
85
+ if (explicit.size)
86
+ return explicit.size === 1 ? [...explicit][0] : null;
87
+ for (const node of doc.querySelectorAll('span,div,p,output,strong,b')) {
88
+ if (excluded(node) || !node.getClientRects().length)
89
+ continue;
90
+ const value = labeled(node.textContent);
91
+ if (value !== null)
92
+ values.add(value);
93
+ }
94
+ return values.size === 1 ? [...values][0] : null;
95
+ }
96
+ function writeField(field, value) {
97
+ const win = field.ownerDocument.defaultView;
98
+ if (field.tagName === 'TEXTAREA') {
99
+ Object.getOwnPropertyDescriptor(win.HTMLTextAreaElement.prototype, 'value').set.call(field, value);
100
+ }
101
+ else {
102
+ field.textContent = value;
103
+ }
104
+ const event = win.InputEvent ? new win.InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }) : new win.Event('input', { bubbles: true });
105
+ field.dispatchEvent(event);
106
+ field.dispatchEvent(new win.Event('change', { bubbles: true }));
107
+ }
108
+ const api = { validate, metadata, fields, pageText, readField, writeField, remainingSeconds };
109
+ if (typeof module !== 'undefined')
110
+ module.exports = api;
111
+ if (!global.document || global.location?.origin !== 'https://admissions.aurora.edu.in')
112
+ return;
113
+ if (!/^\/modules\/reflective-journal\//.test(global.location.pathname))
114
+ return;
115
+ const existing = document.getElementById('assistant-journal');
116
+ const journalBuild = global.chrome?.runtime?.getManifest?.().version || 'development';
117
+ if (existing?.dataset.build === journalBuild)
118
+ return;
119
+ if (existing && global.BrowserAssistantJournalBusy)
120
+ return;
121
+ existing?.remove();
122
+ const storageKey = `assistantJournal:${location.pathname}`;
123
+ let info = null, draft = null, running = false, submitting = false, paused = false, starting = false, startIssued = false, generation = 0, idleState = 'READY';
124
+ let filled = false, waitingForTimer = false;
125
+ try {
126
+ info = JSON.parse(sessionStorage.getItem(storageKey));
127
+ }
128
+ catch { }
129
+ const panel = document.createElement('section');
130
+ panel.id = 'assistant-journal';
131
+ panel.dataset.build = journalBuild;
132
+ panel.style.cssText = 'position:fixed;right:16px;top:160px;width:290px;z-index:2147483646;background:white;color:#17233d;padding:16px;border:2px solid #7638ff;border-radius:12px;box-shadow:0 4px 20px #0003;font:14px Arial';
133
+ panel.innerHTML = '<strong>Reflective Journal</strong><p data-info></p><textarea aria-label="Your class experience and feelings" placeholder="Your actual class experience / feelings (optional)" rows="3" style="width:100%;box-sizing:border-box"></textarea><button type="button" style="margin-top:8px;padding:10px">Fill &amp; Submit Journal</button><p role="status" aria-live="polite"></p>';
134
+ document.body.appendChild(panel);
135
+ const controls = document.createElement('div');
136
+ controls.innerHTML = '<label>Subject <select data-subject aria-label="Journal subject"><option value="">Detect from page</option><option>Backend Development Fundamentals</option><option>Computer Organization and Architecture</option><option>Advanced Data Structures</option><option>Linear Algebra Essentials</option><option>Fundamentals of Artificial Intelligence</option></select></label><label>Module / Journal <select data-number aria-label="Journal number"><option value="">Detect from page</option>' + Array.from({ length: 10 }, (_, i) => `<option value="${i + 1}">${i + 1}</option>`).join('') + '</select></label>';
137
+ for (const label of controls.querySelectorAll('label'))
138
+ label.style.cssText = 'display:block;margin:8px 0';
139
+ for (const select of controls.querySelectorAll('select'))
140
+ select.style.cssText = 'display:block;width:100%;padding:5px;color:#17233d;background:white;border:1px solid #abb3c3';
141
+ panel.insertBefore(controls, panel.querySelector('textarea'));
142
+ const status = panel.querySelector('[role=status]'), button = panel.querySelector('button');
143
+ button.title = 'Fill & Submit Journal (Alt + Shift + J)';
144
+ button.setAttribute('aria-keyshortcuts', 'Alt+Shift+J');
145
+ const shortcutHint = document.createElement('p');
146
+ shortcutHint.textContent = 'Shortcut: Alt + Shift + J';
147
+ shortcutHint.style.cssText = 'font-size:12px;margin:6px 0;color:#516078';
148
+ panel.insertBefore(shortcutHint, status);
149
+ const shortcutListener = event => {
150
+ if (!event.altKey || !event.shiftKey || event.ctrlKey || event.metaKey || event.repeat || event.isComposing)
151
+ return;
152
+ if (event.code !== 'KeyJ' && String(event.key).toLowerCase() !== 'j')
153
+ return;
154
+ if (!document.contains(panel) || storageKey !== `assistantJournal:${location.pathname}`)
155
+ return;
156
+ event.preventDefault();
157
+ if (!running && !button.disabled)
158
+ button.click();
159
+ };
160
+ document.addEventListener('keydown', shortcutListener, true);
161
+ function notify() {
162
+ document.dispatchEvent(new global.Event('assistant-journal-update'));
163
+ }
164
+ const controller = {
165
+ path: location.pathname,
166
+ start() { button.click(); },
167
+ stop() {
168
+ generation++;
169
+ draft = null;
170
+ running = false;
171
+ paused = false;
172
+ submitting = false;
173
+ starting = false;
174
+ filled = false;
175
+ waitingForTimer = false;
176
+ idleState = 'STOPPED';
177
+ global.BrowserAssistantJournalBusy = false;
178
+ button.disabled = false;
179
+ status.textContent = 'Stopped. Any text already entered is retained. A submission already sent cannot be cancelled.';
180
+ notify();
181
+ },
182
+ pause() { if (!running || submitting)
183
+ return; paused = !paused; notify(); },
184
+ mount(slot) {
185
+ if (!document.getElementById('assistant-root'))
186
+ return;
187
+ if (slot) {
188
+ if (panel.parentElement !== slot)
189
+ slot.appendChild(panel);
190
+ panel.style.cssText = 'display:block;position:static;width:100%;box-sizing:border-box;padding:10px;border:1px solid #6c58aa;border-radius:8px;background:#fff;color:#17233d;font:13px Arial;margin:10px 0;';
191
+ button.hidden = true;
192
+ }
193
+ else
194
+ controller.detach();
195
+ },
196
+ detach() {
197
+ if (document.getElementById('assistant-root')) {
198
+ if (panel.parentElement !== document.body)
199
+ document.body.appendChild(panel);
200
+ panel.style.display = 'none';
201
+ }
202
+ },
203
+ getState() {
204
+ return { running, paused, submitting,
205
+ state: paused ? 'PAUSED' : submitting ? 'SUBMITTING' : starting ? 'STARTING_ASSESSMENT' : waitingForTimer ? 'WAITING_FOR_TIMER' : running ? (draft ? 'WAITING_FOR_FORM' : 'PREPARING') : /confirmed by the page/.test(status.textContent) ? 'COMPLETED' : idleState,
206
+ message: paused ? 'Paused. Resume to continue filling and submission.' : status.textContent || 'Start fills 400 words (80 per section), then submits when the portal timer reaches 30:00 remaining.',
207
+ title: info ? `${info.subject} · Module ${info.number}` : 'Select subject and module',
208
+ completed: /confirmed by the page/.test(status.textContent) ? 1 : 0 };
209
+ },
210
+ };
211
+ if (global.BrowserAssistantJournal) {
212
+ global.BrowserAssistantJournal.current = controller;
213
+ global.BrowserAssistantJournal.opening = false;
214
+ }
215
+ function assertCurrentPage() {
216
+ if (!document.contains(panel) || storageKey !== `assistantJournal:${location.pathname}`)
217
+ throw Error('Journal page changed. Filling and submission stopped.');
218
+ if (/assessment not found/i.test(document.querySelector('main')?.textContent || ''))
219
+ throw Error('Assessment is unavailable. Return to Assessments and check its status.');
220
+ }
221
+ function refreshInfo() {
222
+ const found = metadata(pageText(document));
223
+ if (found) {
224
+ info = found;
225
+ try {
226
+ sessionStorage.setItem(storageKey, JSON.stringify(info));
227
+ }
228
+ catch { }
229
+ }
230
+ const subject = controls.querySelector('[data-subject]').value;
231
+ const number = Number(controls.querySelector('[data-number]').value);
232
+ if (!found && !controls.hidden && subject && number >= 1 && number <= 10) {
233
+ info = { subject, number };
234
+ try {
235
+ sessionStorage.setItem(storageKey, JSON.stringify(info));
236
+ }
237
+ catch { }
238
+ }
239
+ controls.hidden = Boolean(info);
240
+ panel.querySelector('[data-info]').textContent = info ? `${info.subject} · Module ${info.number} · 400 words (80 per section)` : 'Subject not detected yet. Select the subject and journal number below, or open Module Information.';
241
+ }
242
+ async function fillAndSubmit() {
243
+ if (!draft || submitting || paused || starting)
244
+ return;
245
+ const run = generation;
246
+ assertCurrentPage();
247
+ let inputs = fields(document);
248
+ if (!inputs) {
249
+ starting = true;
250
+ try {
251
+ // The user explicitly requested automatic declaration/start handling.
252
+ const declarations = [...document.querySelectorAll('input[type="checkbox"]')].filter(box => {
253
+ if (box.closest('#assistant-root,#assistant-journal') || box.disabled || !box.getClientRects().length)
254
+ return false;
255
+ const associated = [...(box.labels || []), ...document.querySelectorAll('label')].filter(label => label.contains(box) || (box.id && label.getAttribute('for') === box.id));
256
+ const text = [box.getAttribute('aria-label') || '', ...associated.map(label => label.textContent)].join(' ').replace(/\s+/g, ' ');
257
+ return /I confirm this is my own original work/i.test(text);
258
+ });
259
+ if (!startIssued) {
260
+ if (declarations.length !== 1)
261
+ throw Error('Could not uniquely identify the journal declaration checkbox. Start stopped.');
262
+ if (run !== generation || paused)
263
+ return;
264
+ status.textContent = 'Accepting the journal declaration and starting the assessment…';
265
+ notify();
266
+ if (!declarations[0].checked)
267
+ declarations[0].click();
268
+ await new Promise(resolve => setTimeout(resolve, 250));
269
+ if (run !== generation || paused)
270
+ return;
271
+ assertCurrentPage();
272
+ const starts = [...document.querySelectorAll('button')].filter(b => !b.closest('#assistant-root,#assistant-journal') && b.getClientRects().length && !b.disabled && b.getAttribute('aria-disabled') !== 'true' && /^(?:Start Assessment|Accept Terms to Start)$/i.test(b.textContent.trim()));
273
+ if (starts.length !== 1)
274
+ throw Error('Journal Start Assessment button is unavailable or ambiguous.');
275
+ startIssued = true;
276
+ starts[0].click();
277
+ }
278
+ for (let attempt = 0; attempt < 30; attempt++) {
279
+ if (run !== generation || paused)
280
+ return;
281
+ assertCurrentPage();
282
+ inputs = fields(document);
283
+ if (inputs)
284
+ break;
285
+ await new Promise(resolve => setTimeout(resolve, 500));
286
+ }
287
+ if (!inputs)
288
+ throw Error('The journal started, but all five writing fields did not appear. Submission stopped.');
289
+ }
290
+ finally {
291
+ if (run === generation)
292
+ starting = false;
293
+ }
294
+ }
295
+ if (run !== generation || paused)
296
+ return;
297
+ if (!inputs)
298
+ return;
299
+ submitting = true;
300
+ try {
301
+ validate(draft.sections);
302
+ if (!filled) {
303
+ // Existing complete text is adopted only by an explicit Start.
304
+ if (keys.some(k => readField(inputs[k]).trim()))
305
+ throw Error('Journal already contains text. Nothing was overwritten.');
306
+ for (const key of keys)
307
+ writeField(inputs[key], draft.sections[key]);
308
+ await new Promise(resolve => setTimeout(resolve, 700));
309
+ if (run !== generation)
310
+ return;
311
+ filled = true;
312
+ }
313
+ assertCurrentPage();
314
+ const current = fields(document);
315
+ if (!current || keys.some(k => readField(current[k]).trim() !== draft.sections[k].trim()))
316
+ throw Error('The page did not retain all journal fields. Submission stopped.');
317
+ const count = validate(Object.fromEntries(keys.map(k => [k, readField(current[k])])));
318
+ const remaining = remainingSeconds(document);
319
+ if (remaining === null || remaining > 30 * 60) {
320
+ waitingForTimer = true;
321
+ const time = remaining === null ? 'Waiting for the portal timer to become readable.'
322
+ : `Current timer: ${String(Math.floor(remaining / 60)).padStart(2, '0')}:${String(remaining % 60).padStart(2, '0')}.`;
323
+ status.textContent = `${count} words ready. Waiting to submit at 30:00 remaining. ${time}`;
324
+ return;
325
+ }
326
+ if (paused)
327
+ return;
328
+ waitingForTimer = false;
329
+ if (remaining === 0)
330
+ throw Error('The portal timer expired. Check the journal status; no automatic click was sent.');
331
+ const candidates = [...document.querySelectorAll('button')].filter(b => !panel.contains(b) && b.getClientRects().length && !b.disabled && b.getAttribute('aria-disabled') !== 'true' && /^Submit Journal$/i.test(b.textContent.trim()));
332
+ if (candidates.length !== 1)
333
+ throw Error('Could not uniquely identify an enabled Submit Journal button. Content is filled; submit manually.');
334
+ // Consume the draft before clicking: observers must never submit twice.
335
+ draft = null;
336
+ candidates[0].click();
337
+ status.textContent = `${count} words filled. Submit clicked; checking the result…`;
338
+ await new Promise(resolve => setTimeout(resolve, 1000));
339
+ if (run !== generation)
340
+ return;
341
+ const dialogs = [...document.querySelectorAll('[role=dialog],dialog[open]')].filter(d => d.getClientRects().length);
342
+ const confirm = dialogs.flatMap(d => [...d.querySelectorAll('button')]).filter(b => !b.disabled && /^(?:Confirm(?: Submission| Submit)?|Yes,? Submit|Submit Journal)$/i.test(b.textContent.trim()));
343
+ if (confirm.length === 1)
344
+ confirm[0].click();
345
+ await new Promise(resolve => setTimeout(resolve, 1200));
346
+ if (run !== generation)
347
+ return;
348
+ const page = document.body.cloneNode(true);
349
+ page.querySelector('#assistant-journal')?.remove();
350
+ status.textContent = /(?:journal|reflection)\s+(?:has been\s+)?submitted successfully|successfully submitted|submission successful/i.test(page.textContent)
351
+ ? `${count} words. Submission confirmed by the page.`
352
+ : `${count} words filled; Submit clicked. Check the page for confirmation or any remaining dialog.`;
353
+ }
354
+ catch (error) {
355
+ if (run === generation) {
356
+ draft = null;
357
+ filled = false;
358
+ waitingForTimer = false;
359
+ idleState = 'ERROR';
360
+ status.textContent = error.message;
361
+ }
362
+ }
363
+ finally {
364
+ if (run === generation) {
365
+ submitting = false;
366
+ if (!waitingForTimer) {
367
+ running = false;
368
+ global.BrowserAssistantJournalBusy = false;
369
+ button.disabled = false;
370
+ }
371
+ notify();
372
+ }
373
+ }
374
+ }
375
+ button.addEventListener('click', async () => {
376
+ if (running)
377
+ return;
378
+ try {
379
+ assertCurrentPage();
380
+ }
381
+ catch (error) {
382
+ status.textContent = error.message;
383
+ return;
384
+ }
385
+ refreshInfo();
386
+ const manualSubject = controls.querySelector('[data-subject]').value;
387
+ const manualNumber = controls.querySelector('[data-number]').value;
388
+ if (!controls.hidden && Boolean(manualSubject) !== Boolean(manualNumber)) {
389
+ status.textContent = 'For manual selection, choose both Subject and Module / Journal.';
390
+ return;
391
+ }
392
+ if (!info) {
393
+ status.textContent = 'Select both Subject and Module / Journal above, then click Fill & Submit Journal.';
394
+ return;
395
+ }
396
+ running = true;
397
+ global.BrowserAssistantJournalBusy = true;
398
+ submitting = false;
399
+ button.disabled = true;
400
+ idleState = 'READY';
401
+ paused = false;
402
+ filled = false;
403
+ waitingForTimer = false;
404
+ startIssued = false;
405
+ const run = ++generation;
406
+ status.textContent = 'Preparing the matching module journal…';
407
+ notify();
408
+ try {
409
+ const existingInputs = fields(document);
410
+ if (existingInputs && keys.some(key => readField(existingInputs[key]).trim())) {
411
+ const existingSections = Object.fromEntries(keys.map(key => [key, readField(existingInputs[key]).trim()]));
412
+ validate(existingSections);
413
+ draft = { sections: existingSections };
414
+ filled = true;
415
+ }
416
+ else {
417
+ const response = await chrome.runtime.sendMessage({ action: 'apiRequest', path: '/journal', body: { ...info, notes: panel.querySelector('textarea').value } });
418
+ if (run !== generation)
419
+ return;
420
+ assertCurrentPage();
421
+ if (!response?.ok) {
422
+ const detail = response?.data?.detail || response?.error || 'Journal generation failed.';
423
+ if (typeof detail === 'string' && detail.includes('Subject does not uniquely match the supplied syllabus')) {
424
+ throw Error('The running server could not match this subject. If you just added this syllabus, restart the updated server once, then try again. Otherwise check the selected subject. No journal was submitted.');
425
+ }
426
+ throw Error(typeof detail === 'string' ? detail : detail.message || 'Journal generation failed.');
427
+ }
428
+ validate(response.data.sections);
429
+ draft = response.data;
430
+ }
431
+ status.textContent = 'Content ready. Filling the journal, then waiting for 30:00 remaining before submission…';
432
+ notify();
433
+ await fillAndSubmit();
434
+ }
435
+ catch (error) {
436
+ if (run !== generation)
437
+ return;
438
+ draft = null;
439
+ filled = false;
440
+ waitingForTimer = false;
441
+ running = false;
442
+ idleState = 'ERROR';
443
+ global.BrowserAssistantJournalBusy = false;
444
+ button.disabled = false;
445
+ status.textContent = String(error.message);
446
+ notify();
447
+ }
448
+ });
449
+ refreshInfo();
450
+ controller.mount(document.getElementById('assistant-journal-slot'));
451
+ document.dispatchEvent(new global.Event('assistant-journal-ready'));
452
+ try {
453
+ const pending = JSON.parse(sessionStorage.getItem('assistantPendingJournal') || 'null');
454
+ if (pending?.path === location.pathname && Date.now() - pending.createdAt < 300000) {
455
+ sessionStorage.removeItem('assistantPendingJournal');
456
+ button.click();
457
+ }
458
+ }
459
+ catch { /* Opening the journal without auto-start remains available. */ }
460
+ // Poll only the journal page; field discovery never changes platform restrictions.
461
+ const timer = setInterval(() => {
462
+ if (global.BrowserAssistantJournal?.current !== controller && global.BrowserAssistantJournal) {
463
+ clearInterval(timer);
464
+ document.removeEventListener('keydown', shortcutListener, true);
465
+ return;
466
+ }
467
+ if (!document.contains(panel)) {
468
+ draft = null;
469
+ global.BrowserAssistantJournalBusy = false;
470
+ document.removeEventListener('keydown', shortcutListener, true);
471
+ clearInterval(timer);
472
+ return;
473
+ }
474
+ if (!/^\/modules\/reflective-journal\//.test(location.pathname) || storageKey !== `assistantJournal:${location.pathname}`) {
475
+ draft = null;
476
+ global.BrowserAssistantJournalBusy = false;
477
+ document.removeEventListener('keydown', shortcutListener, true);
478
+ clearInterval(timer);
479
+ panel.remove();
480
+ return;
481
+ }
482
+ if (!info)
483
+ refreshInfo();
484
+ notify();
485
+ if (draft && !submitting && !starting) {
486
+ const run = generation;
487
+ void fillAndSubmit().catch(error => {
488
+ if (run !== generation)
489
+ return;
490
+ draft = null;
491
+ filled = false;
492
+ waitingForTimer = false;
493
+ running = false;
494
+ idleState = 'ERROR';
495
+ global.BrowserAssistantJournalBusy = false;
496
+ button.disabled = false;
497
+ status.textContent = error.message;
498
+ notify();
499
+ });
500
+ }
501
+ }, 1000);
502
+ }
503
+ if (typeof window !== 'undefined' && location.origin === 'https://admissions.aurora.edu.in') {
504
+ window.BrowserAssistantJournal = window.BrowserAssistantJournal || {
505
+ current: null,
506
+ opening: false,
507
+ matches: () => /^\/modules\/(?:reflective-journal\/|assessments\/assessments\/?$)/.test(location.pathname),
508
+ active() { return this.current?.path === location.pathname ? this.current : null; },
509
+ getState() { return this.active()?.getState() || { state: this.opening ? 'OPENING_JOURNAL' : 'OPEN_JOURNAL', running: this.opening, completed: 0, message: 'Click Start to open the available ongoing journal. If several are available, select its pencil button first.' }; },
510
+ mount(slot) { this.active()?.mount(slot); },
511
+ detach() { this.current?.detach(); },
512
+ stop() { this.active()?.stop(); this.opening = false; try {
513
+ sessionStorage.removeItem('assistantPendingJournal');
514
+ }
515
+ catch { } },
516
+ pause() { this.active()?.pause(); },
517
+ start() {
518
+ if (this.active())
519
+ return this.active().start();
520
+ if (/^\/modules\/reflective-journal\//.test(location.pathname)) {
521
+ browserAssistantJournal(window);
522
+ return this.active()?.start();
523
+ }
524
+ if (this.opening)
525
+ return;
526
+ const links = [...document.querySelectorAll('a[href*="/modules/reflective-journal/"]')].filter(link => [...(link.closest('tr,[role="row"]')?.querySelectorAll('*') || [])].some(node => /^Ongoing$/i.test(node.textContent.trim())) && link.getClientRects().length
527
+ && new URL(link.href, location.origin).origin === location.origin
528
+ && /^\/modules\/reflective-journal\//.test(new URL(link.href, location.origin).pathname));
529
+ if (links.length === 1) {
530
+ this.opening = true;
531
+ try {
532
+ sessionStorage.setItem('assistantPendingJournal', JSON.stringify({ path: new URL(links[0].href, location.origin).pathname, createdAt: Date.now() }));
533
+ }
534
+ catch { }
535
+ links[0].click();
536
+ }
537
+ },
538
+ };
539
+ if (!window.BrowserAssistantJournalListShortcut) {
540
+ window.BrowserAssistantJournalListShortcut = true;
541
+ document.addEventListener('keydown', event => {
542
+ if (!/^\/modules\/assessments\/assessments\/?$/.test(location.pathname) || !event.altKey || !event.shiftKey || event.ctrlKey || event.metaKey || event.repeat || event.isComposing)
543
+ return;
544
+ if (event.code !== 'KeyJ' && String(event.key).toLowerCase() !== 'j')
545
+ return;
546
+ event.preventDefault();
547
+ window.BrowserAssistantJournal.start();
548
+ document.dispatchEvent(new window.Event('assistant-journal-update'));
549
+ }, true);
550
+ }
551
+ }
552
+ browserAssistantJournal(typeof window === 'undefined' ? globalThis : window);
553
+ if (typeof window !== 'undefined' && location.origin === 'https://admissions.aurora.edu.in' && !window.BrowserAssistantJournalBoot) {
554
+ window.BrowserAssistantJournalBoot = true;
555
+ setInterval(() => {
556
+ if (/^\/modules\/reflective-journal\//.test(location.pathname) && !document.getElementById('assistant-journal'))
557
+ browserAssistantJournal(window);
558
+ }, 1000);
559
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Browser Assistant",
4
+ "version": "2.8.2",
5
+ "description": "Scans webpages for MCQs and coding problems. Powered by Groq (cloud) and Ollama (local fallback).",
6
+ "permissions": [
7
+ "activeTab",
8
+ "scripting",
9
+ "tabs",
10
+ "clipboardWrite",
11
+ "alarms"
12
+ ],
13
+ "background": {
14
+ "service_worker": "background.js"
15
+ },
16
+ "action": {
17
+ "default_popup": "popup.html"
18
+ },
19
+ "content_scripts": [
20
+ {
21
+ "matches": [
22
+ "<all_urls>"
23
+ ],
24
+ "js": [
25
+ "automation-policy.js",
26
+ "editor-bridge.js",
27
+ "question-visuals.js",
28
+ "answer-requests.js",
29
+ "coding-automation.js",
30
+ "coding-batch.js",
31
+ "content.js",
32
+ "journal-automation.js"
33
+ ],
34
+ "css": [
35
+ "styles.css"
36
+ ]
37
+ }
38
+ ],
39
+ "host_permissions": [
40
+ "http://localhost:8000/*",
41
+ "http://127.0.0.1:8000/*",
42
+ "<all_urls>"
43
+ ]
44
+ }
@@ -0,0 +1,61 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Browser Assistant</title>
7
+ <style>
8
+ body {
9
+ width: 220px;
10
+ padding: 12px 14px;
11
+ font-family: 'Inter', system-ui, sans-serif;
12
+ background: #0f0f0f;
13
+ color: #eee;
14
+ margin: 0;
15
+ }
16
+ .logo {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 8px;
20
+ margin-bottom: 10px;
21
+ }
22
+ .logo-dot {
23
+ width: 10px;
24
+ height: 10px;
25
+ border-radius: 50%;
26
+ background: #6e8efb;
27
+ box-shadow: 0 0 6px #6e8efb;
28
+ }
29
+ h3 { margin: 0; font-size: 14px; color: #fff; }
30
+ p { font-size: 11px; color: #888; margin: 0 0 10px; line-height: 1.5; }
31
+ .status { font-size: 11px; color: #555; }
32
+ .status span { color: #10b981; font-weight: 600; }
33
+ .shortcuts { margin-top: 10px; border-top: 1px solid #222; padding-top: 8px; }
34
+ .shortcut-row { display: flex; justify-content: space-between; font-size: 10px; color: #666; margin-bottom: 4px; }
35
+ kbd {
36
+ background: #222; border: 1px solid #444; border-radius: 3px;
37
+ padding: 1px 5px; font-size: 10px; color: #bbb;
38
+ }
39
+ #server-status { color: #f59e0b; }
40
+ #server-status.online { color: #10b981; }
41
+ #server-status.offline { color: #ef4444; }
42
+ </style>
43
+ </head>
44
+ <body>
45
+ <div class="logo">
46
+ <div class="logo-dot"></div>
47
+ <h3>Browser Assistant</h3>
48
+ </div>
49
+ <p>Extension active. The floating panel appears on every page.</p>
50
+ <div class="status">Server: <span id="server-status">checking...</span></div>
51
+ <div class="shortcuts">
52
+ <div class="shortcut-row"><span>Open panel</span> <kbd>`</kbd></div>
53
+ <div class="shortcut-row"><span>Close panel</span> <kbd>]</kbd></div>
54
+ <div class="shortcut-row"><span>Vision scan</span> <kbd>\</kbd></div>
55
+ <div class="shortcut-row"><span>Minimal mode</span> <kbd>[</kbd></div>
56
+ <div class="shortcut-row"><span>Journal fill &amp; submit</span> <kbd>Alt+Shift+J</kbd></div>
57
+ <div class="shortcut-row"><span>Automation start / pause / resume</span> <kbd>Alt+Shift+A</kbd></div>
58
+ </div>
59
+ <script src="popup.js"></script>
60
+ </body>
61
+ </html>