import thisHtml from './sidepanel.html'; import thisCss from './sidepanel.css'; import stylesCss from '../../styles.css'; import rootCss from '../../../root.css'; import { delay, extractErrorMsg, getSaferSubstring, pickRandom_Letters, pretty, } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { ROOT_ADDR } from '@ibgib/ts-gib/dist/V1/constants.mjs'; import { IbGibAddr, } from "@ibgib/ts-gib/dist/types.mjs"; import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs"; import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs'; import { CommentIbGib_V1 } from '@ibgib/core-gib/dist/common/comment/comment-types.mjs'; import { MetaspaceService } from '@ibgib/core-gib/dist/witness/space/metaspace/metaspace-types.mjs'; import { IbGibSpaceAny } from '@ibgib/core-gib/dist/witness/space/space-base-v1.mjs'; import { getTjpAddr } from '@ibgib/core-gib/dist/common/other/ibgib-helper.mjs'; import { appendToTimeline, mut8Timeline } from '@ibgib/core-gib/dist/timeline/timeline-api.mjs'; import { alertUser, highlightElement, shadowRoot_getElementById } from "@ibgib/web-gib/dist/helpers.web.mjs"; import { IbGibDynamicComponentInstanceBase, IbGibDynamicComponentMetaBase } from "@ibgib/web-gib/dist/ui/component/ibgib-dynamic-component-bases.mjs"; import { ElementsBase, IbGibDynamicComponentInstance, IbGibDynamicComponentInstanceInitOpts } from "@ibgib/web-gib/dist/ui/component/component-types.mjs"; import { getComponentSvc } from '@ibgib/web-gib/dist/ui/component/ibgib-component-service.mjs'; import { createProjectIbGib, getProjects } from '@ibgib/web-gib/dist/common/project/project-helper.mjs'; import { ProjectIbGib_V1 } from '@ibgib/web-gib/dist/common/project/project-types.mjs'; import { GLOBAL_LOG_A_LOT, } from "../../../constants.mjs"; import { getChunkRel8nName, getGlobalMetaspace_waitIfNeeded, createChunkCommentIbGibs, getProjectTitleFromPageOrContent, } from '../../helpers.mjs'; import { ChromeAIAvailability, } from '../../chrome-ai.mjs'; import { DOMElementInfo, PageContentInfo } from '../../page-analyzer/page-analyzer-types.mjs'; import { addThinkingEntry, showThinkingLog, updateThinkingEntry } from '../../thinking-log.mjs'; import { PROJECT_TJP_ADDR_PROPNAME } from '../../constants.mjs'; import { RABBIT_HOLE_COMMENT_COMPONENT_NAME, RabbitHoleCommentComponentInstance } from '../rabbit-hole-comment/rabbit-hole-comment-component-one-file.mjs'; import { getCurrentTabURL } from '../../helpers.ext.mjs'; import { autoChunkByHeadings, getHeadingInfo, getNodeTextContent_keepspaces, } from '../../page-analyzer/page-analyzer-helpers.mjs'; import { getComponentCtorArg } from '../../../helpers.web.mjs'; const logalot = GLOBAL_LOG_A_LOT || true; export const PHASE_INFO_TEXTS_PREPARE = [ `CLICK THESE HEADINGS AND FIND THE ROOT!`, `Select the root of the page and click "Make Root".`, ``, `Note that you can click each item to navigate to it on the page.`, ``, ]; export const PHASE_INFO_TEXTS_POLISH = [ `Go through the sections, both for a high-level overview and to prune any additional cruft. Then enter a title.`, ``, `Reading experts agree that this is a crucial phase of learning to get an overview of the material.`, ]; export const PHASE_INFO_TEXTS_DIGEST = [`🧠🧠🧠`]; /** * how long to highlight the active phase div */ export const HIGHLIGHT_ACTIVE_PHASE_MS = 2_000; export const SIDEPANEL_COMPONENT_NAME: string = 'ibgib-sidepanel'; export const SIDEPANEL_AUTONEWPROJECT_TEXT = 'Auto-Create New Project'; export const SIDEPANEL_NEWPROJECT_TEXT = 'Create New Project'; export const PROJECTS_DROPDOWN_COMPONENT_NAME: string = 'ibgib-projects-dropdown'; function getProjectsDropdownOption_selectProject(): HTMLOptionElement { const optionEl = document.createElement('option'); optionEl.value = ''; optionEl.textContent = 'Select a Project'; optionEl.disabled = true; optionEl.hidden = true; return optionEl; } function getProjectsDropdownOption_newProject(): HTMLOptionElement { const optionEl = document.createElement('option'); optionEl.value = SIDEPANEL_NEWPROJECT_TEXT; optionEl.textContent = SIDEPANEL_NEWPROJECT_TEXT; return optionEl; } function getProjectsDropdownOption_autoNewProject(): HTMLOptionElement { const optionEl = document.createElement('option'); optionEl.value = SIDEPANEL_AUTONEWPROJECT_TEXT; optionEl.textContent = SIDEPANEL_AUTONEWPROJECT_TEXT; return optionEl; } export type BreakItDownScope = 'selection' | 'page'; export class SidepanelComponentMeta extends IbGibDynamicComponentMetaBase { protected override lc: string = `[${SidepanelComponentMeta.name}]`; routeRegExp: RegExp = new RegExp(`^${SIDEPANEL_COMPONENT_NAME}$`); componentName: string = SIDEPANEL_COMPONENT_NAME; constructor() { super(getComponentCtorArg()); customElements.define(this.componentName, SidepanelComponentInstance); } async createInstance(arg: { path: string; ibGibAddr: IbGibAddr; }): Promise { const lc = `${this.lc}[${this.createInstance.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } const component = document.createElement(this.componentName) as SidepanelComponentInstance; await component.initialize({ ibGibAddr: arg.ibGibAddr, meta: this, html: thisHtml, css: [rootCss, stylesCss, thisCss], }); return component; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } } interface SidepanelElements extends ElementsBase { // api availability apiAvailabilityOverlay: HTMLDivElement; summarizerStatus: HTMLLIElement; summarizerDownloadProgressContainer: HTMLDivElement; summarizerDownloadProgress: HTMLProgressElement; translatorStatus: HTMLLIElement; translatorDownloadProgressContainer: HTMLDivElement; translatorDownloadProgress: HTMLProgressElement; retryApiCheckBtn: HTMLButtonElement; // header currentHrefEl: HTMLHeadingElement; projectsDropdown: HTMLSelectElement; scopeDropdown: HTMLSelectElement; breakItDownBtn: HTMLButtonElement; showThinkingLogBtn: HTMLButtonElement; // phases phasesInfoEl: HTMLDivElement; beginDivEl: HTMLDivElement; prepareDivEl: HTMLDivElement; prepareSpanEl: HTMLSpanElement; prepareDoneBtn: HTMLButtonElement; polishDivEl: HTMLDivElement; polishSpanEl: HTMLSpanElement; polishDoneBtn: HTMLButtonElement; digestDivEl: HTMLDivElement; digestSpanEl: HTMLSpanElement; // content titleContainer: HTMLDivElement; titleInputEl: HTMLInputElement; // contentEl is required in ElementsBase domTwinContainer: HTMLDivElement; domTwinNodeTemplate: HTMLTemplateElement; domTwinGlobalCommands: HTMLDivElement; deleteSelectedNodesBtn: HTMLButtonElement; makeRootGlobalBtn: HTMLButtonElement; domTwinNodesContainer: HTMLDivElement; rabbitHoleContainer: HTMLDivElement; } export class SidepanelComponentInstance extends IbGibDynamicComponentInstanceBase implements IbGibDynamicComponentInstance { protected override lc: string = `[${SidepanelComponentInstance.name}]`; /** * for handling reverting selection of what to use to generate project. * * there were originally two dropdowns for what project and what scope (whole page vs selected text) */ protected selectedProjectAddr: IbGibAddr = ROOT_ADDR; /** * for handling reverting selection of what to use to generate project. * * there were originally two dropdowns for what project and what scope (whole page vs selected text) */ private _lastScope: string = 'page'; // to handle reverting selection /** * for making a node root */ private _fullDomTree: DOMElementInfo | undefined; /** * for making a node root */ private _currentDomRoot: DOMElementInfo | undefined; /** * for shift+click range selection */ private _lastCheckboxClicked: HTMLInputElement | undefined; /** * When true, shows extra debugging UI elements. */ private _debugMode: boolean = true; constructor() { super(); } override async initialize(opts: IbGibDynamicComponentInstanceInitOpts): Promise { const lc = `${this.lc}[${this.initialize.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } await super.initialize(opts); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private async initElements(): Promise { const lc = `${this.lc}[${this.initElements.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: genuuid)`); } if (!this.shadowRoot) { throw new Error(`(UNEXPECTED) shadowRoot is falsy. (E: a9b8c7d6e5f4d3c2b1a0b9c8d7e6f5a4)`); } const headerEl = shadowRoot_getElementById(this.shadowRoot, 'sidepanel-header'); const currentHrefEl = shadowRoot_getElementById(this.shadowRoot, 'current-href'); const projectsDropdown = shadowRoot_getElementById(this.shadowRoot, 'projects-dropdown'); const scopeDropdown = shadowRoot_getElementById(this.shadowRoot, 'scope-dropdown'); const contentEl = shadowRoot_getElementById(this.shadowRoot, 'sidepanel-content'); const breakItDownBtn = shadowRoot_getElementById(this.shadowRoot, 'break-it-down-btn'); const apiAvailabilityOverlay = shadowRoot_getElementById(this.shadowRoot, 'api-availability-overlay'); const summarizerStatus = shadowRoot_getElementById(this.shadowRoot, 'summarizer-status'); const summarizerDownloadProgressContainer = shadowRoot_getElementById(this.shadowRoot, 'summarizer-download-progress-container'); const summarizerDownloadProgress = shadowRoot_getElementById(this.shadowRoot, 'summarizer-download-progress'); const translatorStatus = shadowRoot_getElementById(this.shadowRoot, 'translator-status'); const translatorDownloadProgressContainer = shadowRoot_getElementById(this.shadowRoot, 'translator-download-progress-container'); const translatorDownloadProgress = shadowRoot_getElementById(this.shadowRoot, 'translator-download-progress'); const retryApiCheckBtn = shadowRoot_getElementById(this.shadowRoot, 'retry-api-check'); const showThinkingLogBtn = shadowRoot_getElementById(this.shadowRoot, 'show-thinking-log-btn'); // phases const phasesInfoEl = shadowRoot_getElementById(this.shadowRoot, 'sidepanel-phases-info'); const beginDivEl = shadowRoot_getElementById(this.shadowRoot, 'begin-phase-div'); const prepareDivEl = shadowRoot_getElementById(this.shadowRoot, 'prepare-phase-div'); const prepareSpanEl = shadowRoot_getElementById(this.shadowRoot, 'prepare-span'); const prepareDoneBtn = shadowRoot_getElementById(this.shadowRoot, 'prepare-done-btn'); const polishDivEl = shadowRoot_getElementById(this.shadowRoot, 'polish-phase-div'); const polishSpanEl = shadowRoot_getElementById(this.shadowRoot, 'polish-span'); const polishDoneBtn = shadowRoot_getElementById(this.shadowRoot, 'polish-done-btn'); const digestDivEl = shadowRoot_getElementById(this.shadowRoot, 'digest-phase-div'); const digestSpanEl = shadowRoot_getElementById(this.shadowRoot, 'digest-span'); const titleContainer = shadowRoot_getElementById(this.shadowRoot, 'title-container'); const titleInputEl = shadowRoot_getElementById(this.shadowRoot, 'title-input'); // dom twin UI const domTwinContainer = shadowRoot_getElementById(this.shadowRoot, 'dom-twin-container'); const domTwinNodeTemplate = shadowRoot_getElementById(this.shadowRoot, 'dom-twin-node-template'); const domTwinGlobalCommands = shadowRoot_getElementById(this.shadowRoot, 'dom-twin-global-commands'); const deleteSelectedNodesBtn = shadowRoot_getElementById(this.shadowRoot, 'delete-selected-nodes-btn'); deleteSelectedNodesBtn.addEventListener('click', () => this.handleClick_deleteSelectedNodesBtn()); const makeRootGlobalBtn = shadowRoot_getElementById(this.shadowRoot, 'make-root-global-btn'); makeRootGlobalBtn.addEventListener('click', () => this.handleClick_makeRootGlobalBtn()); // const autoChunkBtn = shadowRoot_getElementById(this.shadowRoot, 'auto-chunk-btn'); // autoChunkBtn.addEventListener('click', () => this.handleClick_autoChunkBtn()); const domTwinNodesContainer = shadowRoot_getElementById(this.shadowRoot, 'dom-twin-nodes-container'); const rabbitHoleContainer = shadowRoot_getElementById(this.shadowRoot, 'rabbit-hole-container'); this.elements = { headerEl, currentHrefEl, projectsDropdown, scopeDropdown, contentEl, breakItDownBtn, apiAvailabilityOverlay, summarizerStatus, summarizerDownloadProgressContainer, summarizerDownloadProgress, translatorStatus, translatorDownloadProgressContainer, translatorDownloadProgress, retryApiCheckBtn, showThinkingLogBtn, phasesInfoEl, beginDivEl, prepareDivEl, prepareSpanEl, prepareDoneBtn, polishDivEl, polishSpanEl, polishDoneBtn, digestDivEl, digestSpanEl, titleContainer, titleInputEl, domTwinContainer, domTwinNodeTemplate, domTwinGlobalCommands, deleteSelectedNodesBtn, makeRootGlobalBtn, // autoChunkBtn, domTwinNodesContainer, rabbitHoleContainer, }; // Set default scope to 'page' and disable the selection option initially. const selectionOption = this.elements.scopeDropdown.querySelector('option[value=\"selection\"]') as HTMLOptionElement; this.elements.scopeDropdown.value = 'page'; if (selectionOption) { selectionOption.disabled = true; } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } override async created(): Promise { const lc = `${this.lc}[${this.created.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } await this.initElements(); await this.checkApiAvailabilityAndInit(); await this.agentsInitialized; if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements is falsy after init. (E: 2c1a8d9b6c094f9b8b9a8d9c24094f9b)`); } const { projectsDropdown, scopeDropdown, beginDivEl, breakItDownBtn, prepareDoneBtn, polishDoneBtn, } = this.elements; projectsDropdown.addEventListener('change', (event) => { const lcChange = `${this.lc}[dropdown change]`; // get the option associated with the newly selected project // get the address from the option element's data-addr // this.selectedProjectAddr = console.log(`${lcChange} selected project via event.target.value: ${(event as any).target.value}`); console.log(`${lcChange} selected project via projectsDropdown.value: ${projectsDropdown.value}`) }); scopeDropdown.addEventListener('change', () => { if (scopeDropdown.value !== 'selection') { this._lastScope = scopeDropdown.value; } }); // add event listener for break it down button breakItDownBtn.addEventListener('click', () => this.handleClick_begin()); prepareDoneBtn.addEventListener('click', () => this.handleClick_prepareDoneBtn()); polishDoneBtn.addEventListener('click', () => this.handleClick_polishDoneBtn()); if (!this.elements.showThinkingLogBtn) { throw new Error(`(UNEXPECTED) showThinkingLogBtn falsy? (E: d222b64775d742e9a781c0c663a033b0)`); } this.elements.showThinkingLogBtn.addEventListener('click', () => showThinkingLog()); chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { const lc = `${this.lc}[onMessage]`; if (message.type === 'selectionChange') { if (logalot) { console.log(`${lc} received selectionChange message:`, message); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: e7b558114c9862a1d8461f7294332e25)`); } const { scopeDropdown, } = this.elements; const selectionOption = scopeDropdown.querySelector('option[value="selection"]') as HTMLOptionElement; if (!selectionOption) { return; } if (message.hasSelection) { selectionOption.disabled = false; // only set the last scope if the current scope isn't already selection if (scopeDropdown.value !== 'selection') { this._lastScope = scopeDropdown.value; } scopeDropdown.value = 'selection'; } else { // if we are currently on selection, revert to the last known scope if (scopeDropdown.value === 'selection') { scopeDropdown.value = this._lastScope; } selectionOption.disabled = true; } } else if (message.type === 'navigationComplete') { if (logalot) { console.log(`${lc} received navigationComplete message. Resetting UI.`); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: genuuid)`); } // await this.renderUI_currentTarget(); // this.resetSidepanelView(); } }); this.renderUI(); // spin off so created finishes this.renderUI_currentURL(); // spin off highlightElement({ el: beginDivEl, magicHighlightTimingMs: HIGHLIGHT_ACTIVE_PHASE_MS }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } override async disconnected(): Promise { const lc = `${this.lc}[${this.disconnected.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: genuuid)`); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } // #region handleClick private async handleClick_begin_getPageContentInfoFromTab(): Promise { const lc = `${this.lc}[${this.handleClick_begin_getPageContentInfoFromTab.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab || !tab.id) { throw new Error(`(UNEXPECTED) tab or tab.id is falsy.`); } const response = await chrome.tabs.sendMessage(tab.id, { type: 'getPageContentInfo' }); if (response && response.success && response.data) { if (logalot) { console.log(`${lc} received successful pageContentInfo response.`); } return response.data; } else { const errorMsg = response?.error || 'Received an empty or invalid response from the content script.'; console.error(`${lc} ${errorMsg}`); throw new Error(errorMsg); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } private async handleClick_begin(): Promise { const lc = `${this.lc}[${this.handleClick_begin.name}]`; let thinkingId: string | undefined; try { if (logalot) { console.log(`${lc} starting...`); } // ================================================================= // RUN THE HEADING TESTS this._runHeadingScoringTests(); // ================================================================= thinkingId = addThinkingEntry('Getting page content...'); if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy.`); } const { currentHrefEl, domTwinContainer, domTwinNodesContainer, titleContainer, beginDivEl, domTwinGlobalCommands, breakItDownBtn, prepareDivEl, prepareSpanEl, prepareDoneBtn, } = this.elements; await this.renderUI_currentURL(); domTwinNodesContainer.innerHTML = '

