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