Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"description": "__MSG_extDescription__",
"version": "1.0.1",
"version": "2.0.0",
"minimum_chrome_version": "120",
"author": "SkillBridge Contributors",
"homepage_url": "https://github.com/heznpc/skillbridge",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skillbridge",
"version": "1.0.1",
"version": "2.0.0",
"private": true,
"scripts": {
"test": "jest --verbose",
Expand Down
36 changes: 36 additions & 0 deletions src/content/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -2044,3 +2044,39 @@ html.si18n-dark .si18n-shortcut-key {
color: #aaa;
box-shadow: 0 1px 0 #333;
}

/* ============================================================
EXAM MODE BANNER
============================================================ */

.si18n-exam-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10001;
background: #FFF3CD;
color: #856404;
text-align: center;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
border-bottom: 1px solid #FFEEBA;
transform: translateY(-100%);
transition: transform 0.3s ease;
}

.si18n-exam-banner.visible {
transform: translateY(0);
}

html.si18n-dark .si18n-exam-banner {
background: #3D3200;
color: #FFD54F;
border-bottom-color: #5C4B00;
}

.si18n-exam-warning {
border-left: 3px solid #FFC107 !important;
background: rgba(255, 193, 7, 0.08) !important;
}
37 changes: 37 additions & 0 deletions src/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
let translator = null;
let subtitleManager = null;
let currentLang = 'en';
let isExamPage = false;
let isReady = false;
let sidebarVisible = false;
let originalTexts = new Map();
Expand Down Expand Up @@ -72,12 +73,37 @@
// (header-controls.js, text-selection.js, sidebar-chat.js)
// ============================================================

// ============================================================
// EXAM PAGE DETECTION
// ============================================================

function detectExamPage() {
const url = location.href;
if (EXAM_URL_PATTERNS.some(p => p.test(url))) return true;
// DOM-based detection: check for quiz forms or answer option containers
if (document.querySelector(SKILLJAR_SELECTORS.quizForm)) return true;
if (document.querySelector(SKILLJAR_SELECTORS.answerOption)) return true;
return false;
}

function showExamBanner() {
if (document.getElementById('si18n-exam-banner')) return;
const banner = document.createElement('div');
banner.id = 'si18n-exam-banner';
banner.className = 'si18n-exam-banner';
banner.setAttribute('role', 'alert');
banner.textContent = t(EXAM_BANNER_LABELS);
document.body.appendChild(banner);
requestAnimationFrame(() => banner.classList.add('visible'));
}