a few moments later...

'; domTwinContainer.classList.remove('collapsed'); const pageContentInfo = await this.handleClick_begin_getPageContentInfoFromTab(); updateThinkingEntry(thinkingId, 'Received page content info. Rendering interactive tree...'); if (logalot) { console.log(`${lc} result.bestCandidate: ${pretty(pageContentInfo.bestCandidate)} (I: 0494281af202ed4718ab50f82372c825)`); } domTwinNodesContainer.innerHTML = ''; // Clear previous content if (pageContentInfo.bestCandidate?.domInfoTree) { this._fullDomTree = pageContentInfo.bestCandidate.domInfoTree; this._currentDomRoot = this._fullDomTree; new Promise(async (resolve) => { this.elements!.titleInputEl.disabled = true; try { const title = await getProjectTitleFromPageOrContent({ pageContentInfo, content: getNodeTextContent_keepspaces(this._currentDomRoot!), }); if (title) { this.elements!.titleInputEl.value = title; } } finally { this.elements!.titleInputEl.disabled = false; resolve(undefined); } }); // spin off this._renderDomFromRoot(); // Get the first expand button in the newly rendered tree and // click it. const firstExpandBtn = this.elements.domTwinNodesContainer.querySelector('.expand-btn'); if (firstExpandBtn) { firstExpandBtn.click(); } } else { const msg = `Could not find best candidate or DOM info tree in page content.`; console.error(`${lc} ${msg}`); updateThinkingEntry(thinkingId, msg, true, true); return; /* <<<< returns early */ } this.renderUI_updateGlobalCommandsState(); // call this to ensure disabled at start domTwinGlobalCommands.classList.remove('collapsed'); beginDivEl.classList.remove('active-phase'); prepareDivEl.classList.add('active-phase'); prepareSpanEl.style.display = 'flex'; this.renderUI_updatePhaseInfo({ paragraphs: PHASE_INFO_TEXTS_PREPARE }); highlightElement({ el: prepareDivEl, magicHighlightTimingMs: HIGHLIGHT_ACTIVE_PHASE_MS }); updateThinkingEntry(thinkingId, 'Render complete.', true); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); if (thinkingId) { updateThinkingEntry(thinkingId, `Error: ${extractErrorMsg(error)}`, true, true); } } finally { if (logalot) { console.log(`${lc} complete.`); } } } private async handleClick_prepareDoneBtn(): Promise { const lc = `${this.lc}[${this.handleClick_prepareDoneBtn.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: ce575a5ccb58480d07ea89b1280f0b25)`); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: 2b2c884023e8c87db80821b55ca4e125)`); } const { titleContainer, prepareDivEl, prepareDoneBtn, polishDivEl, polishSpanEl, polishDoneBtn } = this.elements; await this.autoChunk(); this.renderUI_updateGlobalCommandsState(); // call this to ensure disabled at start prepareDivEl.classList.remove('active-phase'); polishDivEl.classList.add('active-phase'); polishSpanEl.style.display = 'flex'; highlightElement({ el: polishDivEl, magicHighlightTimingMs: HIGHLIGHT_ACTIVE_PHASE_MS }); // spin off this.renderUI_updatePhaseInfo({ paragraphs: PHASE_INFO_TEXTS_POLISH }); delay(HIGHLIGHT_ACTIVE_PHASE_MS).then(() => { highlightElement({ el: titleContainer, magicHighlightTimingMs: HIGHLIGHT_ACTIVE_PHASE_MS }); titleContainer.classList.remove('collapsed'); }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private async handleClick_polishDoneBtn(): Promise { const lc = `${this.lc}[${this.handleClick_polishDoneBtn.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: cd108beb4e4ef608b861ae683ce77c25)`); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: 1b2d4505092879ff287d58886a9bc825)`); } const { titleInputEl, currentHrefEl, polishDivEl, digestDivEl, digestSpanEl, domTwinContainer, rabbitHoleContainer, phasesInfoEl, } = this.elements; let title: string; if (titleInputEl.value) { title = titleInputEl.value; } else { alertUser({ title: 'Doh! Need title', msg: `Have to have a title`, }); return; /* <<<< returns early */ } const href = currentHrefEl.textContent; if (!href) { throw new Error(`(UNEXPECTED) href falsy? (E: 0f4a3874cfa3b6186497311753eaf825)`); } const metaspace = await getGlobalMetaspace_waitIfNeeded(); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local user space from metaspace? (E: 868fc8607daedae7d5e3b636bb4c0b25)`); } // show loading screen domTwinContainer.style.display = 'none'; rabbitHoleContainer.style.display = 'flex'; rabbitHoleContainer.innerHTML = '

