Update F95_BRATR_Management_Ratings_Helper.js

This commit is contained in:
Ryahn 2025-09-22 01:48:03 +00:00
parent 8821dad70d
commit bc4e35e28e

View File

@ -29,7 +29,7 @@
// Cached DOM utilities
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
// Optimized text extraction with caching
const textCache = new WeakMap();
const text = el => {
@ -54,17 +54,17 @@
const csvEscape = s => `"${(s || '').replace(/"/g, '""')}"`;
const avg = arr => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
const truncate = (s, n = 80) => (s && s.length > n ? s.slice(0, n - 1) + '…' : (s || ''));
// Optimized Unicode-aware letter counters with caching
const letterCountCache = new Map();
function countLetters(str) {
if (!str) return { all: 0, ascii: 0, nonAscii: 0 };
if (letterCountCache.has(str)) return letterCountCache.get(str);
const all = (str.match(/\p{L}/gu) || []).length;
const ascii = (str.match(/[A-Za-z]/g) || []).length;
const result = { all, ascii, nonAscii: Math.max(0, all - ascii) };
// Cache only reasonable length strings to avoid memory bloat
if (str.length < 1000) letterCountCache.set(str, result);
return result;
@ -84,7 +84,7 @@
const enders = (str.match(/[.!?…]/g) || []).length;
return enders < 1;
}
// Optimized review scraping with better error handling
const reviews = $$('.message--review').map((msg, i) => {
try {
@ -117,7 +117,7 @@
}
const deleted = msg.classList.contains('message--deleted');
// Batch action link queries
const actionLinks = main.querySelectorAll('a[class*="actionBar-action"]');
const links = {
@ -148,7 +148,7 @@
return null;
}
}).filter(Boolean); // Remove null entries
// Cliché patterns
const cliché = [
{ key: 'good game', label: '“good game”', rx: /\bgood game\b/i },
@ -159,16 +159,16 @@
{ key: 'downloads flex', label: '“downloaded hundreds of games”', rx: /\bdownload(ed)? hundreds of games\b/i },
{ key: 'mentions 200', label: 'mentions “200” (char/limit/min)', rx: /\b200(?:\s*[- ]?(?:char(?:s|acters?)?|word(?:s)?|limit|minimum|min))?\b/i }
];
// Optimized spam analysis with caching and early returns
const spamAnalysisCache = new Map();
function analyzeSpam(body) {
const s = body || '';
if (!s) return [];
// Cache results for identical text
if (spamAnalysisCache.has(s)) return spamAnalysisCache.get(s);
const reasons = [];
const len = s.length;
@ -182,10 +182,10 @@
const punctRuns = s.match(/([.!?…,_\-])\1{5,}/g);
if (punctRuns?.length) {
const totalRunChars = punctRuns.reduce((a, b) => a + b.length, 0);
reasons.push({
code: 'SPAM',
text: `DOTCARPET ×${punctRuns.length}`,
tip: `Punctuation runs total ${totalRunChars} chars`
reasons.push({
code: 'SPAM',
text: `DOTCARPET ×${punctRuns.length}`,
tip: `Punctuation runs total ${totalRunChars} chars`
});
}
@ -193,10 +193,10 @@
const punctChars = (s.match(/[^\w\s]/g) || []).length;
const pRatio = punctChars / len;
if (pRatio > CONFIG.PUNCTUATION_RATIO_THRESHOLD && len >= 120) {
reasons.push({
code: 'SPAM',
text: `PUNC ${Math.round(pRatio * 100)}%`,
tip: `Non-alphanumeric ratio ${Math.round(pRatio * 100)}%`
reasons.push({
code: 'SPAM',
text: `PUNC ${Math.round(pRatio * 100)}%`,
tip: `Non-alphanumeric ratio ${Math.round(pRatio * 100)}%`
});
}
@ -209,20 +209,20 @@
const totalTok = tokens.length;
const freq = new Map();
// Optimized frequency counting
for (const token of tokens) {
freq.set(token, (freq.get(token) || 0) + 1);
}
const uniq = freq.size;
const uniqRatio = uniq / totalTok;
if (uniqRatio < CONFIG.UNIQUE_RATIO_THRESHOLD) {
reasons.push({
code: 'SPAM',
text: `LOW_UNIQUE ${uniq}/${totalTok}`,
tip: `Unique/token ratio ${uniqRatio.toFixed(2)}`
reasons.push({
code: 'SPAM',
text: `LOW_UNIQUE ${uniq}/${totalTok}`,
tip: `Unique/token ratio ${uniqRatio.toFixed(2)}`
});
}
@ -234,12 +234,12 @@
maxWord = word;
}
}
if (maxCount / totalTok > CONFIG.MAX_WORD_FREQUENCY_THRESHOLD && totalTok >= 8) {
reasons.push({
code: 'SPAM',
text: `REP "${truncate(maxWord, 12)}" ×${maxCount}`,
tip: `Word appears ${(maxCount / totalTok * 100).toFixed(0)}% of tokens`
reasons.push({
code: 'SPAM',
text: `REP "${truncate(maxWord, 12)}" ×${maxCount}`,
tip: `Word appears ${(maxCount / totalTok * 100).toFixed(0)}% of tokens`
});
}
@ -247,18 +247,18 @@
function analyzeNgrams(n) {
const ngramMap = new Map();
const limit = tokens.length - n;
for (let i = 0; i <= limit; i++) {
const ngram = tokens.slice(i, i + n).join(' ');
ngramMap.set(ngram, (ngramMap.get(ngram) || 0) + 1);
}
for (const [ngram, count] of ngramMap) {
if (count >= CONFIG.NGRAM_REPEAT_THRESHOLD) {
reasons.push({
code: 'SPAM',
text: `REP${n} "${truncate(ngram, 20)}" ×${count}`,
tip: `Repeated ${n}-gram ${count} times`
reasons.push({
code: 'SPAM',
text: `REP${n} "${truncate(ngram, 20)}" ×${count}`,
tip: `Repeated ${n}-gram ${count} times`
});
return true; // Found one, stop looking
}
@ -274,19 +274,19 @@
// Optimized short sentence analysis
const sentences = s.split(/(?<=[.!?])\s+/);
const shortFreq = new Map();
for (const sentence of sentences) {
const toks = sentence.toLowerCase().match(/[a-z0-9']+/g);
if (toks && toks.length <= CONFIG.SHORT_SENTENCE_MAX_LENGTH) {
const key = toks.join(' ');
const count = (shortFreq.get(key) || 0) + 1;
shortFreq.set(key, count);
if (count >= CONFIG.NGRAM_REPEAT_THRESHOLD) {
reasons.push({
code: 'SPAM',
text: `SHORT_SENT "${truncate(key, 18)}" ×${count}`,
tip: `Short sentence repeated ${count} times`
reasons.push({
code: 'SPAM',
text: `SHORT_SENT "${truncate(key, 18)}" ×${count}`,
tip: `Short sentence repeated ${count} times`
});
break; // Found one, stop looking
}
@ -297,10 +297,10 @@
if (spamAnalysisCache.size < 1000) {
spamAnalysisCache.set(s, reasons);
}
return reasons;
}
// Optimized low-effort diagnosis with caching
const diagnosisCache = new Map();
function diagnoseLowEffort(r, cutoff = CONFIG.DEFAULT_CUTOFF) {
@ -314,10 +314,10 @@
// Length check
if (!r.deleted && (r.effLen || 0) < cutoff) {
reasons.push({
code: 'LEN',
text: `LEN ${r.effLen}<${cutoff}`,
tip: `Effective length ${r.effLen} is below cutoff ${cutoff}`
reasons.push({
code: 'LEN',
text: `LEN ${r.effLen}<${cutoff}`,
tip: `Effective length ${r.effLen} is below cutoff ${cutoff}`
});
}
@ -326,11 +326,11 @@
for (const c of cliché) {
const match = body.match(c.rx);
if (match) {
reasons.push({
code: 'CLICHÉ',
text: `CLICHÉ: "${truncate(match[0], 28)}"`,
tip: `Matched: ${match[0]}`,
key: c.key
reasons.push({
code: 'CLICHÉ',
text: `CLICHÉ: "${truncate(match[0], 28)}"`,
tip: `Matched: ${match[0]}`,
key: c.key
});
break; // Found one cliché, no need to check others
}
@ -342,7 +342,7 @@
const tip = `letters: ascii=${ascii}, nonAscii=${nonAscii}, share=${(nonAscii / Math.max(1, all)).toFixed(2)}`;
reasons.push({ code: 'LANG', text: 'NON-EN', tip });
}
// Style check
if (isRunOnWall(body)) {
reasons.push({ code: 'STYLE', text: 'RUN-ON', tip: 'Long block with no sentence enders' });
@ -363,15 +363,15 @@
);
const result = { low, reasons };
// Cache result (limit cache size)
if (diagnosisCache.size < 500) {
diagnosisCache.set(cacheKey, result);
}
return result;
}
// Panel
const panel = document.createElement('div');
panel.id = 'bratr-helper';
@ -393,12 +393,14 @@
<label><input type="checkbox" class="bh-cliche-only"> Only reviews matching selected cliché</label>
<button class="bh-copy">Copy CSV</button>
<button class="bh-reset">Reset</button>
<button class="bh-prev-page"> Prev</button>
<button class="bh-next-page">Next </button>
</div>
<div class="bh-table"></div>
</div>
`;
document.body.appendChild(panel);
// Styles
const css = document.createElement('style');
css.textContent = `
@ -412,7 +414,9 @@
#bratr-helper .bh-controls{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}
#bratr-helper .bh-controls label{display:flex;gap:6px;align-items:center;background:#161616;border:1px solid #2a2a2a;border-radius:8px;padding:4px 8px}
#bratr-helper input, #bratr-helper select{background:#0f0f0f;border:1px solid #333;color:#ddd;border-radius:6px;padding:3px 6px}
#bratr-helper button.bh-copy,#bratr-helper button.bh-reset{background:#1e1e1e;border:1px solid #444;color:#ddd;border-radius:8px;padding:4px 10px;cursor:pointer}
#bratr-helper button.bh-copy,#bratr-helper button.bh-reset,#bratr-helper button.bh-prev-page,#bratr-helper button.bh-next-page{background:#1e1e1e;border:1px solid #444;color:#ddd;border-radius:8px;padding:4px 10px;cursor:pointer}
#bratr-helper button.bh-prev-page,#bratr-helper button.bh-next-page{background:#2a4a2a;border-color:#4a6a4a;font-size:11px;padding:3px 8px}
#bratr-helper button.bh-prev-page:hover,#bratr-helper button.bh-next-page:hover{background:#3a5a3a}
#bratr-helper .bh-table{overflow:auto;max-height:48vh;border:1px solid #2a2a2a;border-radius:8px}
#bratr-helper table{width:100%;border-collapse:collapse}
#bratr-helper th,#bratr-helper td{padding:6px 8px;border-bottom:1px solid #232323;vertical-align:top}
@ -433,7 +437,7 @@
@media (max-width: 880px){#bratr-helper{left:8px;right:8px;width:auto}}
`;
document.head.appendChild(css);
const ui = {
min: panel.querySelector('.bh-minrating'),
maxlen: panel.querySelector('.bh-maxlen'),
@ -446,7 +450,7 @@
table: panel.querySelector('.bh-table'),
metrics: panel.querySelector('.bh-metrics')
};
// Populate cliché dropdown
const frag = document.createDocumentFragment();
cliché.forEach(c => {
@ -456,7 +460,7 @@
frag.appendChild(opt);
});
ui.clicheSel.appendChild(frag);
// collapse default minimized with persistence
const COLLAPSE_KEY = 'bh-collapsed';
const bodyEl = panel.querySelector('.bh-body');
@ -469,7 +473,7 @@
collapseBtn.addEventListener('click', () => setCollapsed(bodyEl.style.display !== 'none'));
const stored = (typeof localStorage !== 'undefined') ? localStorage.getItem(COLLAPSE_KEY) : null;
setCollapsed(stored === null ? true : stored === '1');
// Optimized event handling with debouncing
let renderTimeout;
const debouncedRender = () => {
@ -494,6 +498,75 @@
}
});
// Navigation helper function
function navigateToPage(pageNumber) {
try {
const currentUrl = window.location.href;
const urlMatch = currentUrl.match(/\/bratr-ratings\/(?:page-(\d+)\/)?management/);
if (urlMatch) {
let targetUrl;
if (pageNumber === 1) {
// For page 1, remove the page number from URL
targetUrl = currentUrl.replace(
/\/bratr-ratings\/page-\d+\/management/,
'/bratr-ratings/management'
);
} else {
// For other pages, add or update the page number
targetUrl = currentUrl.replace(
/\/bratr-ratings\/(?:page-\d+\/)?management/,
`/bratr-ratings/page-${pageNumber}/management`
);
}
console.log(`Navigating to page ${pageNumber}: ${targetUrl}`);
window.location.href = targetUrl;
} else {
console.warn('Could not determine current page number from URL:', currentUrl);
}
} catch (error) {
console.error('Error navigating to page:', error);
}
}
// Previous page handler
panel.querySelector('.bh-prev-page').addEventListener('click', () => {
try {
const currentUrl = window.location.href;
const urlMatch = currentUrl.match(/\/bratr-ratings\/(?:page-(\d+)\/)?management/);
if (urlMatch) {
const currentPage = urlMatch[1] ? parseInt(urlMatch[1], 10) : 1;
const prevPage = currentPage - 1;
if (prevPage >= 1) {
navigateToPage(prevPage);
} else {
console.log('Already on first page');
}
}
} catch (error) {
console.error('Error navigating to previous page:', error);
}
});
// Next page handler
panel.querySelector('.bh-next-page').addEventListener('click', () => {
try {
const currentUrl = window.location.href;
const urlMatch = currentUrl.match(/\/bratr-ratings\/(?:page-(\d+)\/)?management/);
if (urlMatch) {
const currentPage = urlMatch[1] ? parseInt(urlMatch[1], 10) : 1;
const nextPage = currentPage + 1;
navigateToPage(nextPage);
}
} catch (error) {
console.error('Error navigating to next page:', error);
}
});
// Optimized event delegation
const controlElements = [ui.min, ui.maxlen, ui.user, ui.lowonly, ui.from, ui.to, ui.clicheSel, ui.clicheOnly];
['input', 'change'].forEach(eventType => {
@ -503,7 +576,7 @@
}
});
});
function withinDates(iso, from, to) {
if (!iso) return true;
const t = new Date(iso).getTime();
@ -511,7 +584,7 @@
if (to && t > new Date(to).getTime()) return false;
return true;
}
function reviewMatchesSelectedCliche(r) {
if (!ui.clicheOnly.checked) return true;
const selected = ui.clicheSel.value;
@ -520,7 +593,7 @@
if (!def) return true;
return def.rx.test(r.bodyTxt || '');
}
function currentRows() {
const min = parseFloat(ui.min.value || '0');
const maxlen = parseInt(ui.maxlen.value || '0', 10);
@ -528,25 +601,25 @@
const lowOnly = ui.lowonly.checked;
const from = ui.from.value;
const to = ui.to.value;
return reviews.filter(r => {
if (Number.isFinite(min) && r.rating != null && r.rating < min) return false;
if (user && !r.author.toLowerCase().includes(user)) return false;
if (from || to) { if (!withinDates(r.timeIso, from, to)) return false; }
if (!reviewMatchesSelectedCliche(r)) return false;
const diag = diagnoseLowEffort(r, maxlen);
if (lowOnly && !diag.low) return false;
return true;
});
}
// Optimized render function with better error handling
function render() {
try {
const maxlen = parseInt(ui.maxlen?.value || CONFIG.DEFAULT_CUTOFF.toString(), 10);
const live = reviews.filter(r => !r.deleted);
// Optimized metrics calculation
const lowCount = live.filter(r => {
try {
@ -556,12 +629,12 @@
return false;
}
}).length;
const avgRating = avg(live.map(r => r.rating || 0)).toFixed(2);
const clicheInfo = ui.clicheOnly?.checked
? ` • cliché: ${ui.clicheSel?.options[ui.clicheSel?.selectedIndex]?.text || 'All'}`
: '';
if (ui.metrics) {
ui.metrics.textContent = `on page: ${reviews.length} • avg ★ ${avgRating} • low-effort flagged: ${lowCount}${clicheInfo}`;
}
@ -583,7 +656,7 @@
'LEN': 'low', 'CLICHÉ': 'low', 'SPAM': 'spam',
'DELETED': 'delet', 'LANG': 'lang', 'STYLE': 'style'
};
const flagHtml = diag.reasons.map(reason => {
let pill = 'pill';
const extra = pillClassByCode[reason.code];
@ -612,7 +685,7 @@
}
html.push(`</tbody></table>`);
if (ui.table) {
ui.table.innerHTML = html.join('');
@ -627,7 +700,7 @@
console.error('Error in render function:', error);
}
}
function onRowClick(e) {
if (e.target.closest('a, button')) return;
const tr = e.target.closest('tr[data-i]');
@ -635,14 +708,14 @@
const idx = parseInt(tr.getAttribute('data-i'), 10);
const r = reviews[idx];
if (!r || !r.node) return;
r.node.scrollIntoView({ behavior: 'smooth', block: 'center' });
r.node.classList.remove('bh-target-pulse');
void r.node.offsetWidth;
r.node.classList.add('bh-target-pulse');
setTimeout(() => r.node.classList.remove('bh-target-pulse'), 1800);
}
// Tint live page with reasons as tooltip
function retint() {
const maxlen = parseInt(ui.maxlen?.value || '220', 10);
@ -664,9 +737,9 @@
}
});
}
render();
// Optimized CSV export with better error handling
panel.querySelector('.bh-copy').addEventListener('click', async () => {
try {
@ -674,7 +747,7 @@
const maxlen = parseInt(ui.maxlen?.value || CONFIG.DEFAULT_CUTOFF.toString(), 10);
const header = ['author', 'rating', 'thread', 'threadUrl', 'timeIso', 'effLen', 'rawLen', 'deleted', 'low', 'reasons', 'body'];
const lines = [header.join(',')];
// Optimized CSV generation
for (const r of rows) {
try {
@ -697,9 +770,9 @@
console.warn('Error processing row for CSV:', error);
}
}
const csv = lines.join('\n');
// Try multiple clipboard methods
try {
if (typeof GM_setClipboard !== 'undefined') {
@ -725,4 +798,4 @@
console.error('Error exporting CSV:', error);
}
});
})();
})();