window._sb = {
get currentLang() { return currentLang; },
set currentLang(v) { currentLang = v; },
get sidebarVisible() { return sidebarVisible; },
set sidebarVisible(v) { sidebarVisible = v; },
get translator() { return translator; },
get isExamPage() { return isExamPage; },
t,
escapeHtml,
switchLanguage,
Expand Down Expand Up @@ -217,6 +243,7 @@
const stored = await chrome.storage.local.get(['targetLanguage', 'autoTranslate', 'welcomeShown', 'darkMode']);
if (stored.darkMode) document.documentElement.classList.add('si18n-dark');
currentLang = stored.targetLanguage || 'en';
isExamPage = detectExamPage();

translator = new SkilljarTranslator();

Expand Down Expand Up @@ -349,6 +376,9 @@
function applyStaticTranslations(targetLang) {
buildProtectedTermsMap(targetLang);
updateLangClass(targetLang);
// Re-detect exam page (DOM may have loaded since init)
if (!isExamPage) isExamPage = detectExamPage();
if (isExamPage && targetLang !== 'en') showExamBanner();

const elements = getTranslatableElements();
if (elements.length === 0) return;
Expand Down Expand Up @@ -671,8 +701,12 @@
}

function getTranslatableElements() {
const examSkip = isExamPage ? EXAM_SKIP_SELECTORS.join(', ') : null;
return Array.from(document.querySelectorAll(TRANSLATABLE_SELECTOR)).filter(el => {
if (el.closest(EXCLUDE_SELECTOR)) return false;
// On exam pages, skip answer choice elements
if (examSkip && el.matches(examSkip)) return false;
if (examSkip && el.closest(examSkip)) return false;
const parent = el.parentElement;
if (parent && parent.matches && parent.matches(TRANSLATABLE_SELECTOR) &&
!parent.closest(EXCLUDE_SELECTOR)) {
Expand Down Expand Up @@ -925,6 +959,9 @@ RULES:

function getPageContext() {
const title = document.querySelector(`h1, h2, ${SKILLJAR_SELECTORS.courseTitle}`)?.textContent || document.title || '';
if (isExamPage) {
return `Certification Exam: ${title}. Page type: exam/assessment. DO NOT help with answers.`;
}
const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4'))
.map(h => h.textContent.trim())
.slice(0, 5)
Expand Down
12 changes: 12 additions & 0 deletions src/content/sidebar-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@
const messages = document.getElementById('si18n-chat-messages');
const text = input.value.trim();
if (!text) return;

// Exam mode guard — warn user, add system instruction to prevent answer leaking
if (sb.isExamPage) {
const warningMsg = sb.t(TUTOR_EXAM_LABELS);
messages.insertAdjacentHTML('beforeend', `
<div class="si18n-chat-msg si18n-chat-bot">
<div class="si18n-chat-avatar">AI</div>
<div class="si18n-chat-bubble si18n-exam-warning">${sb.escapeHtml(warningMsg)}</div>
</div>
`);
}

isSending = true;

const quoteEl = document.querySelector('.si18n-chat-quote');
Expand Down
18 changes: 18 additions & 0 deletions src/data/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -653,5 +653,23 @@
"hooks": [
"Haken"
]
},
"exam_ui": {
"Pass": "Bestanden",
"Fail": "Nicht bestanden",
"Passing score": "Mindestpunktzahl",
"Your score": "Ihre Punktzahl",
"Retake": "Erneut versuchen",
"Attempt": "Versuch",
"Attempts remaining": "Verbleibende Versuche",
"Time remaining": "Verbleibende Zeit",
"Next question": "Nächste Frage",
"Previous question": "Vorherige Frage",
"Review answers": "Antworten überprüfen",
"Submit exam": "Prüfung abgeben",
"Exam complete": "Prüfung abgeschlossen",
"You passed!": "Herzlichen Glückwunsch, Sie haben bestanden!",
"You did not pass": "Sie haben nicht bestanden",
"Question": "Frage"
}
}
18 changes: 18 additions & 0 deletions src/data/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -653,5 +653,23 @@
"hooks": [
"ganchos"
]
},
"exam_ui": {
"Pass": "Aprobado",
"Fail": "Reprobado",
"Passing score": "Puntuación para aprobar",
"Your score": "Tu puntuación",
"Retake": "Volver a intentar",
"Attempt": "Intento",
"Attempts remaining": "Intentos restantes",
"Time remaining": "Tiempo restante",
"Next question": "Siguiente pregunta",
"Previous question": "Pregunta anterior",
"Review answers": "Revisar respuestas",
"Submit exam": "Enviar examen",
"Exam complete": "Examen completado",
"You passed!": "¡Felicidades, aprobaste!",
"You did not pass": "No aprobaste",
"Question": "Pregunta"
}
}
18 changes: 18 additions & 0 deletions src/data/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -653,5 +653,23 @@
"hooks": [
"crochets"
]
},
"exam_ui": {
"Pass": "Réussi",
"Fail": "Échoué",
"Passing score": "Note de passage",
"Your score": "Votre score",
"Retake": "Repasser",
"Attempt": "Tentative",
"Attempts remaining": "Tentatives restantes",
"Time remaining": "Temps restant",
"Next question": "Question suivante",
"Previous question": "Question précédente",
"Review answers": "Réviser les réponses",
"Submit exam": "Soumettre l'examen",
"Exam complete": "Examen terminé",
"You passed!": "Félicitations, vous avez réussi !",
"You did not pass": "Vous n'avez pas réussi",
"Question": "Question"
}
}
18 changes: 18 additions & 0 deletions src/data/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -653,5 +653,23 @@
"hooks": [
"フック"
]
},
"exam_ui": {
"Pass": "合格",
"Fail": "不合格",
"Passing score": "合格点",
"Your score": "あなたのスコア",
"Retake": "再受験",
"Attempt": "受験回",
"Attempts remaining": "残りの受験回数",
"Time remaining": "残り時間",
"Next question": "次の問題",
"Previous question": "前の問題",
"Review answers": "解答を確認",
"Submit exam": "試験を提出",
"Exam complete": "試験完了",
"You passed!": "おめでとうございます、合格です!",
"You did not pass": "不合格です",
"Question": "問題"
}
}
18 changes: 18 additions & 0 deletions src/data/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -643,5 +643,23 @@
"subagent": ["하위 에이전트", "서브에이전트"],
"hook": ["후크"],
"hooks": ["후크들", "후크"]
},
"exam_ui": {
"Pass": "합격",
"Fail": "불합격",
"Passing score": "합격 점수",
"Your score": "내 점수",
"Retake": "재응시",
"Attempt": "시도",
"Attempts remaining": "남은 시도 횟수",
"Time remaining": "남은 시간",
"Next question": "다음 문제",
"Previous question": "이전 문제",
"Review answers": "답안 검토",
"Submit exam": "시험 제출",
"Exam complete": "시험 완료",
"You passed!": "축하합니다, 합격하셨습니다!",
"You did not pass": "불합격하셨습니다",
"Question": "문제"
}
}
18 changes: 18 additions & 0 deletions src/data/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -656,5 +656,23 @@
"钩子",
"挂钩"
]
},
"exam_ui": {
"Pass": "通过",
"Fail": "未通过",
"Passing score": "及格分数",
"Your score": "您的分数",
"Retake": "重新考试",
"Attempt": "尝试",
"Attempts remaining": "剩余尝试次数",
"Time remaining": "剩余时间",
"Next question": "下一题",
"Previous question": "上一题",
"Review answers": "查看答案",
"Submit exam": "提交考试",
"Exam complete": "考试完成",
"You passed!": "恭喜您,考试通过!",
"You did not pass": "您未通过考试",
"Question": "题目"
}
}
43 changes: 43 additions & 0 deletions src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,49 @@ const DEFAULT_PROTECTED_TERMS = 'API, SDK, Claude, Anthropic, Claude Code, Enter
// YouTube InnerTube client version — update periodically as needed
const YOUTUBE_CLIENT_VERSION = '2.20240101.00.00';

