Skip to content
Draft
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: 2 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"src/content/content-messages.js",
"src/content/content-dom-observer.js",
"src/content/content-term-preview.js",
"src/content/content-surface.js",
"src/content/content.js",
"src/content/gt-queue.js",
"src/content/banners.js",
Expand All @@ -51,6 +52,7 @@
"src/content/shadow-css.js",
"src/content/ui-root.js",
"src/content/pdf-export.js",
"src/content/chat-message-dom.js",
"src/content/sidebar-chat.js",
"src/content/chat-subpanels.js",
"src/content/chat-history.js",
Expand Down
109 changes: 109 additions & 0 deletions src/content/chat-message-dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* SkillBridge — chat message DOM helpers.
*
* Keeps sidebar-chat.js focused on chat flow control while this module owns
* the repeated bubble markup and retry/error DOM mutations.
*/

(function () {
'use strict';

const sb = window._sb;
if (!sb) {
console.warn('[SkillBridge] chat-message-dom: _sb not ready');
return;
}

sb._chat = sb._chat || {};

function appendBotMessage(messages, html, attrs = '') {
messages.insertAdjacentHTML(
'beforeend',
`
<div class="si18n-chat-msg si18n-chat-bot"${attrs}>
<div class="si18n-chat-avatar">AI</div>
<div class="si18n-chat-bubble">${html}</div>
</div>
`,
);
}

function appendOfflineMessage(messages) {
appendBotMessage(messages, sb.escapeHtml(sb.t(TUTOR_OFFLINE_LABELS)), ' role="presentation"');
messages.lastElementChild?.querySelector('.si18n-chat-bubble')?.setAttribute('role', 'alert');
}

function appendUserMessage(messages, text, quotedText) {
const displayHtml = quotedText
? `<div class="si18n-chat-quote" style="margin-bottom:4px">${sb.escapeHtml(quotedText)}</div>${sb.escapeHtml(text)}`
: sb.escapeHtml(text);
messages.insertAdjacentHTML(
'beforeend',
`
<div class="si18n-chat-msg si18n-chat-user">
<div class="si18n-chat-bubble">${displayHtml}</div>
<div class="si18n-chat-avatar">You</div>
</div>
`,
);
}

function appendLoadingMessage(messages, loadingId) {
appendBotMessage(
messages,
`
<span class="si18n-thinking-dots" role="status" aria-label="${sb.t(A11Y_LABELS.loading)}">
<span class="si18n-dot"></span>
<span class="si18n-dot"></span>
<span class="si18n-dot"></span>
</span>
`,
` id="${loadingId}"`,
);
}

function startStreamingBubble(bubble) {
if (!bubble) return;
bubble.innerHTML = '';
bubble.classList.add('si18n-streaming-cursor');
}

function renderStreamingText(bubble, fullText) {
if (!bubble) return;
bubble.innerHTML = sb._chat.formatResponse(fullText);
}

function finishStreamingBubble(bubble) {
bubble?.classList.remove('si18n-streaming-cursor');
}

function renderRetryableError(bubble, retryLabel, onRetry) {
if (!bubble) return;
bubble.classList.remove('si18n-streaming-cursor');
bubble.setAttribute('role', 'alert');
bubble.textContent = sb.t(CHAT_ERROR_LABELS) + ' ';
const retryBtn = document.createElement('button');
retryBtn.className = 'si18n-retry-btn';
retryBtn.textContent = '\u21bb';
retryBtn.title = retryLabel;
retryBtn.addEventListener('click', onRetry);
bubble.appendChild(retryBtn);
}

function appendExamWarning(messages) {
appendBotMessage(messages, sb.escapeHtml(sb.t(TUTOR_EXAM_LABELS)));
messages.lastElementChild?.querySelector('.si18n-chat-bubble')?.classList.add('si18n-exam-warning');
}

sb._chat.dom = {
appendOfflineMessage,
appendUserMessage,
appendLoadingMessage,
startStreamingBubble,
renderStreamingText,
finishStreamingBubble,
renderRetryableError,
appendExamWarning,
};
sb.registerModule?.('chat-message-dom');
})();
45 changes: 45 additions & 0 deletions src/content/content-surface.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* SkillBridge — content-script UI surface helpers.
*
* Loaded before content.js. This file deliberately does not read window._sb:
* content.js owns that namespace and passes it in after construction.
*/

