diff --git a/README.md b/README.md index d568da3..b423d86 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ Learn more: [electronjs/doc](https://www.electronjs.org/docs/latest/api/extensio | [@noi/ask-custom](https://github.com/lencx/Noi/tree/main/extensions/noi-ask-custom) | 0.1.0 | The best assistant for batch asking and quick typing of prompts. | | [@noi/export-chatgpt](https://github.com/lencx/Noi/tree/main/extensions/noi-export-chatgpt) | 0.1.1 | ChatGPT chat history export, supports PDF, Image, and Markdown formats. | | [@noi/reset](https://github.com/lencx/Noi/tree/main/extensions/noi-reset) | 0.1.3 | Reset certain website styles to enhance compatibility with Noi. | -| [Thinking Claude](https://github.com/lencx/Noi/tree/main/extensions/thinking-claude) | 0.1.0 | Makes Claude's thinking process expandable and collapsible | +| [Thinking Claude](https://github.com/lencx/Noi/tree/main/extensions/thinking-claude) | 0.1.1 | Makes Claude's thinking process expandable and collapsible | [![Star History Chart](https://api.star-history.com/svg?repos=lencx/Noi&type=Timeline)](https://star-history.com/#lencx/Noi&Timeline) diff --git a/extensions/README.md b/extensions/README.md index a7eea3c..5174590 100644 --- a/extensions/README.md +++ b/extensions/README.md @@ -13,5 +13,5 @@ Learn more: [electronjs/doc](https://www.electronjs.org/docs/latest/api/extensio | [@noi/ask-custom](https://github.com/lencx/Noi/tree/main/extensions/noi-ask-custom) | 0.1.0 | The best assistant for batch asking and quick typing of prompts. | | [@noi/export-chatgpt](https://github.com/lencx/Noi/tree/main/extensions/noi-export-chatgpt) | 0.1.1 | ChatGPT chat history export, supports PDF, Image, and Markdown formats. | | [@noi/reset](https://github.com/lencx/Noi/tree/main/extensions/noi-reset) | 0.1.3 | Reset certain website styles to enhance compatibility with Noi. | -| [Thinking Claude](https://github.com/lencx/Noi/tree/main/extensions/thinking-claude) | 0.1.0 | Makes Claude's thinking process expandable and collapsible | +| [Thinking Claude](https://github.com/lencx/Noi/tree/main/extensions/thinking-claude) | 0.1.1 | Makes Claude's thinking process expandable and collapsible | \ No newline at end of file diff --git a/extensions/noi.extensions.json b/extensions/noi.extensions.json index b2bda13..146e6eb 100644 --- a/extensions/noi.extensions.json +++ b/extensions/noi.extensions.json @@ -37,7 +37,7 @@ { "name": "Thinking Claude", "description": "Makes Claude's thinking process expandable and collapsible", - "version": "0.1.0", + "version": "0.1.1", "url": "https://github.com/lencx/Noi/tree/main/extensions/thinking-claude", "dirname": "thinking-claude", "disabled": false diff --git a/extensions/thinking-claude/main.js b/extensions/thinking-claude/main.js index 53e5d8b..877da04 100644 --- a/extensions/thinking-claude/main.js +++ b/extensions/thinking-claude/main.js @@ -1,31 +1,86 @@ class CodeBlockCollapser { + static SELECTORS = { + PRE: 'pre', + CODE_CONTAINER: '.code-block__code', + MAIN_CONTAINER: '.relative.flex.flex-col', + THINKING_LABEL: '.text-text-300', + ORIGINAL_COPY_BTN: '.pointer-events-none', + CODE: 'code' + }; + + static CLASSES = { + THINKING_HEADER: 'thinking-header', + COPY_CONTAINER: 'from-bg-300/90 to-bg-300/70 pointer-events-auto rounded-md bg-gradient-to-b p-0.5 backdrop-blur-md', + COPY_BUTTON: 'flex flex-row items-center gap-1 rounded-md p-1 py-0.5 text-xs transition-opacity delay-100 hover:bg-bg-200 opacity-60 hover:opacity-100', + COPY_TEXT: 'text-text-200 pr-0.5', + TOGGLE_BUTTON: 'flex items-center text-text-500 hover:text-text-300', + TOGGLE_LABEL: 'font-medium text-sm' + }; + + static ICONS = { + COPY: ``, + TICK: ``, + ARROW: `` + }; + + static TIMINGS = { + RETRY_DELAY: 1000, + MUTATION_DELAY: 100, + CHECK_INTERVAL: 2000, + COPY_FEEDBACK: 2000, + MAX_RETRIES: 10 + }; + + static STYLES = { + HEADER: 'display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: var(--bg-300);', + CODE_CONTAINER: ` + transition: all 0.3s ease-in-out; + overflow: hidden; + max-height: 0; + opacity: 0; + padding: 0; + max-width: 100%; + display: block; + `, + CODE_ELEMENT: ` + white-space: pre-wrap !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + display: block !important; + max-width: 100% !important; + ` + }; + constructor() { - console.log("CodeBlockCollapser: Initializing..."); // Debug log this.initWithRetry(); } - initWithRetry(retryCount = 0) { - if (retryCount > 10) { - console.log("CodeBlockCollapser: Max retries reached"); // Debug log + createElement(tag, className = '', innerHTML = '') { + const element = document.createElement(tag); + if (className) element.className = className; + if (innerHTML) element.innerHTML = innerHTML; + return element; + } + + async initWithRetry(retryCount = 0) { + if (retryCount >= CodeBlockCollapser.TIMINGS.MAX_RETRIES) { return; } - const blocks = document.querySelectorAll("pre"); + const blocks = document.querySelectorAll(CodeBlockCollapser.SELECTORS.PRE); if (blocks.length === 0) { - console.log("CodeBlockCollapser: No blocks found, retrying..."); // Debug log - setTimeout(() => this.initWithRetry(retryCount + 1), 1000); - return; + await new Promise(resolve => + setTimeout(resolve, CodeBlockCollapser.TIMINGS.RETRY_DELAY) + ); + return this.initWithRetry(retryCount + 1); } - console.log(`CodeBlockCollapser: Found ${blocks.length} blocks`); // Debug log this.init(); } init() { - // Initial processing this.processExistingBlocks(); - // Watch for new content const observer = new MutationObserver((mutations) => { let shouldProcess = false; @@ -40,8 +95,7 @@ class CodeBlockCollapser { }); if (shouldProcess) { - console.log("CodeBlockCollapser: New content detected"); // Debug log - setTimeout(() => this.processExistingBlocks(), 100); + this.processExistingBlocks(); } }); @@ -49,153 +103,167 @@ class CodeBlockCollapser { childList: true, subtree: true, attributes: true, - attributeFilter: ["data-is-streaming"], + attributeFilter: ["data-is-streaming"] }); - // Additional periodic check setInterval(() => { this.processExistingBlocks(); }, 2000); } - createCopyButton() { - const container = document.createElement("div"); - container.className = - "from-bg-300/90 to-bg-300/70 pointer-events-auto rounded-md bg-gradient-to-b p-0.5 backdrop-blur-md"; + setupObserver() { + const observer = new MutationObserver(this.handleMutations.bind(this)); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['data-is-streaming'] + }); + } - const button = document.createElement("button"); - button.className = - "flex flex-row items-center gap-1 rounded-md p-1 py-0.5 text-xs transition-opacity delay-100 hover:bg-bg-200 opacity-60 hover:opacity-100"; + setupPeriodicCheck() { + setInterval(() => { + if (!this.processingTimeout) { + this.processExistingBlocks(); + } + }, CodeBlockCollapser.TIMINGS.CHECK_INTERVAL); + } - const copyIcon = ``; + handleMutations(mutations) { + const hasRelevantChanges = mutations.some(mutation => + mutation.addedNodes.length > 0 || + (mutation.type === 'attributes' && mutation.attributeName === 'data-is-streaming') + ); - const tickIcon = ``; + if (hasRelevantChanges) { + this.debouncedProcess(); + } + } - const iconSpan = document.createElement("span"); - iconSpan.innerHTML = copyIcon; + debouncedProcess() { + if (this.processingTimeout) { + clearTimeout(this.processingTimeout); + } - const textSpan = document.createElement("span"); - textSpan.className = "text-text-200 pr-0.5"; - textSpan.textContent = "Copy"; + this.processingTimeout = setTimeout(() => { + this.processExistingBlocks(); + this.processingTimeout = null; + }, CodeBlockCollapser.TIMINGS.MUTATION_DELAY); + } - button.appendChild(iconSpan); - button.appendChild(textSpan); + async createCopyButton() { + const container = this.createElement('div', CodeBlockCollapser.CLASSES.COPY_CONTAINER); + const button = this.createElement('button', CodeBlockCollapser.CLASSES.COPY_BUTTON); + const iconSpan = this.createElement('span', '', CodeBlockCollapser.ICONS.COPY); + const textSpan = this.createElement('span', CodeBlockCollapser.CLASSES.COPY_TEXT, 'Copy'); + + button.append(iconSpan, textSpan); container.appendChild(button); - button.addEventListener("click", async () => { - const codeText = button - .closest("pre") - ?.querySelector("code")?.textContent; - if (codeText) { + button.addEventListener('click', async () => { + const codeText = button.closest(CodeBlockCollapser.SELECTORS.PRE) + ?.querySelector(CodeBlockCollapser.SELECTORS.CODE)?.textContent; + + if (!codeText) return; + + try { await navigator.clipboard.writeText(codeText); - iconSpan.innerHTML = tickIcon; - textSpan.textContent = "Copied!"; + iconSpan.innerHTML = CodeBlockCollapser.ICONS.TICK; + textSpan.textContent = 'Copied!'; setTimeout(() => { - iconSpan.innerHTML = copyIcon; - textSpan.textContent = "Copy"; - }, 2000); + iconSpan.innerHTML = CodeBlockCollapser.ICONS.COPY; + textSpan.textContent = 'Copy'; + }, CodeBlockCollapser.TIMINGS.COPY_FEEDBACK); + } catch (error) { + console.error('Failed to copy:', error); } }); return container; } + createToggleButton() { + const button = this.createElement('button', CodeBlockCollapser.CLASSES.TOGGLE_BUTTON); + button.innerHTML = ` + ${CodeBlockCollapser.ICONS.ARROW} + View thinking process + `; + return button; + } + processExistingBlocks() { - document.querySelectorAll("pre").forEach((pre) => { - const header = pre.querySelector(".text-text-300"); - if ( - header && - header.textContent.trim() === "thinking" && - !pre.querySelector(".thinking-header") - ) { - console.log("CodeBlockCollapser: Processing block"); // Debug log + document.querySelectorAll(CodeBlockCollapser.SELECTORS.PRE).forEach(pre => { + const header = pre.querySelector(CodeBlockCollapser.SELECTORS.THINKING_LABEL); + if (header?.textContent.trim() === 'thinking' && + !pre.querySelector(`.${CodeBlockCollapser.CLASSES.THINKING_HEADER}`)) { this.processBlock(pre); } }); } - processBlock(pre) { - // Create header container - const headerContainer = document.createElement("div"); - headerContainer.className = "thinking-header"; - headerContainer.style.cssText = - "display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: var(--bg-300);"; + setupCodeContainer(container, toggleBtn) { + if (!container) return; - // Create toggle button - const toggleBtn = document.createElement("button"); - toggleBtn.className = "flex items-center text-text-500 hover:text-text-300"; - toggleBtn.innerHTML = ` - - - - View thinking process - `; + container.style.cssText = CodeBlockCollapser.STYLES.CODE_CONTAINER; - // Create copy button - const copyBtn = this.createCopyButton(); - - // Add buttons to header - headerContainer.appendChild(toggleBtn); - headerContainer.appendChild(copyBtn); - - // Setup code container - const codeContainer = pre.querySelector(".code-block__code"); - if (codeContainer) { - // Set initial styles - codeContainer.style.transition = "all 0.3s ease-in-out"; - codeContainer.style.overflow = "hidden"; - codeContainer.style.maxHeight = "0"; - codeContainer.style.opacity = "0"; - codeContainer.style.padding = "0"; - - toggleBtn.addEventListener("click", () => { - const isCollapsed = codeContainer.style.maxHeight === "0px"; - const arrow = toggleBtn.querySelector("svg"); - - if (isCollapsed) { - codeContainer.style.maxHeight = "1000px"; - codeContainer.style.opacity = "1"; - codeContainer.style.padding = "1em"; - arrow.style.transform = "rotate(180deg)"; - toggleBtn.querySelector("span").textContent = "Hide thinking process"; - } else { - codeContainer.style.maxHeight = "0"; - codeContainer.style.opacity = "0"; - codeContainer.style.padding = "0"; - arrow.style.transform = "rotate(0deg)"; - toggleBtn.querySelector("span").textContent = "View thinking process"; - } - }); + const codeElement = container.querySelector(CodeBlockCollapser.SELECTORS.CODE); + if (codeElement) { + codeElement.style.cssText = CodeBlockCollapser.STYLES.CODE_ELEMENT; } - // Add to DOM - const mainContainer = pre.querySelector(".relative.flex.flex-col"); - if (mainContainer) { - mainContainer.insertBefore( - headerContainer, - pre.querySelector(".code-block__code").parentElement - ); + this.setupToggleHandler(toggleBtn, container); + } - // Hide original elements - const thinkingLabel = pre.querySelector(".text-text-300"); - const originalCopyBtn = pre.querySelector(".pointer-events-none"); - if (thinkingLabel) thinkingLabel.style.display = "none"; - if (originalCopyBtn) originalCopyBtn.style.display = "none"; + setupToggleHandler(toggleBtn, codeContainer) { + toggleBtn.addEventListener('click', () => { + const isCollapsed = codeContainer.style.maxHeight === '0px'; + const arrow = toggleBtn.querySelector('svg'); + const label = toggleBtn.querySelector('span'); + + Object.assign(codeContainer.style, { + maxHeight: isCollapsed ? '1000px' : '0', + opacity: isCollapsed ? '1' : '0', + padding: isCollapsed ? '1em' : '0' + }); + + arrow.style.transform = `rotate(${isCollapsed ? 180 : 0}deg)`; + label.textContent = isCollapsed ? 'Hide thinking process' : 'View thinking process'; + }); + } + + async processBlock(pre) { + const headerContainer = this.createElement('div', CodeBlockCollapser.CLASSES.THINKING_HEADER); + headerContainer.style.cssText = CodeBlockCollapser.STYLES.HEADER; + + const toggleBtn = this.createToggleButton(); + const copyBtn = await this.createCopyButton(); + + headerContainer.append(toggleBtn, copyBtn); + + const codeContainer = pre.querySelector(CodeBlockCollapser.SELECTORS.CODE_CONTAINER); + this.setupCodeContainer(codeContainer, toggleBtn); + + const mainContainer = pre.querySelector(CodeBlockCollapser.SELECTORS.MAIN_CONTAINER); + if (mainContainer) { + const codeParent = pre.querySelector(CodeBlockCollapser.SELECTORS.CODE_CONTAINER)?.parentElement; + if (codeParent) { + mainContainer.insertBefore(headerContainer, codeParent); + } + + [CodeBlockCollapser.SELECTORS.THINKING_LABEL, CodeBlockCollapser.SELECTORS.ORIGINAL_COPY_BTN] + .forEach(selector => { + const element = pre.querySelector(selector); + if (element) element.style.display = 'none'; + }); } } } -// Initialize with both DOMContentLoaded and load events -document.addEventListener("DOMContentLoaded", () => { - console.log("CodeBlockCollapser: DOMContentLoaded fired"); // Debug log - new CodeBlockCollapser(); -}); - -window.addEventListener("load", () => { - console.log("CodeBlockCollapser: Load fired"); // Debug log - new CodeBlockCollapser(); -}); - -// Immediate initialization as well new CodeBlockCollapser(); + +document.addEventListener("DOMContentLoaded", () => { + if (!window.codeBlockCollapser) { + window.codeBlockCollapser = new CodeBlockCollapser(); + } +}); \ No newline at end of file diff --git a/extensions/thinking-claude/manifest.json b/extensions/thinking-claude/manifest.json index 8d14509..8151e28 100644 --- a/extensions/thinking-claude/manifest.json +++ b/extensions/thinking-claude/manifest.json @@ -1,10 +1,9 @@ { "manifest_version": 3, "name": "Thinking Claude", - "version": "0.1.0", + "version": "0.1.1", "description": "Makes Claude's thinking process expandable and collapsible", "homepage": "https://github.com/lencx/Noi/tree/main/extensions/thinking-claude", - "permissions": ["activeTab"], "content_scripts": [ { "matches": ["https://claude.ai/*"],