diff --git a/configs/noi.mode.json b/configs/noi.mode.json index c9703d4..80f6791 100644 --- a/configs/noi.mode.json +++ b/configs/noi.mode.json @@ -1,6 +1,6 @@ { "name": "Noi Mode", - "version": "0.1.1", + "version": "0.1.2", "sync": "https://github.com/lencx/noi/blob/main/configs/noi.mode.json", "modes": [ { @@ -45,6 +45,12 @@ "text": "Poe", "url": "https://poe.com" }, + { + "id": "noi:copilot", + "parent": "noi@ai", + "text": "Copilot", + "url": "https://copilot.microsoft.com" + }, { "id": "noi:perplexity", "parent": "noi@ai", diff --git a/extensions/noi-chatgpt/main.js b/extensions/noi-chatgpt/main.js new file mode 100644 index 0000000..eb42257 --- /dev/null +++ b/extensions/noi-chatgpt/main.js @@ -0,0 +1,174 @@ +const icons = { + check: ``, + noCheck: ``, +} + +const QUERY_CHAT_LIST = 'main [role="presentation"] div[data-testid]'; +const QUERY_CHECKBOX_AREA = '.empty\\:hidden div.visible'; +const QUERY_ACTION_BUTTON = '.empty\\:hidden div.visible button'; + +window.noiExport = function() { + const allNodes = Array.from(document.querySelectorAll(QUERY_CHAT_LIST)); + if (!allNodes.length) { + return { + selected: [], + all: [], + }; + } + + return { + selected: window._noiSelectedNodes, + all: allNodes.map((el) => { + const node = el.cloneNode(true); + const msgNode = node.querySelector('[data-message-author-role]'); + if (msgNode) { + msgNode.className = 'whitespace-pre-wrap break-words'; + } + return node; + }), + }; +} + +// ------------------------------ + +window.addEventListener('load', () => { + window._noiSelectedNodes = []; + const debouncedHandleMainChanges = debounce(handleMainChanges, 250); + + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + if (!mutation.target.form) { + debouncedHandleMainChanges(); + } + }); + }); + + const mainElement = document.querySelector('main'); + observer.observe(mainElement, { childList: true, subtree: true }); + + debouncedHandleMainChanges(); + + changeURL(() => { + window._noiSelectedNodes = []; + }); +}) + +function debounce(func, wait) { + let timeout; + return function(...args) { + clearTimeout(timeout); + timeout = setTimeout(() => { + func.apply(this, args); + }, wait); + }; +} + +function createButtonForNode(i, index, nodesMap) { + i.setAttribute('noi-node-id', index); + nodesMap.set(index, i); + + const btnArea = i.querySelector(QUERY_CHECKBOX_AREA); + if (!btnArea) return; + + if (btnArea.querySelector('button.noi-checkbox')) return; + + const defaultBtn = i.querySelector(QUERY_ACTION_BUTTON); + if (!defaultBtn) return; + + const btn = document.createElement('button'); + btn.classList.add(...defaultBtn.classList, 'noi-checkbox'); + btn.innerHTML = icons.noCheck; + btn.setAttribute('data-checked', 'false'); + + btn.onclick = () => { + const isChecked = btn.getAttribute('data-checked') === 'true'; + btn.innerHTML = isChecked ? icons.noCheck : icons.check; + btn.setAttribute('data-checked', `${!isChecked}`); + i.classList.toggle('noi-selected', !isChecked); + updateSelectedNodes(isChecked, i.cloneNode(true), nodesMap); + } + + btnArea.insertBefore(btn, btnArea.firstChild); +} + +function updateSelectedNodes(isChecked, node, nodesMap) { + const nodeId = node.getAttribute('noi-node-id'); + const indexInAll = nodesMap.get(Number(nodeId)); + const indexInSelected = window._noiSelectedNodes.findIndex(n => n.getAttribute('noi-node-id') === nodeId); + + if (!isChecked) { + if (indexInSelected === -1) { + let inserted = false; + for (let i = 0; i < window._noiSelectedNodes.length; i++) { + const currentId = window._noiSelectedNodes[i].getAttribute('noi-node-id'); + if (nodesMap.get(Number(currentId)) > indexInAll) { + window._noiSelectedNodes.splice(i, 0, node); + inserted = true; + break; + } + } + if (!inserted) { + const msgNode = node.querySelector('[data-message-author-role]'); + if (msgNode) { + msgNode.className = 'whitespace-pre-wrap break-words'; + } + window._noiSelectedNodes.push(node); + } + } + } else { + if (indexInSelected > -1) { + window._noiSelectedNodes.splice(indexInSelected, 1); + } + } + window._noiSelectedNodes.sort((a, b) => { + const nodeIdA = a.getAttribute('noi-node-id'); + const nodeIdB = b.getAttribute('noi-node-id'); + return Number(nodeIdA) - Number(nodeIdB); + }); + console.log(window._noiSelectedNodes); +} + +function handleMainChanges() { + const nodesMap = new Map(); + document.querySelectorAll(QUERY_CHAT_LIST) + .forEach((node, index) => createButtonForNode(node, index, nodesMap)); +} + +(function (history) { + function triggerEvent(eventName, state) { + if (typeof history[eventName] === 'function') { + history[eventName]({ state: state, url: window.location.href }); + } + } + + function overrideHistoryMethod(methodName, eventName) { + const originalMethod = history[methodName]; + history[methodName] = function (state, ...rest) { + const result = originalMethod.apply(this, [state, ...rest]); + triggerEvent(eventName, state); + return result; + }; + } + + overrideHistoryMethod('pushState', 'onpushstate'); + overrideHistoryMethod('replaceState', 'onreplacestate'); + + window.addEventListener('popstate', () => { + triggerEvent('onpopstate', null); + }); +})(window.history); + +function changeURL(callback) { + window.history.onpushstate = (event) => { + console.log('pushState called:', event.url); + callback && callback(event); + }; + window.history.onreplacestate = (event) => { + console.log('replaceState called:', event.url); + callback && callback(event); + }; + window.history.onpopstate = (event) => { + console.log('popstate event triggered', window.location.href); + callback && callback(event); + }; +} diff --git a/extensions/noi-chatgpt/manifest.json b/extensions/noi-chatgpt/manifest.json new file mode 100644 index 0000000..6a49f58 --- /dev/null +++ b/extensions/noi-chatgpt/manifest.json @@ -0,0 +1,14 @@ +{ + "manifest_version": 3, + "name": "@noi/chatgpt", + "version": "0.1.0", + "content_scripts": [ + { + "matches": ["https://*.openai.com/*"], + "js": ["main.js"], + "css": ["style.css"], + "run_at": "document_end", + "world": "MAIN" + } + ] +} \ No newline at end of file diff --git a/extensions/noi-chatgpt/style.css b/extensions/noi-chatgpt/style.css new file mode 100644 index 0000000..b5fb7f9 --- /dev/null +++ b/extensions/noi-chatgpt/style.css @@ -0,0 +1,3 @@ +.noi-selected { + border: dashed 2px rgb(34 197 94); +} \ No newline at end of file