// ==================== EXAM / ASSESSMENT ====================

const EXAM_URL_PATTERNS = [
/\/quiz\b/i,
/\/exam\b/i,
/\/assessment\b/i,
/\/certification\b/i,
/[?&]type=quiz/i,
/[?&]type=exam/i,
];

// Elements whose text should NOT be translated on exam pages
// (answer choices, form inputs — translating these could alter meaning)
const EXAM_SKIP_SELECTORS = [
'input[type="radio"] + label',
'input[type="checkbox"] + label',
`${SKILLJAR_SELECTORS.answerOption}`,
`${SKILLJAR_SELECTORS.answerLabel}`,
`${SKILLJAR_SELECTORS.quizForm} label`,
`${SKILLJAR_SELECTORS.quizForm} .option`,
`${SKILLJAR_SELECTORS.quizForm} li`,
];

const EXAM_BANNER_LABELS = {
'en': 'Exam mode — answer choices are not translated to preserve accuracy.',
'ko': '시험 모드 — 정확성을 위해 답안 선택지는 번역되지 않습니다.',
'ja': '試験モード — 正確性のため、回答選択肢は翻訳されません。',
'zh-CN': '考试模式 — 为确保准确性,答案选项不会被翻译。',
'es': 'Modo examen — las opciones de respuesta no se traducen para mayor precisión.',
'fr': 'Mode examen — les choix de réponse ne sont pas traduits pour préserver la précision.',
'de': 'Prüfungsmodus — Antwortmöglichkeiten werden nicht übersetzt, um die Genauigkeit zu wahren.',
};

const TUTOR_EXAM_LABELS = {
'en': "I can't help with exam answers directly, but I can explain concepts after you submit.",
'ko': '시험 답안은 직접 도와드릴 수 없지만, 제출 후 개념 설명은 가능합니다.',
'ja': '試験の回答は直接お手伝いできませんが、提出後にコンセプトを説明できます。',
'zh-CN': '我不能直接帮助回答考试题目,但提交后可以解释相关概念。',
'es': 'No puedo ayudar directamente con las respuestas del examen, pero puedo explicar conceptos después de enviar.',
'fr': "Je ne peux pas aider directement avec les réponses d'examen, mais je peux expliquer les concepts après soumission.",
'de': 'Ich kann nicht direkt bei Prüfungsantworten helfen, aber nach der Abgabe kann ich Konzepte erklären.',
};

// ==================== THRESHOLDS ====================

const SKILLBRIDGE_THRESHOLDS = {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ const SKILLJAR_SELECTORS = {
// FAQ
faqTitle: '.faq-title',
faqPost: '.faq-post',

// Quiz / Assessment / Certification
quizForm: '.quiz-form, .assessment-form, form[class*="quiz"], form[class*="assessment"]',
answerOption: '.answer-option, .answer-choice, .quiz-option, .assessment-option',
answerLabel: 'label[class*="answer"], label[class*="option"], label[class*="choice"]',
quizResult: '.quiz-result, .assessment-result, .quiz-score, .score-display',
certificateSection: '.certificate-section, .certificate-panel, .certificate-container',
};
9 changes: 7 additions & 2 deletions tests/constants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@
const fs = require('fs');
const path = require('path');

const selectorsSrc = fs.readFileSync(
path.join(__dirname, '..', 'src', 'lib', 'selectors.js'), 'utf8'
);
const constantsSrc = fs.readFileSync(
path.join(__dirname, '..', 'src', 'lib', 'constants.js'), 'utf8'
);

// Eval constants into a returned object so they're accessible in test scope
const constants = new Function(`${constantsSrc}; return {
// Eval selectors first (constants.js references SKILLJAR_SELECTORS), then constants
const constants = new Function(`${selectorsSrc}\n${constantsSrc}; return {
SKILLBRIDGE_MODELS, SKILLBRIDGE_THRESHOLDS, SKILLBRIDGE_DELAYS, SKILLBRIDGE_LIMITS,
PREMIUM_LANGUAGES, AVAILABLE_LANGUAGES, AVAILABLE_LANGUAGE_CODES,
SUPPORTED_LANGUAGE_MAP, POPUP_LABELS, DEFAULT_PROTECTED_TERMS,
YOUTUBE_CLIENT_VERSION, SKILLBRIDGE_MODEL_LABELS,
SHORTCUT_LABELS, SHORTCUT_DESCRIPTIONS,
EXAM_URL_PATTERNS, EXAM_SKIP_SELECTORS, EXAM_BANNER_LABELS, TUTOR_EXAM_LABELS,
SKILLJAR_SELECTORS,
};`)();

const {
Expand Down
Loading
Loading