(function () {
'use strict';

const CERTIFICATION_SURFACE_SELECTORS = [
'#si18n-header-lang',
'#si18n-dark-toggle',
'#si18n-welcome-banner',
'#si18n-term-preview',
'#si18n-exam-banner',
'#si18n-reading-bar',
'.si18n-ask-tutor-btn',
];

const NON_AI_SURFACE_SELECTORS = [...CERTIFICATION_SURFACE_SELECTORS, '#si18n-toc-toggle', '#si18n-toc-panel'];

function removeUiHost(doc, sb) {
const uiHost = doc.getElementById('skillbridge-root');
if (uiHost) uiHost.remove();
if (sb) sb._uiHost = null;
}

function removeSelectors(doc, selectors) {
for (const sel of selectors) {
doc.querySelectorAll(sel).forEach((el) => el.remove());
}
}

function removeContentSurfaces(doc, sb, selectors) {
removeUiHost(doc, sb);
removeSelectors(doc, selectors);
}

window._sbContentSurface = {
CERTIFICATION_SURFACE_SELECTORS,
NON_AI_SURFACE_SELECTORS,
removeContentSurfaces,
};
})();
62 changes: 18 additions & 44 deletions src/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@
'shadow-css',
'ui-root',
'pdf-export',
'chat-message-dom',
'sidebar-chat',
'chat-subpanels',
'chat-history',
Expand Down Expand Up @@ -556,6 +557,15 @@
originalComments.clear();
}

function injectHostSurfaces() {
if (sb.hostCaps.headerControls) {
window._sb.injectHeaderLanguageSelect?.();
window._sb.injectDarkModeToggle?.();
}
if (sb.hostCaps.sidebar) window._sb.injectSidebar?.();
if (sb.hostCaps.fab) window._sb.injectFloatingButton?.();
}

function teardownCertificationSurface() {
isCertDisabled = true;
window._sb.cancelActiveStream?.();
Expand All @@ -564,34 +574,19 @@
subtitleManager = null;
restoreOriginal();
sidebarVisible = false;

// Remove every UI surface that could translate, open the tutor, or trigger
// page actions while the SPA tab is sitting on a proctored cert page.
const uiHost = document.getElementById('skillbridge-root');
if (uiHost) uiHost.remove();
sb._uiHost = null;
for (const sel of [
'#si18n-header-lang',
'#si18n-dark-toggle',
'#si18n-welcome-banner',
'#si18n-term-preview',
'#si18n-exam-banner',
'#si18n-reading-bar',
'.si18n-ask-tutor-btn',
]) {
document.querySelectorAll(sel).forEach((el) => el.remove());
}
window._sbContentSurface.removeContentSurfaces(
document,
sb,
window._sbContentSurface.CERTIFICATION_SURFACE_SELECTORS,
);
}

function reenableAfterCertificationSurface() {
if (!isCertDisabled) return;
isCertDisabled = false;
if (sb.hostCaps.headerControls) {
window._sb.injectHeaderLanguageSelect?.();
window._sb.injectDarkModeToggle?.();
}
if (sb.hostCaps.sidebar) window._sb.injectSidebar?.();
if (sb.hostCaps.fab) window._sb.injectFloatingButton?.();
injectHostSurfaces();
}

function teardownNonAIContentSurface() {
Expand All @@ -601,23 +596,7 @@
subtitleManager = null;
if (translator) restoreOriginal();
sidebarVisible = false;

const uiHost = document.getElementById('skillbridge-root');
if (uiHost) uiHost.remove();
sb._uiHost = null;
for (const sel of [
'#si18n-header-lang',
'#si18n-dark-toggle',
'#si18n-welcome-banner',
'#si18n-term-preview',
'#si18n-exam-banner',
'#si18n-reading-bar',
'#si18n-toc-toggle',
'#si18n-toc-panel',
'.si18n-ask-tutor-btn',
]) {
document.querySelectorAll(sel).forEach((el) => el.remove());
}
window._sbContentSurface.removeContentSurfaces(document, sb, window._sbContentSurface.NON_AI_SURFACE_SELECTORS);
}

async function switchLanguage(newLang, opts = {}) {
Expand Down Expand Up @@ -836,12 +815,7 @@
init,
teardownNonAIContentSurface,
rehydrateAfterGateResume: () => {
if (sb.hostCaps.headerControls) {
window._sb.injectHeaderLanguageSelect?.();
window._sb.injectDarkModeToggle?.();
}
if (sb.hostCaps.sidebar) window._sb.injectSidebar?.();
if (sb.hostCaps.fab) window._sb.injectFloatingButton?.();
injectHostSurfaces();
runActivationCallbacks();
},
cancelActiveStream: () => window._sb.cancelActiveStream?.(),
Expand Down
Loading