a few moments later...

'; // create the root src comment ibgib from the dom root if (!this._currentDomRoot) { throw new Error(`(UNEXPECTED) this._currentDomRoot falsy? (E: 697b45590a88301589f6eae864338825)`); } let [srcCommentIbGib] = await createChunkCommentIbGibs({ domNodesOrStrings: this._currentDomRoot, metaspace, space, recursive: false, }); // now we can create the project based off this root ibgib let project: ProjectIbGib_V1; project = await this.createNewProject({ title, href, srcCommentIbGib, metaspace, space, }); // add a soft link back from the comment to the project const projectTjpAddr = getTjpAddr({ ibGib: project, defaultIfNone: 'incomingAddr' }); if (!projectTjpAddr) { throw new Error(`(UNEXPECTED) projectTjpAddr falsy? (E: 951fd86d451aaf5028f635f89e781825)`); } srcCommentIbGib = await mut8Timeline({ timeline: srcCommentIbGib, mut8Opts: { dataToAddOrPatch: { [PROJECT_TJP_ADDR_PROPNAME]: projectTjpAddr, } }, // also adding title to ibgib data metaspace, space, skipLock: true, // newly created timeline that no one knows about }); project = await appendToTimeline({ timeline: project, rel8nInfos: [{ rel8nName: getChunkRel8nName({ contextScope: 'default' }), ibGibs: [srcCommentIbGib], }], metaspace, space, skipLock: false, }) as ProjectIbGib_V1; // create the rest of the root comment's children and relate to the root comment if (!this._currentDomRoot) { throw new Error(`(UNEXPECTED) this._currentDomRoot falsy? (E: c293381ba4df029a38b5181788881825)`); } let childrenChunkIbGibs = await createChunkCommentIbGibs({ domNodesOrStrings: this._currentDomRoot.content, project, parentCommentIbGib: srcCommentIbGib, recursive: true, metaspace, space, }); if (childrenChunkIbGibs.length > 0) { srcCommentIbGib = await appendToTimeline({ timeline: srcCommentIbGib, rel8nInfos: [{ rel8nName: getChunkRel8nName({ contextScope: 'default' }), ibGibs: childrenChunkIbGibs, }], metaspace, space, }) as CommentIbGib_V1; } // create and inject the comment component corresponding to the // newSrcCommentIbGib const srcCommentAddr = getIbGibAddr({ ibGib: srcCommentIbGib }); const componentSvc = await getComponentSvc(); const commentComponent = await componentSvc.getComponentInstance({ path: RABBIT_HOLE_COMMENT_COMPONENT_NAME, ibGibAddr: srcCommentAddr, useRegExpPrefilter: true, }) as RabbitHoleCommentComponentInstance; if (!commentComponent) { throw new Error(`(UNEXPECTED) commentComponent falsy? couldn't get a commentComponent? (E: genuuid)`); } await componentSvc.inject({ parentEl: rabbitHoleContainer, componentToInject: commentComponent, }); // // FINALLY, UPDATE THE UI // await this.renderUI_projectsDropdown(); polishDivEl.classList.remove('active-phase'); digestDivEl.classList.add('active-phase'); digestSpanEl.style.display = 'flex'; highlightElement({ el: digestDivEl, magicHighlightTimingMs: HIGHLIGHT_ACTIVE_PHASE_MS }); // spin off this.renderUI_updatePhaseInfo({ paragraphs: PHASE_INFO_TEXTS_DIGEST }); delay(HIGHLIGHT_ACTIVE_PHASE_MS).then(() => { phasesInfoEl.style.display = 'none'; }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private handleClick_deleteSelectedNodesBtn(): void { const lc = `${this.lc}[${this.handleClick_deleteSelectedNodesBtn.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy?`); } if (!this._currentDomRoot) { console.error(`${lc} _currentDomRoot is not set. Cannot delete nodes.`); return; } const { domTwinNodesContainer } = this.elements; const checkedBoxes = domTwinNodesContainer.querySelectorAll('.node-select-checkbox:checked'); // Step 1: Collect all gibIds to be deleted. const gibIdsToRemove = new Set(); checkedBoxes.forEach(checkbox => { const nodeEl = checkbox.closest('.dom-twin-node'); const gibId = nodeEl?.dataset.gibId; if (gibId) { gibIdsToRemove.add(gibId); } }); if (gibIdsToRemove.size === 0) { return; } // Nothing selected // Step 2: Actually remove the nodes from the underlying data tree. this._recursivelyRemoveNodesByGibId(gibIdsToRemove, this._currentDomRoot); // Step 3: Remove the nodes from the UI, now that data is updated. checkedBoxes.forEach(checkbox => { checkbox.closest('.dom-twin-node')?.remove(); }); // Step 4: Update the state of the global command buttons. this.renderUI_updateGlobalCommandsState(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private handleClick_makeRootGlobalBtn(): void { const lc = `${this.lc}[${this.handleClick_makeRootGlobalBtn.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: fa88c471b37d6c1f18d00c28ae149825)`); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: 3d5e388e0b5868d737dbb828c3369825)`); } const { domTwinNodesContainer } = this.elements; const checkedCheckbox = domTwinNodesContainer.querySelector('.node-select-checkbox:checked') as HTMLInputElement; if (checkedCheckbox) { const nodeEl = checkedCheckbox.closest('.dom-twin-node') as HTMLElement; const gibId = nodeEl?.dataset.gibId; if (gibId) { this.makeNodeRoot(gibId); } else { console.error(`${lc} Global "Make Root" button clicked, but could not find gibId on the selected node.`); } } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } // #endregion handleClick // #region renderUI methods protected override async renderUI(): Promise { const lc = `${this.lc}[${this.renderUI.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 2a679810c7a800276cc8c8687c23f725)`); } await super.renderUI(); if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: a890781e65c6a3ea68c7ff7f73dac825)`); } const { } = this.elements; await this.renderUI_projectsDropdown(); // await this.renderUI_currentTarget(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private renderUI_DomInfoNode(nodeInfo: DOMElementInfo, parentEl: HTMLElement): void { const lc = `${this.lc}[${this.renderUI_DomInfoNode.name}]`; try { if (!this.elements) { throw new Error("(UNEXPECTED) this.elements falsy."); } const { domTwinNodeTemplate } = this.elements; const nodeClone = domTwinNodeTemplate.content.cloneNode(true) as DocumentFragment; const nodeEl = nodeClone.querySelector('.dom-twin-node') as HTMLElement; const nodeHeader = nodeClone.querySelector('.node-header') as HTMLElement; const expandBtn = nodeClone.querySelector('.expand-btn') as HTMLButtonElement; const checkbox = nodeClone.querySelector('.node-select-checkbox') as HTMLInputElement; const tagNameSpan = nodeClone.querySelector('.node-tag-name') as HTMLSpanElement; const textPreviewSpan = nodeClone.querySelector('.node-text-preview') as HTMLSpanElement; const childrenContainer = nodeClone.querySelector('.node-children-container') as HTMLDivElement; const deleteBtn = nodeClone.querySelector('.delete-node-btn') as HTMLButtonElement; const makeRootBtn = nodeClone.querySelector('.make-root-btn') as HTMLButtonElement; tagNameSpan.textContent = `<${nodeInfo.tagName}>`; if (nodeInfo.gibId) { nodeEl.dataset.gibId = nodeInfo.gibId; } // const textContent = this._getNodeTextContent(nodeInfo); const textContent = this._getNodeTextContent(nodeInfo); if (textContent) { const previewText = textContent.substring(0, 30); textPreviewSpan.textContent = previewText + (textContent.length > 30 ? '...' : ''); } const headingScore = getHeadingInfo(nodeInfo).headingScore; // if (headingScore > 0) { const scoreSpan = document.createElement('span'); scoreSpan.textContent = ` (Score: ${headingScore})`; scoreSpan.style.fontSize = '0.8em'; scoreSpan.style.color = '#999'; scoreSpan.style.marginLeft = '5px'; textPreviewSpan.insertAdjacentElement('afterend', scoreSpan); checkbox.addEventListener('click', (e) => { const currentCheckbox = e.target as HTMLInputElement; if (e.shiftKey && this._lastCheckboxClicked) { // Shift-click logic if (!this.elements) { return; } const { domTwinNodesContainer } = this.elements; const allCheckboxes = Array.from(domTwinNodesContainer.querySelectorAll('.node-select-checkbox')) as HTMLInputElement[]; const lastClickedIndex = allCheckboxes.indexOf(this._lastCheckboxClicked); const currentIndex = allCheckboxes.indexOf(currentCheckbox); if (lastClickedIndex !== -1 && currentIndex !== -1) { const start = Math.min(lastClickedIndex, currentIndex); const end = Math.max(lastClickedIndex, currentIndex); const shouldBeChecked = currentCheckbox.checked; for (let i = start; i <= end; i++) { allCheckboxes[i].checked = shouldBeChecked; } } } this._lastCheckboxClicked = currentCheckbox; this.renderUI_updateGlobalCommandsState(); }); deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); if (!this._currentDomRoot) { console.error(`${lc} _currentDomRoot is not set. Cannot delete node.`); // still remove the element from the UI for user feedback nodeEl.remove(); return; } // Get the gibId for the node we want to remove. const gibId = nodeEl.dataset.gibId; if (gibId) { // Create a set with just this one ID. const gibIdsToRemove = new Set([gibId]); // Call the recursive function to remove it from the data tree. this._recursivelyRemoveNodesByGibId(gibIdsToRemove, this._currentDomRoot); } else { console.warn(`${lc} Could not find gibId on node to delete.`); } // Now remove the element from the UI and update global state. nodeEl.remove(); this.renderUI_updateGlobalCommandsState(); }); // deleteBtn.addEventListener('click', (e) => { // e.stopPropagation(); // nodeEl.remove(); // this.renderUI_updateGlobalCommandsState(); // }); makeRootBtn.addEventListener('click', (e) => { e.stopPropagation(); const gibId = nodeEl.dataset.gibId; if (gibId) { this.makeNodeRoot(gibId); } else { console.error(`${lc} make-root-btn clicked, but node element has no gibId. (UNEXPECTED)`); } }); if (this._debugMode) { const debugBtn = document.createElement('button'); debugBtn.textContent = '🐞'; debugBtn.classList.add('node-action-btn'); // Use same class as other buttons for styling debugBtn.title = 'Show Debug Info'; // We want to insert it alongside the other action buttons. // Assuming deleteBtn is present and has a parent. if (deleteBtn.parentElement) { deleteBtn.parentElement.insertBefore(debugBtn, deleteBtn.nextSibling); } debugBtn.addEventListener('click', (e) => { e.stopPropagation(); alertUser({ title: 'Node Info', msg: pretty(nodeInfo), }); }); } nodeHeader.addEventListener('click', async (e) => { if (e.target === expandBtn || (e.target as HTMLElement).tagName === 'INPUT' || e.target === deleteBtn || e.target === makeRootBtn) { return; } const gibId = nodeEl.dataset.gibId; if (!gibId) { return; } try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (tab && tab.id) { chrome.tabs.sendMessage(tab.id, { type: 'scrollToGib', gibId: gibId, }); } } catch (error) { console.error(`${lc} Error sending message to content script: ${extractErrorMsg(error)}`); } }); const hasChildElements = nodeInfo.content?.some(c => typeof c !== 'string'); const hasChildText = nodeInfo.content?.some(c => typeof c === 'string' && c.trim().length > 0); if (hasChildElements || hasChildText) { expandBtn.addEventListener('click', (e) => { e.stopPropagation(); childrenContainer.classList.toggle('collapsed'); expandBtn.textContent = childrenContainer.classList.contains('collapsed') ? '›' : '˅'; }); for (const child of nodeInfo.content) { if (typeof child === 'string') { if (child.trim()) { const textDiv = document.createElement('div'); textDiv.textContent = `"${child.trim()}"`; textDiv.classList.add('dom-twin-text'); childrenContainer.appendChild(textDiv); } } else if (child) { this.renderUI_DomInfoNode(child, childrenContainer); } } } else { expandBtn.style.visibility = 'collapsed'; } parentEl.appendChild(nodeClone); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } private _renderDomFromRoot(): void { const lc = `${this.lc}[${this._renderDomFromRoot.name}]`; try { if (!this.elements) { throw new Error("(UNEXPECTED) this.elements falsy."); } if (!this._currentDomRoot) { throw new Error("(UNEXPECTED) this._currentDomRoot is not set."); } const { domTwinNodesContainer } = this.elements; domTwinNodesContainer.innerHTML = ''; // Clear the current view this.renderUI_DomInfoNode(this._currentDomRoot, domTwinNodesContainer); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } private async renderUI_currentURL(): Promise { const lc = `${this.lc}[${this.renderUI_currentURL.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 5ddcb8ede6885f97d80c13274fa9c625)`); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy. (E: 2811587dc348ac76dc8372c813545d25)`); } const { currentHrefEl, } = this.elements; const href = await getCurrentTabURL(); currentHrefEl.textContent = href; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private async renderUI_projectsDropdown(): Promise { const lc = `${this.lc}[${this.renderUI_projectsDropdown.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 409fa21e57087fd81884e2532777c825)`); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy? (E: 6e35785bb31b548ad8373918928e5625)`); } const { projectsDropdown } = this.elements; // init the dropdown projectsDropdown.innerHTML = ''; // let's get the auto new project option implemented first // const newProjectOptionEl =getProjectsDropdownOption_newProject(); // projectsDropdown.appendChild(newProjectOptionEl); const autoNewProjectOptionEl = getProjectsDropdownOption_autoNewProject(); projectsDropdown.appendChild(autoNewProjectOptionEl); // load the projects into the dropdown const projectIbGibs = await this.getFilteredProjects(); const projectOptionEls: HTMLOptionElement[] = []; const erroredOptions: HTMLOptionElement[] = []; for (const projectIbGib of projectIbGibs) { const optionEl = document.createElement('option'); try { if (!projectIbGib.data) { throw new Error(`(UNEXPECTED) projectIbGib.data falsy? (E: 7a6a689860c3fdbbb8a411bc87ff2125)`); } if (!projectIbGib.data.name) { throw new Error(`(UNEXPECTED) project.data.name falsy? (E: 31b432799d037984ca5ba6682f21f225)`); } // set the option's props optionEl.value = getIbGibAddr(projectIbGib); optionEl.textContent = projectIbGib.data.name; // don't add the option to the dropdown yet, because of // nuances with Select Project option and currently selected addr. projectOptionEls.push(optionEl); } catch (error) { // maybe overkill here but don't want UI to error out, so // catch all errors const randomLetters = pickRandom_Letters({ count: 5 }); console.error(`${lc}[subcode: ${randomLetters}] ${extractErrorMsg(error)}`); optionEl.value = ''; optionEl.textContent = `ERROR See Console ${randomLetters} for details`; optionEl.disabled = true; erroredOptions.push(optionEl); } } // configure what is the selected option in the dropdown, and if // applicable, deselect the current selection if it's been filtered // out. if (projectOptionEls.length > 0) { // we have valid projects, so either we already have one // selected via this.selectedProjectAddr, or we want to tell the // user to select one // only add the select project if we have any projects to select const selectAProjectOptionEl = getProjectsDropdownOption_selectProject(); projectsDropdown.insertBefore(selectAProjectOptionEl, projectsDropdown.firstChild); projectOptionEls.forEach(x => projectsDropdown.appendChild(x)); if (this.selectedProjectAddr === ROOT_ADDR) { // no selected addr set projectsDropdown.value = selectAProjectOptionEl.value; } else { // we are RE-rendering the dropdown, as can be inferred by // this.selectedProjectAddr not being the root ib^gib addr. // The only thing ATOW (10/2025) that would trigger this is // a filter. /** * this is a naive check. We really should check for the * same tjp addr/tjpGib, but we'll implement this first */ const selectedOptionEl = projectOptionEls.find(x => x.value === this.selectedProjectAddr); if (selectedOptionEl) { // the selectedProjectAddr is still valid after the // filter, so we continue to select it. projectsDropdown.value = selectedOptionEl.value; } else { // the selectedProjectAddr is now filtered out, so we deselect it. await this.deselectProjectAddr(); projectsDropdown.value = selectAProjectOptionEl.value; } } } else { // we have no valid existing project options, so we don't // include the select a project option, and we default to the // auto-new-project option projectsDropdown.value = autoNewProjectOptionEl.value; await this.deselectProjectAddr(); } // append errored ones last erroredOptions.forEach(x => projectsDropdown.appendChild(x)); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private renderUI_updateGlobalCommandsState(): void { const lc = `${this.lc}[${this.renderUI_updateGlobalCommandsState.name}]`; try { if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements is falsy.`); } const { domTwinContainer, deleteSelectedNodesBtn, makeRootGlobalBtn } = this.elements; const checkedCheckboxes = domTwinContainer.querySelectorAll('.node-select-checkbox:checked'); const checkedCount = checkedCheckboxes.length; // The "Delete" button should be enabled if one or more are checked. deleteSelectedNodesBtn.disabled = checkedCount === 0; // The global "Make Root" button should only be enabled if exactly one is checked. makeRootGlobalBtn.disabled = checkedCount !== 1; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } private renderUI_updatePhaseInfo({ paragraphs }: { paragraphs: string[] }): void { const lc = `${this.lc}[${this.renderUI_updatePhaseInfo.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 114d083452c82430fe85e2f9ad7f0825)`); } if (!this.elements) { throw new Error(`(UNEXPECTED) ? (E: 8071dfc3b0c3cf65a7694a486dc0e125)`); } const { phasesInfoEl } = this.elements; phasesInfoEl.innerHTML = ''; if (paragraphs && paragraphs.length > 0) { phasesInfoEl.style.display = 'flex'; for (let x of paragraphs) { const pEl = document.createElement('p'); pEl.textContent = x; phasesInfoEl.appendChild(pEl); } highlightElement({ el: phasesInfoEl, magicHighlightTimingMs: HIGHLIGHT_ACTIVE_PHASE_MS }); } else { phasesInfoEl.style.display = 'none'; } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } // #endregion renderUI methods // #region test kluge private _runHeadingScoringTests(): void { const lc = `${this.lc}[${this._runHeadingScoringTests.name}]`; console.log(`${lc} Running heading scoring tests...`); const tests: { name: string, node: DOMElementInfo, expectedMinScore: number }[] = [ { name: "Plain Paragraph", node: { tagName: 'p', content: ['Just some normal text.'], gibId: 'a' }, expectedMinScore: 0 }, { name: "Wikipedia H2", node: { tagName: 'div', content: [ { tagName: 'h2', content: ['Function'], gibId: 'b' }, { tagName: 'span', content: [' edit '], gibId: 'm' }, ], gibId: 'c' }, expectedMinScore: 90 }, { name: "Hackathon Main Title", node: { tagName: 'p', content: [{ tagName: 'span', content: [{ tagName: 'strong', content: ['Google Chrome Built-in AI Challenge 2025 Official Rules'], gibId: 'd' }], gibId: 'e', }], gibId: 'f', }, expectedMinScore: 100 }, { name: "Hackathon Numbered Section", node: { tagName: 'p', content: [ { tagName: 'strong', content: ['1. BINDING AGREEMENT:'], gibId: 'g' }, ' In order to enter the Contest, you must agree...' ], gibId: 'h', }, expectedMinScore: 90 }, { name: "Hackathon Italic Sub-heading", node: { tagName: 'p', content: [ { tagName: 'strong', content: [ { tagName: 'em', content: ['Application Requirements:'], gibId: 'h' } ], gibId: 'i' }, ], gibId: 'j', }, expectedMinScore: 70 }, { name: "Simple H1", node: { tagName: 'h1', content: ['Main Title'], gibId: 'k' }, expectedMinScore: 100 } ]; let allTestsPassed = true; for (const test of tests) { const headingInfo = getHeadingInfo(test.node); const score = headingInfo.headingScore; const pass = score >= test.expectedMinScore; if (!pass) { allTestsPassed = false; } console.log( `${lc} [${pass ? '✅ PASS' : '❌ FAIL'}] Test: "${test.name}". Score: ${score}. (Expected >= ${test.expectedMinScore})` ); } console.log(`${lc} All tests ${allTestsPassed ? 'passed' : 'failed'}.`); console.log(`${lc} Tests complete.`); } // #endregion test kluge // #region checkApiAvailabilityAndInit private async checkApiAvailabilityAndInit_summarizer(availability: ChromeAIAvailability): Promise { const lc = `${this.lc}[checkApiAvailabilityAndInit_summarizer]`; if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements is falsy.`); } const { summarizerStatus, summarizerDownloadProgress, summarizerDownloadProgressContainer } = this.elements; const statusSpan = summarizerStatus.querySelector('.status'); if (!statusSpan) { throw new Error(`(UNEXPECTED) summarizer statusSpan is falsy.`); } switch (availability) { case ChromeAIAvailability.available: statusSpan.textContent = 'Available'; statusSpan.style.color = 'green'; summarizerDownloadProgressContainer.classList.add('collapsed'); return true; case ChromeAIAvailability.downloading: statusSpan.textContent = 'Downloading...'; statusSpan.style.color = 'orange'; summarizerDownloadProgressContainer.classList.remove('collapsed'); // We monitor the download and then throw a specific error to signal that the parent // function needs to restart the entire availability check process from the top. await Summarizer.create({ monitor: (p) => p.addEventListener('downloadprogress', (e) => { summarizerDownloadProgress.value = e.loaded * 100; }) }); throw new Error('RESTART_CHECK'); case ChromeAIAvailability.downloadable: statusSpan.textContent = 'Download Required'; statusSpan.style.color = 'orange'; return false; default: // unavailable statusSpan.textContent = 'Not Available'; statusSpan.style.color = 'red'; summarizerDownloadProgressContainer.classList.add('collapsed'); return false; } } private async checkApiAvailabilityAndInit_translator(availability: ChromeAIAvailability): Promise { const lc = `${this.lc}[checkApiAvailabilityAndInit_translator]`; if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements is falsy.`); } const { translatorStatus, translatorDownloadProgress, translatorDownloadProgressContainer } = this.elements; const statusSpan = translatorStatus.querySelector('.status'); if (!statusSpan) { throw new Error(`(UNEXPECTED) translator statusSpan is falsy.`); } switch (availability) { case ChromeAIAvailability.available: statusSpan.textContent = 'Available'; statusSpan.style.color = 'green'; translatorDownloadProgressContainer.classList.add('collapsed'); return true; case ChromeAIAvailability.downloading: statusSpan.textContent = 'Downloading...'; statusSpan.style.color = 'orange'; translatorDownloadProgressContainer.classList.remove('collapsed'); // We monitor the download and then throw a specific error to signal that the parent // function needs to restart the entire availability check process from the top. await Translator.create({ sourceLanguage: 'en', targetLanguage: 'es', monitor: (p) => p.addEventListener('downloadprogress', (e) => { translatorDownloadProgress.value = e.loaded * 100; }) }); throw new Error('RESTART_CHECK'); case ChromeAIAvailability.downloadable: statusSpan.textContent = 'Download Required'; statusSpan.style.color = 'orange'; return false; default: // unavailable statusSpan.textContent = 'Not Available'; statusSpan.style.color = 'red'; translatorDownloadProgressContainer.classList.add('collapsed'); return false; } } private async checkApiAvailabilityAndInit(): Promise { const lc = `${this.lc}[${this.checkApiAvailabilityAndInit.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements is falsy.`); } const { apiAvailabilityOverlay, retryApiCheckBtn, breakItDownBtn } = this.elements; // First, get the current availability status of both APIs. const [summarizerAvailability, translatorAvailability] = await Promise.all([ Summarizer.availability().catch(e => { console.error(`${lc} Summarizer.availability() failed: ${extractErrorMsg(e)}`); return ChromeAIAvailability.unavailable; }), Translator.availability({ sourceLanguage: 'en', targetLanguage: 'es' }).catch(e => { console.warn(`${lc} Translator.availability() check failed: ${extractErrorMsg(e)}`); return ChromeAIAvailability.unavailable; }) ]); // The sub-methods handle UI updates and trigger downloads. A 'RESTART_CHECK' error // is thrown by a sub-method if a download completes, signaling a full re-check is needed. const [isSummarizerReady, isTranslatorReady] = await Promise.all([ this.checkApiAvailabilityAndInit_summarizer(summarizerAvailability), this.checkApiAvailabilityAndInit_translator(translatorAvailability), ]); // If both APIs are ready, we can hide the overlay and enable the app. if (isSummarizerReady && isTranslatorReady) { apiAvailabilityOverlay.classList.add('collapsed'); breakItDownBtn.disabled = false; return; // All good, we are done. } // If we reach here, at least one API is not ready. Show the overlay. apiAvailabilityOverlay.classList.remove('collapsed'); breakItDownBtn.disabled = true; const isSummarizerDownloadable = summarizerAvailability === ChromeAIAvailability.downloadable; const isTranslatorDownloadable = translatorAvailability === ChromeAIAvailability.downloadable; // Configure the button to either trigger downloads or a simple retry. if (isSummarizerDownloadable || isTranslatorDownloadable) { retryApiCheckBtn.textContent = 'Download Models'; retryApiCheckBtn.onclick = async () => { retryApiCheckBtn.textContent = 'Starting Download...'; retryApiCheckBtn.disabled = true; // We only need to call create() to trigger the download. The 'downloading' // case in the sub-methods will handle monitoring on the subsequent check. if (isSummarizerDownloadable) { Summarizer.create().catch(e => console.error(`${lc} Error triggering summarizer download: ${extractErrorMsg(e)}`)); } if (isTranslatorDownloadable) { Translator.create({ sourceLanguage: 'en', targetLanguage: 'es' }).catch(e => console.error(`${lc} Error triggering translator download: ${extractErrorMsg(e)}`)); } // Give a moment for the download to start before re-checking. setTimeout(() => this.checkApiAvailabilityAndInit(), 500); }; } else { retryApiCheckBtn.textContent = 'Retry API Check'; retryApiCheckBtn.onclick = () => this.checkApiAvailabilityAndInit(); } } catch (error: any) { if (error.message === 'RESTART_CHECK') { // This is our signal to re-run the entire check from the beginning. if (logalot) { console.log(`${lc} Restarting check after download completion.`); } setTimeout(() => this.checkApiAvailabilityAndInit(), 250); // Short delay allows API state to settle. } else { // Handle unexpected errors. console.error(`${lc} ${extractErrorMsg(error)}`); if (this.elements) { this.elements.apiAvailabilityOverlay.classList.remove('collapsed'); const summarizerStatusSpan = this.elements.summarizerStatus.querySelector('.status'); if (summarizerStatusSpan) { summarizerStatusSpan.textContent = 'Error checking APIs.'; } const translatorStatusSpan = this.elements.translatorStatus.querySelector('.status'); if (translatorStatusSpan) { translatorStatusSpan.textContent = 'See console for details.'; } } } } finally { if (logalot) { console.log(`${lc} complete.`); } } } // #endregion checkApiAvailabilityAndInit private async getFilteredProjects(): Promise { const lc = `${this.lc}[${this.getFilteredProjects.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 626d34b016b48d56ee0f2bd88e708d25)`); } const metaspace = await getGlobalMetaspace_waitIfNeeded(); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local user space? (E: 5de728fad9988f0f620478585192e825)`); } const projectIbGibs = await getProjects({ metaspace, space }); // todo: add filtering here return projectIbGibs; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private async deselectProjectAddr(): Promise { const lc = `${this.lc}[${this.deselectProjectAddr.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 5b0a759afa9af83806812c989871b925)`); } if (this.selectedProjectAddr === ROOT_ADDR) { if (logalot) { console.log(`${lc} no project selected. returning early... (I: 807525dde6980e4e5e9106e89fbcc425)`); } return; /* <<<< returns early */ } // right now, this is all we do but this may be expanded in the near // future. this.selectedProjectAddr = ROOT_ADDR; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * @internal why do we have this and not just use the helper from page-analyzer-helpers.mts? */ private _getNodeTextContent(nodeInfo: DOMElementInfo): string { const lc = `${this.lc}[${this._getNodeTextContent.name}]`; try { if (!nodeInfo) { throw new Error(`(UNEXPECTED) nodeInfo falsy? (E: 0e643e3a4068e217c49b2f088d41ba25)`); } if (nodeInfo.headingInfo?.headingText) { return nodeInfo.headingInfo.headingText; /* <<<< returns early */ } if (!nodeInfo.content) { return ''; /* <<<< returns early */ } return nodeInfo.content.map(child => { if (typeof child === 'string') { return child.trim(); } else if (child) { return this._getNodeTextContent(child); } return ''; }).join(' ').replace(/\s+/g, ' '); // a single space between text parts } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); return ''; // return empty string on error } } private _findNodeByGibId(gibId: string, node: DOMElementInfo | undefined = this._fullDomTree): DOMElementInfo | undefined { if (!node) { return undefined; } if (node.gibId === gibId) { return node; } if (node.content) { for (const child of node.content) { if (typeof child !== 'string') { const found = this._findNodeByGibId(gibId, child); if (found) { return found; } } } } return undefined; } private _recursivelyRemoveNodesByGibId(gibIdsToRemove: Set, node: DOMElementInfo): void { const lc = `${this.lc}[${this._recursivelyRemoveNodesByGibId.name}]`; if (!node || !node.content || !Array.isArray(node.content)) { return; } // Filter out children whose gibId is in the set to remove node.content = node.content.filter(child => { if (typeof child === 'string') { return true; // Always keep string content } if (gibIdsToRemove.has(child.gibId)) { if (logalot) { console.log(`${lc} Removing node with gibId: ${child.gibId}`); } return false; // This is the node to remove, so filter it out } return true; // Keep this node }); // Now, recurse on the remaining children to check their content node.content.forEach(child => { if (typeof child !== 'string') { this._recursivelyRemoveNodesByGibId(gibIdsToRemove, child); } }); } private makeNodeRoot(gibId: string): void { const lc = `${this.lc}[${this.makeNodeRoot.name}]`; try { if (logalot) { console.log(`${lc} making gibId ${gibId} the root.`); } const newRoot = this._findNodeByGibId(gibId); if (newRoot) { newRoot.isRoot = true; this._currentDomRoot = newRoot; this._renderDomFromRoot(); this.renderUI_updateGlobalCommandsState(); } else { console.error(`${lc} Could not find node with gibId: ${gibId}`); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } /** * Creates a project ibgib based on the given src comment ibgib. */ private async createNewProject({ title, href, srcCommentIbGib, metaspace, space, thinkingId, }: { title: string, href: string, srcCommentIbGib: CommentIbGib_V1, metaspace: MetaspaceService, space: IbGibSpaceAny, thinkingId?: string, }): Promise { const lc = `${this.lc}[${this.createNewProject.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } const title_alphanumericsAndSpacesOnly = getSaferSubstring({ text: title, keepLiterals: [' '], replaceMap: { ':': ' -', }, }); if (thinkingId) { updateThinkingEntry(thinkingId, `Creating project: \"${title_alphanumericsAndSpacesOnly}\"...`); } const resProject = await createProjectIbGib({ name: title_alphanumericsAndSpacesOnly, description: [`This project was auto-generated.`, '', `URL: ${href}`].join('\n'), saveInSpace: true, space, srcCommentIbGib, }); const ibGib = resProject.newIbGib as ProjectIbGib_V1; if (logalot) { console.log(`${lc} newProject: ${pretty(ibGib)}`); } if (thinkingId) { updateThinkingEntry(thinkingId, 'Registering new project...'); } await metaspace.registerNewIbGib({ ibGib, space, }); return ibGib; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private async autoChunk(): Promise { const lc = `${this.lc}[${this.autoChunk.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 8a1d5d6a8658bb8ba8a6814411a45825)`); } if (!this._fullDomTree) { console.warn(`${lc} Full DOM tree is not available. Cannot auto-chunk.`); return; } // Generate the new, chunked structure if (typeof this._fullDomTree.content === "string") { console.warn(`${lc} dom tree is string? (W: 9820488c5d48daad28d93ad80ded2825)`) return; /* <<<< returns early */ } const initialRoot = this._currentDomRoot || this._fullDomTree; const chunkedTree = autoChunkByHeadings(initialRoot); // Set the new structure as the current root and re-render this._currentDomRoot = chunkedTree; this._renderDomFromRoot(); // Get the first expand button in newly chunked tree and click it. const firstExpandBtn = this.elements?.domTwinNodesContainer.querySelector('.expand-btn'); if (firstExpandBtn) { firstExpandBtn.click(); } if (logalot) { console.log(`${lc} Auto-chunking complete. DOM re-rendered.`); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } }