mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
Small refactor
This commit is contained in:
@@ -8,10 +8,7 @@ import { type Plugin } from "@/plugins/core/types";
|
|||||||
import stringToHTML from "@/seqta/utils/stringToHTML";
|
import stringToHTML from "@/seqta/utils/stringToHTML";
|
||||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
||||||
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts";
|
import { initStorage, clearStuck, getClassByPattern, parseGrade, createWeightLabel, parseAssessments, letterToNumber} from "./utils.ts";
|
||||||
import * as pdfjs from "pdfjs-dist";
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc =
|
|
||||||
"https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.worker.min.mjs";
|
|
||||||
|
|
||||||
// Storage
|
// Storage
|
||||||
interface weightingsStorage {
|
interface weightingsStorage {
|
||||||
@@ -43,26 +40,12 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
settings: instance.settings,
|
settings: instance.settings,
|
||||||
|
|
||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
await api.storage.loaded;
|
|
||||||
|
|
||||||
if (!api.storage.weightings) {
|
// Ensure storage is ready for use
|
||||||
api.storage.weightings = {};
|
await initStorage(api);
|
||||||
}
|
|
||||||
if (!api.storage.assessments) {
|
|
||||||
api.storage.assessments = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear any stuck "processing" states so they can retry
|
// Clear any stuck "processing" states so they can retry
|
||||||
let hasStuckProcessing = false;
|
clearStuck(api);
|
||||||
for (const key in api.storage.weightings) {
|
|
||||||
if (api.storage.weightings[key] === "processing") {
|
|
||||||
delete api.storage.weightings[key];
|
|
||||||
hasStuckProcessing = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasStuckProcessing) {
|
|
||||||
api.storage.weightings = { ...api.storage.weightings };
|
|
||||||
}
|
|
||||||
|
|
||||||
api.seqta.onMount(".assessmentsWrapper", async () => {
|
api.seqta.onMount(".assessmentsWrapper", async () => {
|
||||||
await waitForElm(
|
await waitForElm(
|
||||||
@@ -74,19 +57,6 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
|
|
||||||
await parseAssessments(api);
|
await parseAssessments(api);
|
||||||
|
|
||||||
// Helper function to find actual class names by their base pattern
|
|
||||||
const getClassByPattern = (
|
|
||||||
element: Element | Document,
|
|
||||||
basePattern: string,
|
|
||||||
): string => {
|
|
||||||
// Find all classes on the element
|
|
||||||
const classes = Array.from(element.querySelectorAll("*"))
|
|
||||||
.flatMap((el) => Array.from(el.classList))
|
|
||||||
.filter((className) => className.startsWith(basePattern));
|
|
||||||
|
|
||||||
return classes.length ? classes[0] : "";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find actual class names from the DOM
|
// Find actual class names from the DOM
|
||||||
const sampleAssessmentItem = document.querySelector(
|
const sampleAssessmentItem = document.querySelector(
|
||||||
"[class*='AssessmentItem__AssessmentItem___']",
|
"[class*='AssessmentItem__AssessmentItem___']",
|
||||||
@@ -149,36 +119,6 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
if (!marks || !marks.length) return;
|
if (!marks || !marks.length) return;
|
||||||
|
|
||||||
// Parse and average grades
|
// Parse and average grades
|
||||||
const letterToNumber: Record<string, number> = {
|
|
||||||
"A+": 100,
|
|
||||||
A: 95,
|
|
||||||
"A-": 90,
|
|
||||||
"B+": 85,
|
|
||||||
B: 80,
|
|
||||||
"B-": 75,
|
|
||||||
"C+": 70,
|
|
||||||
C: 65,
|
|
||||||
"C-": 60,
|
|
||||||
"D+": 55,
|
|
||||||
D: 50,
|
|
||||||
"D-": 45,
|
|
||||||
"E+": 40,
|
|
||||||
E: 35,
|
|
||||||
"E-": 30,
|
|
||||||
F: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseGrade(text: string): number {
|
|
||||||
const str = text.trim().toUpperCase();
|
|
||||||
if (str.includes("/")) {
|
|
||||||
const [raw, max] = str.split("/").map((n) => parseFloat(n));
|
|
||||||
return (raw / max) * 100;
|
|
||||||
}
|
|
||||||
if (str.includes("%")) {
|
|
||||||
return parseFloat(str.replace("%", "")) || 0;
|
|
||||||
}
|
|
||||||
return letterToNumber[str] ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all assessment items (excluding the average we might have added)
|
// Get all assessment items (excluding the average we might have added)
|
||||||
const assessmentItems = Array.from(
|
const assessmentItems = Array.from(
|
||||||
@@ -219,55 +159,9 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
? api.storage.weightings?.[assessmentID]
|
? api.storage.weightings?.[assessmentID]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const statsContainer = assessmentItem.querySelector(
|
|
||||||
`[class*='AssessmentItem__stats___']`,
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
// Creates a weighting label next to the average score
|
// Creates a weighting label next to the average score
|
||||||
if (statsContainer) {
|
createWeightLabel(assessmentItem, weighting)
|
||||||
// Only add label if it hasn't been added before
|
|
||||||
if (!statsContainer.querySelector(".betterseqta-weight-label")) {
|
|
||||||
const label = statsContainer.querySelector(
|
|
||||||
`[class*='Label__Label___']`,
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
if (label) {
|
|
||||||
// Clone average score node
|
|
||||||
const weightLabel = label.cloneNode(true) as HTMLElement;
|
|
||||||
|
|
||||||
// Mark as added to prevent duplicates
|
|
||||||
weightLabel.classList.add("betterseqta-weight-label");
|
|
||||||
|
|
||||||
const innerTextDiv = weightLabel.querySelector(
|
|
||||||
`[class*='Label__innerText___']`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Repurpose for showing weight
|
|
||||||
if (innerTextDiv) innerTextDiv.textContent = "Weight";
|
|
||||||
|
|
||||||
const textNodes = Array.from(weightLabel.childNodes).filter(
|
|
||||||
(node) => node.nodeType === Node.TEXT_NODE,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set weight value, discarding useless decimals (.0)
|
|
||||||
if (textNodes.length) {
|
|
||||||
textNodes[0].textContent =
|
|
||||||
weighting && weighting !== "processing"
|
|
||||||
? `${Number(weighting) % 1 === 0 ? Number(weighting) : weighting}%`
|
|
||||||
: "N/A";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set position opposite to the average score node
|
|
||||||
statsContainer.style.position = "relative";
|
|
||||||
weightLabel.style.position = "absolute";
|
|
||||||
weightLabel.style.right = "0";
|
|
||||||
weightLabel.style.top = "50%";
|
|
||||||
weightLabel.style.transform = "translateY(-50%)";
|
|
||||||
|
|
||||||
statsContainer.appendChild(weightLabel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if weighting is unavailable or still processing
|
// Check if weighting is unavailable or still processing
|
||||||
if (
|
if (
|
||||||
@@ -354,410 +248,4 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Detect Firefox (has stricter CSP for blob URLs)
|
|
||||||
// Use userAgent instead of deprecated InstallTrigger
|
|
||||||
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 &&
|
|
||||||
!navigator.userAgent.toLowerCase().includes('seamonkey') &&
|
|
||||||
!navigator.userAgent.toLowerCase().includes('waterfox');
|
|
||||||
|
|
||||||
async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
|
|
||||||
// Detect if URL is a blob URL
|
|
||||||
const isBlobUrl = url.startsWith('blob:');
|
|
||||||
|
|
||||||
// For Firefox, ALWAYS use page context to avoid any CSP issues
|
|
||||||
// For blob URLs in any browser, use page context
|
|
||||||
if (isBlobUrl || isFirefox) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Inject script into page context to fetch (bypasses Firefox CSP restrictions)
|
|
||||||
const script = document.createElement('script');
|
|
||||||
const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`;
|
|
||||||
|
|
||||||
// Escape URL for use in script
|
|
||||||
const escapedUrl = url.replace(/'/g, "\\'");
|
|
||||||
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
fetch('${escapedUrl}')
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
|
||||||
}
|
|
||||||
return response.arrayBuffer();
|
|
||||||
})
|
|
||||||
.then(arrayBuffer => {
|
|
||||||
window.postMessage({
|
|
||||||
type: '${requestId}',
|
|
||||||
success: true,
|
|
||||||
data: Array.from(new Uint8Array(arrayBuffer))
|
|
||||||
}, '*');
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
window.postMessage({
|
|
||||||
type: '${requestId}',
|
|
||||||
success: false,
|
|
||||||
error: error.message || String(error)
|
|
||||||
}, '*');
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
const messageHandler = (event: MessageEvent) => {
|
|
||||||
if (event.data?.type === requestId) {
|
|
||||||
window.removeEventListener('message', messageHandler);
|
|
||||||
if (script.parentNode) {
|
|
||||||
script.parentNode.removeChild(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.data.success) {
|
|
||||||
// Convert back to ArrayBuffer
|
|
||||||
const uint8Array = new Uint8Array(event.data.data);
|
|
||||||
resolve(uint8Array.buffer);
|
|
||||||
} else {
|
|
||||||
reject(new Error(event.data.error || 'Failed to fetch PDF'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('message', messageHandler);
|
|
||||||
(document.head || document.documentElement).appendChild(script);
|
|
||||||
|
|
||||||
// Timeout after 30 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
window.removeEventListener('message', messageHandler);
|
|
||||||
if (script.parentNode) {
|
|
||||||
script.parentNode.removeChild(script);
|
|
||||||
}
|
|
||||||
reject(new Error('Timeout fetching PDF'));
|
|
||||||
}, 30000);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Regular URL - fetch normally, but check if response URL becomes blob
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
credentials: 'include',
|
|
||||||
redirect: 'follow',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if response URL is a blob URL (server might redirect to blob)
|
|
||||||
if (response.url && response.url.startsWith('blob:')) {
|
|
||||||
// Re-fetch using page context
|
|
||||||
return await fetchPDFAsArrayBuffer(response.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch PDF: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.arrayBuffer();
|
|
||||||
} catch (error: any) {
|
|
||||||
// If error mentions blob or security, try using page context
|
|
||||||
if (error?.message?.includes('blob') || error?.message?.includes('Security') || error?.message?.includes('CSP')) {
|
|
||||||
// Force use page context
|
|
||||||
return await fetchPDFAsArrayBuffer(url);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractPDFText(url: string): Promise<string> {
|
|
||||||
// For Firefox, do everything in page context to avoid blob URL CSP issues
|
|
||||||
if (isFirefox) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
const requestId = `pdf-extract-${Date.now()}-${Math.random()}`;
|
|
||||||
|
|
||||||
// Escape URL for use in script (handle both single and double quotes)
|
|
||||||
const escapedUrl = url.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
|
|
||||||
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
const requestId = '${requestId}';
|
|
||||||
const url = '${escapedUrl}';
|
|
||||||
|
|
||||||
// Check if pdfjs is already loaded
|
|
||||||
if (window.pdfjsLib) {
|
|
||||||
extractPDF();
|
|
||||||
} else {
|
|
||||||
// Load pdfjs in page context
|
|
||||||
const pdfjsScript = document.createElement('script');
|
|
||||||
pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js';
|
|
||||||
pdfjsScript.type = 'text/javascript';
|
|
||||||
|
|
||||||
pdfjsScript.onload = function() {
|
|
||||||
extractPDF();
|
|
||||||
};
|
|
||||||
pdfjsScript.onerror = function() {
|
|
||||||
window.postMessage({
|
|
||||||
type: requestId,
|
|
||||||
success: false,
|
|
||||||
error: 'Failed to load pdfjs library'
|
|
||||||
}, '*');
|
|
||||||
};
|
|
||||||
|
|
||||||
document.head.appendChild(pdfjsScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractPDF() {
|
|
||||||
try {
|
|
||||||
// Disable worker for Firefox to avoid blob URL CSP issues
|
|
||||||
// Set to empty string to disable worker completely
|
|
||||||
window.pdfjsLib.GlobalWorkerOptions.workerSrc = '';
|
|
||||||
|
|
||||||
// Use XMLHttpRequest instead of fetch for better blob URL handling
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('GET', url, true);
|
|
||||||
xhr.responseType = 'arraybuffer';
|
|
||||||
xhr.withCredentials = true;
|
|
||||||
|
|
||||||
xhr.onload = function() {
|
|
||||||
if (xhr.status !== 200) {
|
|
||||||
window.postMessage({
|
|
||||||
type: requestId,
|
|
||||||
success: false,
|
|
||||||
error: 'HTTP ' + xhr.status + ': ' + xhr.statusText
|
|
||||||
}, '*');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = xhr.response;
|
|
||||||
if (!arrayBuffer || arrayBuffer.byteLength === 0) {
|
|
||||||
throw new Error('PDF response is empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.pdfjsLib.getDocument({
|
|
||||||
data: arrayBuffer,
|
|
||||||
useSystemFonts: true,
|
|
||||||
verbosity: 0,
|
|
||||||
// Explicitly disable worker
|
|
||||||
useWorkerFetch: false,
|
|
||||||
isEvalSupported: false
|
|
||||||
}).promise
|
|
||||||
.then(pdf => {
|
|
||||||
const pagePromises = [];
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
|
||||||
pagePromises.push(
|
|
||||||
pdf.getPage(i).then(page => {
|
|
||||||
return page.getTextContent().then(content => {
|
|
||||||
return content.items.map(item => item.str).join(' ');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Promise.all(pagePromises);
|
|
||||||
})
|
|
||||||
.then(pages => {
|
|
||||||
const text = pages.join('\\n');
|
|
||||||
window.postMessage({
|
|
||||||
type: requestId,
|
|
||||||
success: true,
|
|
||||||
text: text
|
|
||||||
}, '*');
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
window.postMessage({
|
|
||||||
type: requestId,
|
|
||||||
success: false,
|
|
||||||
error: 'PDF parsing error: ' + (error.message || String(error))
|
|
||||||
}, '*');
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
window.postMessage({
|
|
||||||
type: requestId,
|
|
||||||
success: false,
|
|
||||||
error: 'ArrayBuffer error: ' + (error.message || String(error))
|
|
||||||
}, '*');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = function() {
|
|
||||||
window.postMessage({
|
|
||||||
type: requestId,
|
|
||||||
success: false,
|
|
||||||
error: 'Network error fetching PDF'
|
|
||||||
}, '*');
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.ontimeout = function() {
|
|
||||||
window.postMessage({
|
|
||||||
type: requestId,
|
|
||||||
success: false,
|
|
||||||
error: 'Timeout fetching PDF'
|
|
||||||
}, '*');
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.timeout = 30000;
|
|
||||||
xhr.send();
|
|
||||||
} catch (error) {
|
|
||||||
window.postMessage({
|
|
||||||
type: requestId,
|
|
||||||
success: false,
|
|
||||||
error: 'Setup error: ' + (error.message || String(error))
|
|
||||||
}, '*');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
const messageHandler = (event: MessageEvent) => {
|
|
||||||
if (event.data?.type === requestId) {
|
|
||||||
window.removeEventListener('message', messageHandler);
|
|
||||||
if (script.parentNode) {
|
|
||||||
script.parentNode.removeChild(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.data.success) {
|
|
||||||
resolve(event.data.text);
|
|
||||||
} else {
|
|
||||||
reject(new Error(event.data.error || 'Failed to extract PDF text'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('message', messageHandler);
|
|
||||||
(document.head || document.documentElement).appendChild(script);
|
|
||||||
|
|
||||||
// Timeout after 60 seconds (PDF parsing can take time)
|
|
||||||
setTimeout(() => {
|
|
||||||
window.removeEventListener('message', messageHandler);
|
|
||||||
if (script.parentNode) {
|
|
||||||
script.parentNode.removeChild(script);
|
|
||||||
}
|
|
||||||
reject(new Error('Timeout extracting PDF text'));
|
|
||||||
}, 60000);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Chrome - use extension context
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await fetchPDFAsArrayBuffer(url);
|
|
||||||
|
|
||||||
if (arrayBuffer.byteLength === 0) {
|
|
||||||
throw new Error('PDF response is empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadingTask = pdfjs.getDocument({
|
|
||||||
data: arrayBuffer,
|
|
||||||
useSystemFonts: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pdf = await loadingTask.promise;
|
|
||||||
|
|
||||||
let text = "";
|
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
|
||||||
const page = await pdf.getPage(i);
|
|
||||||
const content = await page.getTextContent();
|
|
||||||
text += content.items.map((item: any) => item.str).join(" ") + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleWeightings(mark: any, api: any) {
|
|
||||||
const assessmentID = mark.id;
|
|
||||||
const metaclassID = mark.metaclassID;
|
|
||||||
const userInfo = await getUserInfo();
|
|
||||||
const userID = userInfo.id;
|
|
||||||
const title = mark.title;
|
|
||||||
|
|
||||||
// Skip if already processed (not "processing")
|
|
||||||
if (api.storage.weightings[assessmentID] != undefined && api.storage.weightings[assessmentID] !== "processing") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set to processing
|
|
||||||
api.storage.weightings = {
|
|
||||||
...api.storage.weightings,
|
|
||||||
[assessmentID]: "processing",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Correlate assessment title with its ID
|
|
||||||
api.storage.assessments = {
|
|
||||||
...api.storage.assessments,
|
|
||||||
[title.trim()]: assessmentID
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filename =
|
|
||||||
"BetterSEQTA-" + String(Math.floor(Math.random() * 1e15)).padStart(15, "0");
|
|
||||||
|
|
||||||
const printResponse = await fetch(`${location.origin}/seqta/student/print/assessment`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify({
|
|
||||||
fileName: filename,
|
|
||||||
id: assessmentID,
|
|
||||||
metaclass: metaclassID,
|
|
||||||
student: userID,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!printResponse.ok) {
|
|
||||||
throw new Error(`Failed to generate PDF: ${printResponse.status} ${printResponse.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait a bit for the PDF to be generated
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
|
|
||||||
|
|
||||||
// Check if URL is a blob URL (which extensions can't access)
|
|
||||||
if (pdfUrl.startsWith('blob:')) {
|
|
||||||
throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let text: string;
|
|
||||||
try {
|
|
||||||
// For Firefox, extractPDFText already handles everything in page context
|
|
||||||
// For Chrome, it uses extension context
|
|
||||||
text = await extractPDFText(pdfUrl);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (isFirefox && (error?.message?.includes('blob') || error?.message?.includes('Security') || error?.message?.includes('CSP'))) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
try {
|
|
||||||
text = await extractPDFText(pdfUrl);
|
|
||||||
} catch (retryError: any) {
|
|
||||||
throw new Error(`PDF extraction failed after retry: ${retryError.message}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`PDF extraction failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match weighting from extracted text
|
|
||||||
const match = text.match(/weight:\s*(\d+\.?\d*)/i);
|
|
||||||
const weight = match ? match[1] : "N/A";
|
|
||||||
|
|
||||||
// Store and correlate weight with assessment ID
|
|
||||||
api.storage.weightings = {
|
|
||||||
...api.storage.weightings,
|
|
||||||
[assessmentID]: weight,
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
api.storage.weightings = {
|
|
||||||
...api.storage.weightings,
|
|
||||||
[assessmentID]: "N/A",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function parseAssessments(api: any) {
|
|
||||||
const state = await ReactFiber.find(
|
|
||||||
"[class*='AssessmentList__items___']",
|
|
||||||
).getState();
|
|
||||||
|
|
||||||
const marks = state["marks"];
|
|
||||||
if (!marks) return;
|
|
||||||
|
|
||||||
// Dispatch for all assessments asynchronously
|
|
||||||
await Promise.all(marks.map((mark: any) => handleWeightings(mark, api)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default assessmentsAveragePlugin;
|
export default assessmentsAveragePlugin;
|
||||||
|
|||||||
@@ -0,0 +1,555 @@
|
|||||||
|
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts";
|
||||||
|
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
||||||
|
import * as pdfjs from "pdfjs-dist";
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc =
|
||||||
|
"https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.worker.min.mjs";
|
||||||
|
|
||||||
|
export async function initStorage(api: any) {
|
||||||
|
await api.storage.loaded;
|
||||||
|
|
||||||
|
if (!api.storage.weightings) {
|
||||||
|
api.storage.weightings = {};
|
||||||
|
}
|
||||||
|
if (!api.storage.assessments) {
|
||||||
|
api.storage.assessments = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStuck(api: any) {
|
||||||
|
let hasStuckProcessing = false;
|
||||||
|
for (const key in api.storage.weightings) {
|
||||||
|
if (api.storage.weightings[key] === "processing") {
|
||||||
|
delete api.storage.weightings[key];
|
||||||
|
hasStuckProcessing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasStuckProcessing) {
|
||||||
|
api.storage.weightings = { ...api.storage.weightings };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to find actual class names by their base pattern
|
||||||
|
export const getClassByPattern = (
|
||||||
|
element: Element | Document,
|
||||||
|
basePattern: string,
|
||||||
|
): string => {
|
||||||
|
// Find all classes on the element
|
||||||
|
const classes = Array.from(element.querySelectorAll("*"))
|
||||||
|
.flatMap((el) => Array.from(el.classList))
|
||||||
|
.filter((className) => className.startsWith(basePattern));
|
||||||
|
|
||||||
|
return classes.length ? classes[0] : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const letterToNumber: Record<string, number> = {
|
||||||
|
"A+": 100,
|
||||||
|
A: 95,
|
||||||
|
"A-": 90,
|
||||||
|
"B+": 85,
|
||||||
|
B: 80,
|
||||||
|
"B-": 75,
|
||||||
|
"C+": 70,
|
||||||
|
C: 65,
|
||||||
|
"C-": 60,
|
||||||
|
"D+": 55,
|
||||||
|
D: 50,
|
||||||
|
"D-": 45,
|
||||||
|
"E+": 40,
|
||||||
|
E: 35,
|
||||||
|
"E-": 30,
|
||||||
|
F: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseGrade(text: string): number {
|
||||||
|
const str = text.trim().toUpperCase();
|
||||||
|
if (str.includes("/")) {
|
||||||
|
const [raw, max] = str.split("/").map((n) => parseFloat(n));
|
||||||
|
return (raw / max) * 100;
|
||||||
|
}
|
||||||
|
if (str.includes("%")) {
|
||||||
|
return parseFloat(str.replace("%", "")) || 0;
|
||||||
|
}
|
||||||
|
return letterToNumber[str] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWeightLabel(assessmentItem: Element, weighting: string | undefined) {
|
||||||
|
const statsContainer = assessmentItem.querySelector(
|
||||||
|
`[class*='AssessmentItem__stats___']`,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
if (statsContainer) {
|
||||||
|
// Only add label if it hasn't been added before
|
||||||
|
if (!statsContainer.querySelector(".betterseqta-weight-label")) {
|
||||||
|
const label = statsContainer.querySelector(
|
||||||
|
`[class*='Label__Label___']`,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
// Clone average score node
|
||||||
|
const weightLabel = label.cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
|
// Mark as added to prevent duplicates
|
||||||
|
weightLabel.classList.add("betterseqta-weight-label");
|
||||||
|
|
||||||
|
const innerTextDiv = weightLabel.querySelector(
|
||||||
|
`[class*='Label__innerText___']`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Repurpose for showing weight
|
||||||
|
if (innerTextDiv) innerTextDiv.textContent = "Weight";
|
||||||
|
|
||||||
|
const textNodes = Array.from(weightLabel.childNodes).filter(
|
||||||
|
(node) => node.nodeType === Node.TEXT_NODE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set weight value, discarding useless decimals (.0)
|
||||||
|
if (textNodes.length) {
|
||||||
|
textNodes[0].textContent =
|
||||||
|
weighting && weighting !== "processing"
|
||||||
|
? `${Number(weighting) % 1 === 0 ? Number(weighting) : weighting}%`
|
||||||
|
: "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set position opposite to the average score node
|
||||||
|
statsContainer.style.position = "relative";
|
||||||
|
weightLabel.style.position = "absolute";
|
||||||
|
weightLabel.style.right = "0";
|
||||||
|
weightLabel.style.top = "50%";
|
||||||
|
weightLabel.style.transform = "translateY(-50%)";
|
||||||
|
|
||||||
|
statsContainer.appendChild(weightLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect Firefox (has stricter CSP for blob URLs)
|
||||||
|
// Use userAgent instead of deprecated InstallTrigger
|
||||||
|
export const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 &&
|
||||||
|
!navigator.userAgent.toLowerCase().includes('seamonkey') &&
|
||||||
|
!navigator.userAgent.toLowerCase().includes('waterfox');
|
||||||
|
|
||||||
|
async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
|
||||||
|
// Detect if URL is a blob URL
|
||||||
|
const isBlobUrl = url.startsWith("blob:");
|
||||||
|
|
||||||
|
// For Firefox, ALWAYS use page context to avoid any CSP issues
|
||||||
|
// For blob URLs in any browser, use page context
|
||||||
|
if (isBlobUrl || isFirefox) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Inject script into page context to fetch (bypasses Firefox CSP restrictions)
|
||||||
|
const script = document.createElement("script");
|
||||||
|
const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`;
|
||||||
|
|
||||||
|
// Escape URL for use in script
|
||||||
|
const escapedUrl = url.replace(/'/g, "\\'");
|
||||||
|
|
||||||
|
script.textContent = `
|
||||||
|
(function() {
|
||||||
|
fetch('${escapedUrl}')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||||
|
}
|
||||||
|
return response.arrayBuffer();
|
||||||
|
})
|
||||||
|
.then(arrayBuffer => {
|
||||||
|
window.postMessage({
|
||||||
|
type: '${requestId}',
|
||||||
|
success: true,
|
||||||
|
data: Array.from(new Uint8Array(arrayBuffer))
|
||||||
|
}, '*');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
window.postMessage({
|
||||||
|
type: '${requestId}',
|
||||||
|
success: false,
|
||||||
|
error: error.message || String(error)
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
if (event.data?.type === requestId) {
|
||||||
|
window.removeEventListener("message", messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.success) {
|
||||||
|
// Convert back to ArrayBuffer
|
||||||
|
const uint8Array = new Uint8Array(event.data.data);
|
||||||
|
resolve(uint8Array.buffer);
|
||||||
|
} else {
|
||||||
|
reject(new Error(event.data.error || "Failed to fetch PDF"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", messageHandler);
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
|
||||||
|
// Timeout after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
window.removeEventListener("message", messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
reject(new Error("Timeout fetching PDF"));
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Regular URL - fetch normally, but check if response URL becomes blob
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
credentials: "include",
|
||||||
|
redirect: "follow",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if response URL is a blob URL (server might redirect to blob)
|
||||||
|
if (response.url && response.url.startsWith("blob:")) {
|
||||||
|
// Re-fetch using page context
|
||||||
|
return await fetchPDFAsArrayBuffer(response.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch PDF: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.arrayBuffer();
|
||||||
|
} catch (error: any) {
|
||||||
|
// If error mentions blob or security, try using page context
|
||||||
|
if (
|
||||||
|
error?.message?.includes("blob") ||
|
||||||
|
error?.message?.includes("Security") ||
|
||||||
|
error?.message?.includes("CSP")
|
||||||
|
) {
|
||||||
|
// Force use page context
|
||||||
|
return await fetchPDFAsArrayBuffer(url);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractPDFText(url: string): Promise<string> {
|
||||||
|
// For Firefox, do everything in page context to avoid blob URL CSP issues
|
||||||
|
if (isFirefox) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
const requestId = `pdf-extract-${Date.now()}-${Math.random()}`;
|
||||||
|
|
||||||
|
// Escape URL for use in script (handle both single and double quotes)
|
||||||
|
const escapedUrl = url
|
||||||
|
.replace(/\\/g, "\\\\")
|
||||||
|
.replace(/'/g, "\\'")
|
||||||
|
.replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
script.textContent = `
|
||||||
|
(function() {
|
||||||
|
const requestId = '${requestId}';
|
||||||
|
const url = '${escapedUrl}';
|
||||||
|
|
||||||
|
// Check if pdfjs is already loaded
|
||||||
|
if (window.pdfjsLib) {
|
||||||
|
extractPDF();
|
||||||
|
} else {
|
||||||
|
// Load pdfjs in page context
|
||||||
|
const pdfjsScript = document.createElement('script');
|
||||||
|
pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js';
|
||||||
|
pdfjsScript.type = 'text/javascript';
|
||||||
|
|
||||||
|
pdfjsScript.onload = function() {
|
||||||
|
extractPDF();
|
||||||
|
};
|
||||||
|
pdfjsScript.onerror = function() {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to load pdfjs library'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.head.appendChild(pdfjsScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPDF() {
|
||||||
|
try {
|
||||||
|
// Disable worker for Firefox to avoid blob URL CSP issues
|
||||||
|
// Set to empty string to disable worker completely
|
||||||
|
window.pdfjsLib.GlobalWorkerOptions.workerSrc = '';
|
||||||
|
|
||||||
|
// Use XMLHttpRequest instead of fetch for better blob URL handling
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', url, true);
|
||||||
|
xhr.responseType = 'arraybuffer';
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'HTTP ' + xhr.status + ': ' + xhr.statusText
|
||||||
|
}, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const arrayBuffer = xhr.response;
|
||||||
|
if (!arrayBuffer || arrayBuffer.byteLength === 0) {
|
||||||
|
throw new Error('PDF response is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pdfjsLib.getDocument({
|
||||||
|
data: arrayBuffer,
|
||||||
|
useSystemFonts: true,
|
||||||
|
verbosity: 0,
|
||||||
|
// Explicitly disable worker
|
||||||
|
useWorkerFetch: false,
|
||||||
|
isEvalSupported: false
|
||||||
|
}).promise
|
||||||
|
.then(pdf => {
|
||||||
|
const pagePromises = [];
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
pagePromises.push(
|
||||||
|
pdf.getPage(i).then(page => {
|
||||||
|
return page.getTextContent().then(content => {
|
||||||
|
return content.items.map(item => item.str).join(' ');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.all(pagePromises);
|
||||||
|
})
|
||||||
|
.then(pages => {
|
||||||
|
const text = pages.join('\\n');
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: true,
|
||||||
|
text: text
|
||||||
|
}, '*');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'PDF parsing error: ' + (error.message || String(error))
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'ArrayBuffer error: ' + (error.message || String(error))
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = function() {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Network error fetching PDF'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.ontimeout = function() {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Timeout fetching PDF'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.timeout = 30000;
|
||||||
|
xhr.send();
|
||||||
|
} catch (error) {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Setup error: ' + (error.message || String(error))
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
if (event.data?.type === requestId) {
|
||||||
|
window.removeEventListener("message", messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.success) {
|
||||||
|
resolve(event.data.text);
|
||||||
|
} else {
|
||||||
|
reject(new Error(event.data.error || "Failed to extract PDF text"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", messageHandler);
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
|
||||||
|
// Timeout after 60 seconds (PDF parsing can take time)
|
||||||
|
setTimeout(() => {
|
||||||
|
window.removeEventListener("message", messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
reject(new Error("Timeout extracting PDF text"));
|
||||||
|
}, 60000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Chrome - use extension context
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await fetchPDFAsArrayBuffer(url);
|
||||||
|
|
||||||
|
if (arrayBuffer.byteLength === 0) {
|
||||||
|
throw new Error("PDF response is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingTask = pdfjs.getDocument({
|
||||||
|
data: arrayBuffer,
|
||||||
|
useSystemFonts: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdf = await loadingTask.promise;
|
||||||
|
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const content = await page.getTextContent();
|
||||||
|
text += content.items.map((item: any) => item.str).join(" ") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWeightings(mark: any, api: any) {
|
||||||
|
const assessmentID = mark.id;
|
||||||
|
const metaclassID = mark.metaclassID;
|
||||||
|
const userInfo = await getUserInfo();
|
||||||
|
const userID = userInfo.id;
|
||||||
|
const title = mark.title;
|
||||||
|
|
||||||
|
// Skip if already processed (not "processing")
|
||||||
|
if (
|
||||||
|
api.storage.weightings[assessmentID] != undefined &&
|
||||||
|
api.storage.weightings[assessmentID] !== "processing"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set to processing
|
||||||
|
api.storage.weightings = {
|
||||||
|
...api.storage.weightings,
|
||||||
|
[assessmentID]: "processing",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Correlate assessment title with its ID
|
||||||
|
api.storage.assessments = {
|
||||||
|
...api.storage.assessments,
|
||||||
|
[title.trim()]: assessmentID,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filename =
|
||||||
|
"BetterSEQTA-" +
|
||||||
|
String(Math.floor(Math.random() * 1e15)).padStart(15, "0");
|
||||||
|
|
||||||
|
const printResponse = await fetch(
|
||||||
|
`${location.origin}/seqta/student/print/assessment`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
fileName: filename,
|
||||||
|
id: assessmentID,
|
||||||
|
metaclass: metaclassID,
|
||||||
|
student: userID,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!printResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to generate PDF: ${printResponse.status} ${printResponse.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit for the PDF to be generated
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
|
||||||
|
|
||||||
|
// Check if URL is a blob URL (which extensions can't access)
|
||||||
|
if (pdfUrl.startsWith("blob:")) {
|
||||||
|
throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text: string;
|
||||||
|
try {
|
||||||
|
// For Firefox, extractPDFText already handles everything in page context
|
||||||
|
// For Chrome, it uses extension context
|
||||||
|
text = await extractPDFText(pdfUrl);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
isFirefox &&
|
||||||
|
(error?.message?.includes("blob") ||
|
||||||
|
error?.message?.includes("Security") ||
|
||||||
|
error?.message?.includes("CSP"))
|
||||||
|
) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
try {
|
||||||
|
text = await extractPDFText(pdfUrl);
|
||||||
|
} catch (retryError: any) {
|
||||||
|
throw new Error(
|
||||||
|
`PDF extraction failed after retry: ${retryError.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`PDF extraction failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match weighting from extracted text
|
||||||
|
const match = text.match(/weight:\s*(\d+\.?\d*)/i);
|
||||||
|
const weight = match ? match[1] : "N/A";
|
||||||
|
|
||||||
|
// Store and correlate weight with assessment ID
|
||||||
|
api.storage.weightings = {
|
||||||
|
...api.storage.weightings,
|
||||||
|
[assessmentID]: weight,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
api.storage.weightings = {
|
||||||
|
...api.storage.weightings,
|
||||||
|
[assessmentID]: "N/A",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseAssessments(api: any) {
|
||||||
|
const state = await ReactFiber.find(
|
||||||
|
"[class*='AssessmentList__items___']",
|
||||||
|
).getState();
|
||||||
|
|
||||||
|
const marks = state["marks"];
|
||||||
|
if (!marks) return;
|
||||||
|
|
||||||
|
// Dispatch for all assessments asynchronously
|
||||||
|
await Promise.all(marks.map((mark: any) => handleWeightings(mark, api)));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user