('.detail-view-content');
if (!translationViewContent) { throw new Error(`(UNEXPECTED) couldn't get translationViewContent from translationView? (E: d1d5822919e8d08aa1700a2360d83325)`); }
const sourceLanguage = sourceInput.value;
const targetLanguage = languageDropdown.value;
if (sourceLanguage && targetLanguage) {
/**
* right now, we're only doing ibGib.data.text translations. in
* the future, we will be doing tldr translations I think, but
* may be yagni.
*/
const dataKey = 'text';
const key = getTranslationTextKeyForIbGib({
dataKey,
targetLanguage,
});
const translationText = this.ibGib.data[key] as (string | undefined);
if (translationText) {
const paragraphs = translationText.split('\n');
const paragraphsHtml = paragraphs.map(x => `${x}
`).join('\n');
translationViewContent.innerHTML = paragraphsHtml;
} else {
translationViewContent.innerHTML = `Translating from ${sourceLanguage} into ${targetLanguage}...
`;
}
} else {
translationViewContent.innerHTML = 'Select a language...
';
}
// Toggle visibility based on the active flag.
translationView.classList.toggle('collapsed', !this.activeDetailViews.translation);
if (this.activeDetailViews.translation) {
// If the view is visible, ensure its content is up-to-date.
if (sourceInput?.value && languageDropdown?.value) {
this.renderUI_translationView_content(translationView, sourceInput.value, languageDropdown.value);
} else {
// Clear content if no language is selected.
const contentView = translationView.querySelector('.detail-view-content');
if (contentView) { contentView.innerHTML = ''; }
}
}
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
}
}
private renderUI_translationView_content(view: HTMLDivElement, sourceLanguage: string, targetLanguage: string): void {
const lc = `${this.lc}[${this.renderUI_translationView_content.name}]`;
if (!this.ibGib?.data || !view) { return; }
const contentView = view.querySelector('.detail-view-content');
if (!contentView) { console.error(`${lc} contentView not found in view.`); return; }
const dataKey = 'text'; // we only do this.ibGib.text atow 11/2025
const translationKey = getTranslationTextKeyForIbGib({
dataKey,
targetLanguage,
});
const translationText = this.ibGib.data[translationKey] as string | undefined;
if (translationText) {
const paragraphs = translationText.split('\n');
const paragraphsHtml = paragraphs.map(x => `${x}
`).join('\n');
contentView.innerHTML = paragraphsHtml;
} else {
//
// Check if this task is queued or in-progress.
let resFilter = Object.entries(this.enqueuedTasks).filter(([key, x]) => {
if (x.type !== 'translation') { return false; };
let opts = x.options as TranslationQueueInfo;
return (opts.dataKey === dataKey && opts.targetLanguage === targetLanguage);
});
if (resFilter.length === 1) {
// we just finished this task, so why no translationText?
} else if (resFilter.length > 1) {
throw new Error(`(UNEXPECTED) more than one task for the same translation opts? (E: 2bc578a50ff8b9fad25953f4b9022825)`);
} else {
// length === 0, i.e., task is just started
contentView.innerHTML = `Translating from ${sourceLanguage} into ${targetLanguage}...
`;
}
}
}
private async renderUI_keyPointsView(): Promise {
const lc = `${this.lc}[${this.renderUI_keyPointsView.name}]`;
try {
if (logalot) { console.log(`${lc} starting... (I: 8fe038f13d669c1c483887e2bcd97725)`); }
// createDetailView_keyPoints
if (!this.ibGib) { throw new Error(`(UNEXPECTED) this.ibGib falsy? (E: genuuid)`); }
if (!this.ibGib.data) { throw new Error(`(UNEXPECTED) this.ibGib.data falsy? (E: genuuid)`); }
if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: genuuid)`); }
let keypointsView = this.elements.viewContainer.querySelector('#key-points-detail-view');
if (!this.activeDetailViews.keyPoints) {
if (keypointsView) { keypointsView.classList.add('collapsed'); }
return; /* <<<< returns early */
}
// Create the view if needed
keypointsView ??= await this.createDetailView_keyPoints();
// don't do anything further atm
// console.error(`${lc} not implemented yet (E: cb04c863a828d357c7ef0a08196f6825)`);
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
} finally {
if (logalot) { console.log(`${lc} complete.`); }
}
}
private async renderUI_highlightBtn(): Promise {
const lc = `${this.lc}[${this.renderUI_highlightBtn.name}]`;
try {
if (logalot) { console.log(`${lc} starting... (I: dc571bb1253b2ea3e2dc9ab815139825)`); }
if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: 60eda849f8a44e0d6df64348c97a5825)`); }
const { highlightBtn } = this.elements;
highlightBtn.classList.toggle('active', this.isHighlighted);
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
} finally {
if (logalot) { console.log(`${lc} complete.`); }
}
}
// #endregion renderUI
// #region handleClick
private async handleClick_expandBtn(event: MouseEvent): Promise {
const lc = `${this.lc}[${this.handleClick_expandBtn.name}]`;
try {
if (logalot) { console.log(`${lc} starting...`); }
// Stop the click from bubbling up to the header's navigation handler.
event.stopPropagation();
// Toggle the internal state
this._isExpanded = !this._isExpanded;
// If we are EXPANDING and this component HAS children, default to
// showing the key points view automatically.
if (this._isExpanded && this.hasChildren) {
this.activeDetailViews.keyPoints = true;
}
// The renderUI method is now the single source of truth for visibility.
// It will show/hide the content panel and any active detail views based on component state.
await this.renderUI();
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
}
}
private async handleClick_highlight(event: MouseEvent): Promise {
const lc = `${this.lc}[${this.handleClick_highlight.name}]`;
try {
if (logalot) { console.log(`${lc} starting...`); }
event.stopPropagation(); // Prevent header navigation
// Kick off the recursive highlight, starting with the new state.
await this.setHighlightState(!this.isHighlighted);
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
}
}
private async handleClick_toggleView({
viewName,
}: {
viewName: keyof RabbitHoleCommentComponentInstance['activeDetailViews']
}): Promise {
const lc = `${this.lc}[_handleViewToggle]`;
try {
if (logalot) { console.log(`${lc} starting... viewName: ${viewName}`); }
// Determine if the view is about to become active.
const becomingActive = !this.activeDetailViews[viewName];
this.activeDetailViews[viewName] = becomingActive;
if (becomingActive) {
// If we're turning a view ON, always ensure the main panel is expanded.
this._isExpanded = true;
} else {
// If we're turning a view OFF, check if any other views are still active.
const anyViewsActive = Object.values(this.activeDetailViews).some(isActive => isActive);
if (!anyViewsActive) {
// If no views are left, collapse the main panel.
this._isExpanded = false;
}
}
// Special case: If we're activating "Key Points" for the first time, generate them.
if (viewName === 'keyPoints' && becomingActive && !this.hasChildren) {
await this.breakItDown();
// The breakItDown process calls renderUI itself, so we can exit early.
return;
}
// For all other cases, call the main render function to update the DOM.
await this.renderUI();
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
}
}
private async handleClick_header(): Promise {
const lc = `${this.lc}[${this.handleClick_header.name}]`;
try {
if (logalot) { console.log(`${lc} starting... (expanded: ${this._isExpanded})`); }
await this.scrollToGib();
this.isHighlighted = false;
this.renderUI_highlightBtn();
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
}
}
/**
* Handles a click on a length button within a TLDR detail view.
* This is where the AI summary task would be triggered.
*/
private async handleClick_tldrLength(length: SummarizerLength, view: HTMLDivElement): Promise {
const lc = `${this.lc}[${this.handleClick_tldrLength.name}]`;
try {
if (logalot) { console.log(`${lc} starting... length: ${length}`); }
if (!this.ibGib) { throw new Error(`(UNEXPECTED) this.ibGib falsy? (E: 7e6c0b4623116a4759f006760433a825)`); }
if (!this.ibGib.data) { throw new Error(`(UNEXPECTED) this.ibGib.data falsy? (E: 3c15480612df256a1d05ddb803fb1825)`); }
this.activeTldrLength = length;
await this.renderUI_tldrView();
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
}
}
// #endregion handleClick
private async handleChange_translationLanguage(event: Event): Promise {
const lc = `${this.lc}[${this.handleChange_translationLanguage.name}]`;
try {
if (!this.ibGib) { throw new Error(`(UNEXPECTED) this.ibGib falsy? (E: genuuid)`); }
if (!this.ibGib.data) { throw new Error(`(UNEXPECTED) this.ibGib.data falsy? (E: c16d4fe6e948d79f310acafee3597425)`); }
const select = event.target as HTMLSelectElement;
const view = select.closest('.detail-view');
if (!view) { throw new Error(`(UNEXPECTED) could not find parent view.`); }
const targetLanguage = select.value;
if (!targetLanguage) {
// User selected "Select...", so clear the content.
const contentView = view.querySelector('.detail-view-content');
if (contentView) { contentView.innerHTML = ''; }
return;
}
const sourceInput = view.querySelector('.source-language-input');
const sourceLanguage = sourceInput?.value ?? 'en';
const text = this.ibGib.data?.text ?? '';
if (!text) { return; } // Nothing to translate
const dataKey = 'text'; // we only do this.ibGib.text atow 11/2025
const translationKey = getTranslationTextKeyForIbGib({
dataKey,
targetLanguage,
});
const existingTranslation = this.ibGib.data[translationKey];
// If we don't already have this translation and it's not already started...
if (existingTranslation) {
await this.renderUI_translationView();
} else if (!existingTranslation && !this.enqueuedTasks[translationKey]) {
const queueSvc = getPriorityQueueSvc();
queueSvc.enqueue({
type: 'translation',
ibGib: this.ibGib,
priority: QUEUE_SERVICE_PRIORITY_USER_JUST_CLICKED,
thinkingId: this.getThinkingIdFromTjpGib(),
options: { dataKey, sourceLanguage, targetLanguage, text } as TranslationQueueInfo,
fnOnQueued: async (info) => {
// Defer to the main render loop to show "Translating..." or the result.
this.enqueuedTasks[translationKey] = info;
this.isThinking_refCount++;
await this.renderUI_thinking();
},
fnOnComplete: async (info) => {
this.isThinking_refCount--;
await this.renderUI_thinking();
},
fnOnError: async (info) => {
this.isThinking_refCount--;
await this.renderUI_thinking();
// do what here?
console.error(`${lc} the translation info failed. now what? (E: genuuid)`);
// this.tasksToHandle.push(info);
// Defer to the main render loop to show "Translating..." or the result.
await this.renderUI();
},
});
}
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
}
}
// #region createDetailView
private createDetailView_tldr(): void {
const lc = `${this.lc}[${this.createDetailView_tldr.name}]`;
try {
if (logalot) { console.log(`${lc} starting... (I: genuuid)`); }
if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: genuuid)`); }
// Clone the template
const tldrTemplate = this.elements.tldrViewTemplate;
const tldrViewClone = tldrTemplate.content.cloneNode(true) as DocumentFragment;
const tldrView = tldrViewClone.firstElementChild as HTMLDivElement;
if (!tldrView) { throw new Error(`(UNEXPECTED) Cloning tldr-view-template failed? (E: genuuid)`); }
// Wire up internal controls
const shortBtn = tldrView.querySelector('[data-view-length="short"]');
const longBtn = tldrView.querySelector('[data-view-length="long"]');
if (!shortBtn || !longBtn) { throw new Error(`(UNEXPECTED) Could not find length buttons in TLDR template clone? (E: genuuid)`); }
shortBtn.addEventListener('click', () => this.handleClick_tldrLength('short', tldrView));
longBtn.addEventListener('click', () => this.handleClick_tldrLength('long', tldrView));
// Append to the DOM
this.elements.viewContainer.appendChild(tldrView);
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
}
}
private async createDetailView_translation(): Promise {
const lc = `${this.lc}[${this.createDetailView_translation.name}]`;
try {
if (logalot) { console.log(`${lc} starting...`); }
if (!this.elements) { throw new Error(`(UNEXPECTED) elements not initialized.`); }
const template = this.elements.translationViewTemplate;
const viewClone = template.content.cloneNode(true) as DocumentFragment;
const view = viewClone.firstElementChild as HTMLDivElement;
if (!view) { throw new Error(`(UNEXPECTED) Cloning translation-view-template failed? (E: genuuid)`); }
const sourceInput = view.querySelector('.source-language-input');
const languageDropdown = view.querySelector('.language-dropdown');
if (!sourceInput || !languageDropdown) { throw new Error(`Could not find controls in Translation template clone. (E: genuuid)`); }
const updateDropdown = () => {
const sourceLang = sourceInput.value.toLowerCase();
languageDropdown.querySelectorAll('option').forEach(opt => {
const shouldHide = opt.value !== '' && opt.value === sourceLang;
opt.style.display = shouldHide ? 'none' : '';
if (shouldHide && opt.selected) { languageDropdown.value = ''; }
});
};
sourceInput.addEventListener('input', debounce(updateDropdown, 300));
// Attach our new handler to the 'change' event.
languageDropdown.addEventListener('change', (e) => this.handleChange_translationLanguage(e));
this.elements.viewContainer.appendChild(view);
// --- Language Detection ---
sourceInput.value = 'Detecting...';
sourceInput.disabled = true;
let detectedLanguage = 'en';
try {
const textToDetect = this.ibGib?.data?.text ?? '';
if (textToDetect) {
const detector = await LanguageDetector.create();
const langResults = await detector.detect(textToDetect);
detectedLanguage = (langResults[0]?.detectedLanguage) ?? 'en';
}
} catch (error) {
console.error(`${lc} Language detection failed. Defaulting to 'en'. ${extractErrorMsg(error)} (E: genuuid)`);
} finally {
sourceInput.value = detectedLanguage;
sourceInput.disabled = false;
updateDropdown();
}
return view;
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
}
}
private async createDetailView_keyPoints(): Promise {
const lc = `${this.lc}[${this.createDetailView_keyPoints.name}]`;
try {
if (logalot) { console.log(`${lc} starting... (I: genuuid)`); }
if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: genuuid)`); }
if (!this.ibGib) { throw new Error(`(UNEXPECTED) this.ibGib falsy? (E: genuuid)`); }
// 1. Clone the template
const template = this.elements.keyPointsViewTemplate;
const viewClone = template.content.cloneNode(true) as DocumentFragment;
const view = viewClone.firstElementChild as HTMLDivElement;
if (!view) { throw new Error(`(UNEXPECTED) Cloning key-points-view-template failed. (E: genuuid)`); }
// Append the view ahead of time so we see progress as children are
// added to DOM
this.elements.viewContainer.appendChild(view);
const contentView = view.querySelector('.detail-view-content');
if (!contentView) { throw new Error(`(UNEXPECTED) Could not find content area in Key Points template clone. (E: genuuid)`); }
// Get Child Data
const chunkRel8nName = getChunkRel8nName({ contextScope: 'default' });
const childAddrs = this.ibGib.rel8ns?.[chunkRel8nName] ?? [];
if (childAddrs.length === 0) {
if (logalot) { console.log(`${lc} No children to render. (I: genuuid)`); }
// We could put a message here, but for now, we'll just be blank if there are no children.
}
// Render Child Components into the new view's content area
this.childComponents = []; // clear existing component instances
const componentSvc = await getComponentSvc();
for (const childAddr of childAddrs) {
const childInstance =
await componentSvc.getComponentInstance({
path: RABBIT_HOLE_COMMENT_COMPONENT_NAME,
ibGibAddr: childAddr,
useRegExpPrefilter: true,
}) as RabbitHoleCommentComponentInstance;
if (!childInstance) {
console.error(`${lc} (UNEXPECTED) could not get a component for addr: ${childAddr} (E: genuuid)`);
continue;
}
this.childComponents.push(childInstance);
const divChild = document.createElement('div');
divChild.classList.add('comment-child');
contentView.appendChild(divChild); // Append to the content area of our new clone
await componentSvc.inject({
parentEl: divChild,
componentToInject: childInstance,
});
}
return view;
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
}
}
// #endregion createDetailView
// #region other helpers
private async scrollToGib(): Promise {
const lc = `${this.lc}[${this.scrollToGib.name}]`;
try {
if (logalot) { console.log(`${lc} starting...`); }
if (!this.ibGib) { throw new Error(`(UNEXPECTED) this.ibGib falsy? (E: 660978e4ac2832399f6f70a80b3dc725)`); }
if (!this.ib) { throw new Error(`(UNEXPECTED) this.ib falsy? (E: b12d98e127c3306178d70ff81ca93125)`); }
if (!this.ibGib.data) { throw new Error(`(UNEXPECTED) this.ibGib.data falsy? (E: 159e2542c5a8e3ac88e2f8e279236225)`); }
const gibId = this.ibGib.data.domInfo?.gibId ?? '';
if (logalot) { console.log(`${lc} Found gibId: ${gibId}. Sending message to content script.`); }
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab || !tab.id) { throw new Error(`(UNEXPECTED) could not find active tab to message.`); }
chrome.tabs.sendMessage(tab.id, {
type: 'scrollToGib',
gibId: gibId,
});
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
// Don't rethrow, as this is a non-critical UI interaction.
} finally {
if (logalot) { console.log(`${lc} complete.`); }
}
}
private async breakItDown(): Promise {
const lc = `${this.lc}[${this.breakItDown.name}]`;
if (this._breakingDown) { return; /* <<<< returns early */ }
try {
this._breakingDown = true;
// Confirm with user if children already exist.
if (this.hasChildren) {
const confirm = await promptForConfirm({
msg: "This will replace the existing key points. Are you sure?",
yesLabel: "YES, Replace Them",
noLabel: "NO, Keep Them",
});
if (!confirm) {
this.activeDetailViews.keyPoints = false; // Revert state
await this.renderUI(); // Update button to be inactive
this._breakingDown = false; // Reset flag
return; /* <<<< returns early */
}
}
// Set thinking state and update UI to show it.
this.isThinking_refCount++;
await this.renderUI_thinking();
// NOTE: The actual chunking implementation is still pending.
// We will eventually re-integrate the `chunkCommentIbGib` call here.
console.error(`${lc} not fully implemented...needs to call chunkCommentIbGib. (E: 9e4aec549ea63df5bcf2ac0a8598d525)`);
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
this.isThinking_refCount--;
this._breakingDown = false;
await this.renderUI();
}
}
/**
* If this component's ibGib does not have a title, this will use the AI
* to generate one and mut8 the ibGib, which will trigger a re-render.
* This is intended to be called after the component has already been
* rendered as a "stub".
*/
public async generateTitle({
thinkingId,
}: {
thinkingId?: string,
}): Promise {
const lc = `${this.lc}[${this.generateTitle.name}]`;
try {
if (logalot) { console.log(`${lc} starting... (I: 58cfb85d41c80aac8c6d98059ab03425)`); }
if (!this.ibGib) { throw new Error(`(UNEXPECTED) this.ibGib falsy? (E: 3ec17829f1086bf99c26e1382e8b6a25)`); }
if (!this.ibGib.data) { throw new Error(`(UNEXPECTED) this.ibGib.data falsy? (E: 341d38298578e438b8ed633954ece825)`); }
if (!this.ibGib.data.text) { throw new Error(`(UNEXPECTED) this.ibGib.data.text falsy? (E: 7e124ce0f698874198d1a9580c7d0825)`); }
if (this.ibGib.data.title) {
if (logalot) { console.log(`${lc} already has a title. (I: genuuid)`); }
return undefined; /* <<<< returns early */
}
if (thinkingId) {
updateThinkingEntry(thinkingId, `generating title for "${getSaferSubstring({ text: this.ibGib?.data?.text ?? 'hmm...wha?', length: 32 })}"...`);
}
const title = await getTitleFromSummarizer({
content: this.ibGib.data.text,
useCaseDescription: 'creating a heading in a table of contents',
thinkingId,
});
if (logalot) { console.log(`${lc} generated title: "${title}" (I: genuuid)`); }
if (thinkingId) { updateThinkingEntry(thinkingId, 'title created.'); }
return title;
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
if (thinkingId) { updateThinkingEntry(thinkingId, `title create failed: ${extractErrorMsg(error)}`, /*isComplete*/ true, /*isError*/ true); }
// throw error;
} finally {
if (logalot) { console.log(`${lc} complete.`); }
}
}
/**
* Sets the highlight state for this component and all its descendants.
* It also sends the message to the content script to update the DOM twin.
*/
private async setHighlightState(shouldBeHighlighted: boolean): Promise {
const lc = `${this.lc}[${this.setHighlightState.name}]`;
try {
if (logalot) { console.log(`${lc} starting... shouldBeHighlighted: ${shouldBeHighlighted}`); }
// 1. Set the state for the current component.
this.isHighlighted = shouldBeHighlighted;
// 2. Send the message to the content script to update the DOM twin.
const gibId = this.ibGib?.data?.domInfo?.gibId ?? '';
if (gibId) {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab?.id) {
chrome.tabs.sendMessage(tab.id, {
type: 'toggleHighlight',
gibId: gibId,
shouldBeHighlighted: this.isHighlighted,
});
}
}
// 3. Update just this component's highlight button UI.
await this.renderUI_highlightBtn();
// 4. Recursively call for all children.
for (const child of this.childComponents) {
await child.setHighlightState(shouldBeHighlighted);
}
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
}
}
private async updateBreakingDownFlag(): Promise {
const lc = `${this.lc}[${this.updateBreakingDownFlag.name}]`;
try {
if (logalot) { console.log(`${lc} starting... (I: 68c98a597bf83f6338c2645ea2ea4e25)`); }
if (!this.ibGib) { throw new Error(`(UNEXPECTED) this.ibGib falsy? (E: 08f6c868ef287e4548a8e708952bdd25)`); }
if (!this.ibGib.data) { throw new Error(`(UNEXPECTED) this.ibGib falsy? (E: 08f6c868ef287e4548a8e708952bdd25)`); }
const data = this.ibGib.data as ChunkCommentData_V1;
// const keysCompleted: string[] = [];
// Object.entries(this.queuedTasks)
// .filter(([_key, status]) => status === 'started')
// .forEach(([key, _status]) => {
// if (!!data[key]) { keysCompleted.push(key); }
// });
// keysCompleted.forEach(key => {
// this.queuedTasks[key] = 'complete';
// });
// const summaryKey = getSummaryTextKeyForIbGib({ type: view.split('_')[0] as SummarizerType, length: view.split('_')[1] as SummarizerLength });
// const summaryText = this.ibGib.data?.[summaryKey];
if (this._breakingDown && this.hasChildren) {
this._breakingDown = false;
}
} catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
} finally {
if (logalot) { console.log(`${lc} complete.`); }
}
}
private getThinkingIdFromTjpGib(): string | undefined {
if (this.ibGib) {
const tjpGib = getGibInfo({ gib: this.gib }).tjpGib ?? this.gib;
return tjpGib.substring(0, 16);
} else {
return undefined;
}
}
// #endregion other helpers
}