@kudoai/chatgpt.js 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +153 -86
  3. package/chatgpt.js +91 -76
  4. package/dist/chatgpt.min.js +4 -4
  5. package/docs/README.md +153 -86
  6. package/docs/SECURITY.md +16 -18
  7. package/docs/USERGUIDE.md +19 -5
  8. package/package.json +9 -6
  9. package/starters/chrome/LICENSE.md +3 -3
  10. package/starters/chrome/docs/README.md +5 -5
  11. package/starters/chrome/docs/SECURITY.md +3 -5
  12. package/starters/chrome/extension/components/icons.js +4 -5
  13. package/starters/chrome/extension/components/modals.js +98 -65
  14. package/starters/chrome/extension/content.js +41 -47
  15. package/starters/chrome/extension/icons/faded/icon128.png +0 -0
  16. package/starters/chrome/extension/icons/faded/icon16.png +0 -0
  17. package/starters/chrome/extension/icons/faded/icon32.png +0 -0
  18. package/starters/chrome/extension/icons/faded/icon64.png +0 -0
  19. package/starters/chrome/extension/lib/chatgpt.js +91 -76
  20. package/starters/chrome/extension/lib/dom.js +70 -11
  21. package/starters/chrome/extension/lib/settings.js +8 -9
  22. package/starters/chrome/extension/manifest.json +2 -5
  23. package/starters/chrome/extension/popup/controller.js +18 -16
  24. package/starters/chrome/extension/popup/index.html +1 -1
  25. package/starters/chrome/extension/popup/style.css +26 -13
  26. package/starters/chrome/extension/service-worker.js +7 -4
  27. package/starters/chrome/images/icons/question-mark/icon16.png +0 -0
  28. package/starters/chrome/images/icons/question-mark/icon512.png +0 -0
  29. package/starters/docs/LICENSE.md +21 -1
  30. package/starters/docs/README.md +19 -6
  31. package/starters/greasemonkey/LICENSE.md +3 -3
  32. package/starters/greasemonkey/chatgpt.js-greasemonkey-starter.user.js +5 -6
  33. package/starters/greasemonkey/docs/README.md +1 -1
  34. package/starters/greasemonkey/docs/SECURITY.md +3 -5
  35. /package/starters/greasemonkey/{media → assets}/images/icons/robot/icon48.png +0 -0
  36. /package/starters/greasemonkey/{media → assets}/images/icons/robot/icon64.png +0 -0
  37. /package/starters/greasemonkey/{media → assets}/images/screenshots/chatgpt-userscript-on.png +0 -0
@@ -1,4 +1,4 @@
1
- // © 2023–2024 KudoAI & contributors under the MIT license.
1
+ // © 2023–2025 KudoAI & contributors under the MIT license.
2
2
  // Source: https://github.com/KudoAI/chatgpt.js
3
3
  // User guide: https://chatgptjs.org/userguide
4
4
  // Latest minified release: https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js/chatgpt.min.js
@@ -23,7 +23,7 @@ const chatgpt = {
23
23
  actAs(persona) {
24
24
  // Prompts ChatGPT to act as a persona from https://github.com/KudoAI/chat-prompts/blob/main/personas.json
25
25
 
26
- const promptsUrl = 'https://raw.githubusercontent.com/KudoAI/chat-prompts/main/dist/personas.min.json';
26
+ const promptsUrl = 'https://cdn.jsdelivr.net/gh/KudoAI/chat-prompts/dist/personas.min.json';
27
27
  return new Promise((resolve, reject) => {
28
28
  const xhr = new XMLHttpRequest();
29
29
  xhr.open('GET', promptsUrl, true); xhr.send();
@@ -73,7 +73,7 @@ const chatgpt = {
73
73
 
74
74
  dismiss: {
75
75
  click(event) {
76
- if (event.target == event.currentTarget || event.target.closest('[class*="-close-btn]'))
76
+ if (event.target == event.currentTarget || event.target.closest('[class*=-close-btn]'))
77
77
  dismissAlert()
78
78
  },
79
79
 
@@ -99,7 +99,8 @@ const chatgpt = {
99
99
  chatgpt.draggableElem = event.currentTarget
100
100
  chatgpt.draggableElem.style.cursor = 'grabbing'
101
101
  event.preventDefault(); // prevent sub-elems like icons being draggable
102
- ['mousemove', 'mouseup'].forEach(event => document.addEventListener(event, handlers.drag[event]))
102
+ ['mousemove', 'mouseup'].forEach(eventType =>
103
+ document.addEventListener(eventType, handlers.drag[eventType]))
103
104
  const draggableElemRect = chatgpt.draggableElem.getBoundingClientRect()
104
105
  handlers.drag.offsetX = event.clientX - draggableElemRect.left +21
105
106
  handlers.drag.offsetY = event.clientY - draggableElemRect.top +12
@@ -114,8 +115,8 @@ const chatgpt = {
114
115
 
115
116
  mouseup() { // remove listeners, reset chatgpt.draggableElem
116
117
  chatgpt.draggableElem.style.cursor = 'inherit';
117
- ['mousemove', 'mouseup'].forEach(event =>
118
- document.removeEventListener(event, handlers.drag[event]))
118
+ ['mousemove', 'mouseup'].forEach(eventType =>
119
+ document.removeEventListener(eventType, handlers.drag[eventType]))
119
120
  chatgpt.draggableElem = null
120
121
  }
121
122
  }
@@ -130,7 +131,7 @@ const chatgpt = {
130
131
  modalMessage = document.createElement('p');
131
132
 
132
133
  // Create/append/update modal style (if missing or outdated)
133
- const thisUpdated = 1734685032942; // timestamp of last edit for this file's `modalStyle`
134
+ const thisUpdated = 1735768363880 // timestamp of last edit for this file's `modalStyle`
134
135
  let modalStyle = document.querySelector('#chatgpt-modal-style'); // try to select existing style
135
136
  if (!modalStyle || parseInt(modalStyle.getAttribute('last-updated'), 10) < thisUpdated) { // if missing or outdated
136
137
  if (!modalStyle) { // outright missing, create/id/attr/append it first
@@ -139,14 +140,21 @@ const chatgpt = {
139
140
  document.head.append(modalStyle);
140
141
  }
141
142
  modalStyle.innerText = ( // update prev/new style contents
142
- '.no-mobile-tap-outline { outline: none ; -webkit-tap-highlight-color: transparent }'
143
+ '.chatgpt-modal {' // vars
144
+ + '--transition: opacity 0.65s cubic-bezier(.165,.84,.44,1),' // for fade-in
145
+ + 'transform 0.55s cubic-bezier(.165,.84,.44,1) ;' // for move-in
146
+ + '--bg-transition: background-color 0.25s ease }' // for bg dim
147
+
148
+ + '.no-mobile-tap-outline { outline: none ; -webkit-tap-highlight-color: transparent }'
143
149
 
144
150
  // Background styles
145
151
  + '.chatgpt-modal {'
146
152
  + 'pointer-events: auto ;' // override any disabling from site modals (like guest login spam)
147
153
  + 'position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ;' // expand to full view-port
148
- + 'transition: background-color 0.25s ease !important ;' // speed to show bg dim
149
- + 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 }' // align
154
+ + 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 ;' // align
155
+ + 'transition: var(--bg-transition) ;' // for bg dim
156
+ + '-webkit-transition: var(--bg-transition) ; -moz-transition: var(--bg-transition) ;'
157
+ + '-o-transition: var(--bg-transition) ; -ms-transition: var(--bg-transition) }'
150
158
 
151
159
  // Alert styles
152
160
  + '.chatgpt-modal > div {'
@@ -156,13 +164,16 @@ const chatgpt = {
156
164
  + `color: ${ scheme == 'dark' ? 'white' : 'black' };`
157
165
  + `background-color: ${ scheme == 'dark' ? 'black' : 'white' };`
158
166
  + 'transform: translateX(-3px) translateY(7px) ;' // offset to move-in from
159
- + 'transition: opacity 0.65s cubic-bezier(.165,.84,.44,1),' // for fade-ins
160
- + 'transform 0.55s cubic-bezier(.165,.84,.44,1) ;' // for move-ins
161
- + 'max-width: 75vw ; word-wrap: break-word ;'
162
- + 'padding: 20px ; margin: 12px 23px ; border-radius: 15px ; box-shadow: 0 30px 60px rgba(0, 0, 0, .12) ;'
163
- + ' -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none ; }'
167
+ + 'max-width: 75vw ; word-wrap: break-word ; border-radius: 15px ;'
168
+ + 'padding: 20px ; margin: 12px 23px ; box-shadow: 0 30px 60px rgba(0,0,0,0.12) ;'
169
+ + 'user-select: none ; -webkit-user-select: none ; -moz-user-select: none ; -o-user-select: none ;'
170
+ + '-ms-user-select: none ;'
171
+ + 'transition: var(--transition) ;' // for fade-in + move-in
172
+ + '-webkit-transition: var(--transition) ; -moz-transition: var(--transition) ;'
173
+ + '-o-transition: var(--transition) ; -ms-transition: var(--transition) }'
164
174
  + '.chatgpt-modal h2 { margin-bottom: 9px }'
165
175
  + `.chatgpt-modal a { color: ${ scheme == 'dark' ? '#00cfff' : '#1e9ebb' }}`
176
+ + '.chatgpt-modal a:hover { text-decoration: underline }'
166
177
  + '.chatgpt-modal.animated > div { z-index: 13456 ; opacity: 0.98 ; transform: translateX(0) translateY(0) }'
167
178
  + '@keyframes alert-zoom-fade-out {'
168
179
  + '0% { opacity: 1 } 50% { opacity: 0.25 ; transform: scale(1.05) }'
@@ -193,12 +204,12 @@ const chatgpt = {
193
204
  + '.chatgpt-modal .checkbox-group label {'
194
205
  + 'font-size: .7rem ; margin: -.04rem 0 0px .3rem ;'
195
206
  + `color: ${ scheme == 'dark' ? '#e1e1e1' : '#1e1e1e' }}`
196
- + '.chatgpt-modal input[type="checkbox"] { transform: scale(0.7) ;'
207
+ + '.chatgpt-modal input[type=checkbox] { transform: scale(0.7) ;'
197
208
  + `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' }}`
198
- + '.chatgpt-modal input[type="checkbox"]:checked {'
209
+ + '.chatgpt-modal input[type=checkbox]:checked {'
199
210
  + `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' } ;`
200
211
  + 'background-color: black ; position: inherit }'
201
- + '.chatgpt-modal input[type="checkbox"]:focus { outline: none ; box-shadow: none }'
212
+ + '.chatgpt-modal input[type=checkbox]:focus { outline: none ; box-shadow: none }'
202
213
  );
203
214
  }
204
215
 
@@ -281,7 +292,7 @@ const chatgpt = {
281
292
  if (alertQueue.length === 1) {
282
293
  modalContainer.style.display = '';
283
294
  setTimeout(() => { // dim bg
284
- modal.parentNode.style.backgroundColor = `rgba(67, 70, 72, ${ scheme == 'dark' ? 0.62 : 0.33 })`
295
+ modal.parentNode.style.backgroundColor = `rgba(67,70,72,${ scheme == 'dark' ? 0.62 : 0.33 })`
285
296
  modal.parentNode.classList.add('animated')
286
297
  }, 100) // delay for transition fx
287
298
  }
@@ -294,30 +305,29 @@ const chatgpt = {
294
305
 
295
306
  // Define alert dismisser
296
307
  const dismissAlert = () => {
297
- modalContainer.style.backgroundColor = 'transparent';
298
- modal.style.animation = 'alert-zoom-fade-out 0.135s ease-out';
299
- setTimeout(() => { // delay removal for fade-out
308
+ modalContainer.style.backgroundColor = 'transparent'
309
+ modal.style.animation = 'alert-zoom-fade-out 0.165s ease-out'
310
+ modal.onanimationend = () => {
300
311
 
301
312
  // Remove alert
302
- modalContainer.remove(); // ...from DOM
303
- alertQueue = JSON.parse(localStorage.alertQueue);
304
- alertQueue.shift(); // + memory
305
- localStorage.alertQueue = JSON.stringify(alertQueue); // + storage
306
- document.removeEventListener('keydown', handlers.dismiss.key); // prevent memory leaks
313
+ modalContainer.remove() // ...from DOM
314
+ alertQueue = JSON.parse(localStorage.alertQueue)
315
+ alertQueue.shift() // + memory
316
+ localStorage.alertQueue = JSON.stringify(alertQueue) // + storage
317
+ document.removeEventListener('keydown', handlers.dismiss.key) // prevent memory leaks
307
318
 
308
319
  // Check for pending alerts in queue
309
320
  if (alertQueue.length > 0) {
310
- const nextAlert = document.getElementById(alertQueue[0]);
321
+ const nextAlert = document.getElementById(alertQueue[0])
311
322
  setTimeout(() => {
312
- nextAlert.style.display = '';
313
- setTimeout(() => { nextAlert.classList.add('animated'); }, 100);
314
- }, 500);
323
+ nextAlert.style.display = ''
324
+ setTimeout(() => nextAlert.classList.add('animated'), 100)
325
+ }, 500)
315
326
  }
327
+ }
328
+ }
316
329
 
317
- }, 135);
318
- };
319
-
320
- return modalContainer.id; // if assignment used
330
+ return modalContainer.id // if assignment used
321
331
  },
322
332
 
323
333
  async askAndGetReply(query) {
@@ -455,7 +465,7 @@ const chatgpt = {
455
465
  async isIdle(timeout = null) {
456
466
  const obsConfig = { childList: true, subtree: true },
457
467
  selectors = { msgDiv: 'div[data-message-author-role]',
458
- replyDiv: 'div[data-message-author-role="assistant"]' };
468
+ replyDiv: 'div[data-message-author-role=assistant]' };
459
469
 
460
470
  // Create promises
461
471
  const timeoutPromise = timeout ? new Promise(resolve => setTimeout(() => resolve(false), timeout)) : null;
@@ -590,7 +600,7 @@ const chatgpt = {
590
600
  // Create transcript from active chat
591
601
  if (chatToGet == 'active' && /\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/.test(window.location.href)) {
592
602
  const chatDivs = document.querySelectorAll('main > div > div > div > div > div > div[class*=group]');
593
- if (chatDivs.length === 0) return console.error('Chat is empty!');
603
+ if (!chatDivs.length) return console.error('Chat is empty!');
594
604
  const msgs = []; let isUserMsg = true;
595
605
  chatDivs.forEach((div) => {
596
606
  const sender = isUserMsg ? 'USER' : 'CHATGPT'; isUserMsg = !isUserMsg;
@@ -620,7 +630,7 @@ const chatgpt = {
620
630
  filename = `${ parsedHtml.querySelector('title').textContent || 'ChatGPT conversation' }.html`;
621
631
 
622
632
  // Convert relative CSS paths to absolute ones
623
- const cssLinks = parsedHtml.querySelectorAll('link[rel="stylesheet"]');
633
+ const cssLinks = parsedHtml.querySelectorAll('link[rel=stylesheet]');
624
634
  cssLinks.forEach(link => {
625
635
  const href = link.getAttribute('href');
626
636
  if (href?.startsWith('/')) link.setAttribute('href', 'https://chat.openai.com' + href);
@@ -932,7 +942,7 @@ const chatgpt = {
932
942
  getLastResponse() { return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest'); },
933
943
 
934
944
  getNewChatButton() {
935
- return document.querySelector('button[data-testid*="new-chat-button"], button:has([d^="M15.6729"])'); },
945
+ return document.querySelector('button[data-testid*=new-chat-button], button:has([d^="M15.6729"])'); },
936
946
 
937
947
  getNewChatLink() { return document.querySelector('nav a[href="/"]'); },
938
948
  getRegenerateButton() { return document.querySelector('button:has([d^="M3.06957"])'); },
@@ -949,8 +959,8 @@ const chatgpt = {
949
959
  getResponseFromAPI(chatToGet, responseToGet) { return chatgpt.response.getFromAPI(chatToGet, responseToGet); },
950
960
  getResponseFromDOM(pos) { return chatgpt.response.getFromDOM(pos); },
951
961
  getScrollToBottomButton() { return document.querySelector('button:has([d^="M12 21C11.7348"])'); },
952
- getSendButton() { return document.querySelector('[data-testid="send-button"]'); },
953
- getStopButton() { return document.querySelector('button[data-testid="stop-button"]'); },
962
+ getSendButton() { return document.querySelector('[data-testid=send-button]'); },
963
+ getStopButton() { return document.querySelector('button[data-testid=stop-button]'); },
954
964
 
955
965
  getUserLanguage() {
956
966
  return navigator.languages[0] || navigator.language || navigator.browserLanguage ||
@@ -1104,7 +1114,7 @@ const chatgpt = {
1104
1114
  }
1105
1115
  },
1106
1116
 
1107
- isDarkMode() { return document.documentElement.classList.toString().includes('dark'); },
1117
+ isDarkMode() { return document.documentElement.className.includes('dark') },
1108
1118
  isFullScreen() { return chatgpt.browser.isFullScreen(); },
1109
1119
 
1110
1120
  async isIdle(timeout = null) {
@@ -1147,6 +1157,7 @@ const chatgpt = {
1147
1157
  },
1148
1158
 
1149
1159
  isLightMode() { return document.documentElement.classList.toString().includes('light'); },
1160
+ isTyping() { return !!this.getStopButton() },
1150
1161
 
1151
1162
  logout() { window.location.href = 'https://chat.openai.com/auth/logout'; },
1152
1163
 
@@ -1211,7 +1222,7 @@ const chatgpt = {
1211
1222
  }
1212
1223
 
1213
1224
  const addElementsToMenu = () => {
1214
- const optionButtons = document.querySelectorAll('a[role="menuitem"]');
1225
+ const optionButtons = document.querySelectorAll('a[role=menuitem]');
1215
1226
  let cssClasses;
1216
1227
 
1217
1228
  for (const navLink of optionButtons)
@@ -1230,7 +1241,7 @@ const chatgpt = {
1230
1241
  };
1231
1242
 
1232
1243
  this.elements.push(newElement);
1233
- const menuBtn = document.querySelector('nav button[id*="headless"]');
1244
+ const menuBtn = document.querySelector('nav button[id*=headless]');
1234
1245
  if (!this.addedEvent) { // to prevent adding more than one event
1235
1246
  menuBtn?.addEventListener('click', () => { setTimeout(addElementsToMenu, 25); });
1236
1247
  this.addedEvent = true; }
@@ -1239,12 +1250,12 @@ const chatgpt = {
1239
1250
  },
1240
1251
 
1241
1252
  close() {
1242
- try { document.querySelector('nav [id*="menu-button"][aria-expanded="true"]').click(); }
1253
+ try { document.querySelector('nav [id*=menu-button][aria-expanded=true]').click(); }
1243
1254
  catch (err) { console.error(err.message); }
1244
1255
  },
1245
1256
 
1246
1257
  open() {
1247
- try { document.querySelector('nav [id*="menu-button"][aria-expanded="false"]').click(); }
1258
+ try { document.querySelector('nav [id*=menu-button][aria-expanded=false]').click(); }
1248
1259
  catch (err) { console.error(err.message); }
1249
1260
  }
1250
1261
  },
@@ -1285,7 +1296,7 @@ const chatgpt = {
1285
1296
  + (notificationDiv.isRight ? 'Right' : 'Left');
1286
1297
 
1287
1298
  // Create/append/update notification style (if missing or outdated)
1288
- const thisUpdated = 20231110; // datestamp of last edit for this file's `notifStyle`
1299
+ const thisUpdated = 1735767823541 // timestamp of last edit for this file's `notifStyle`
1289
1300
  let notifStyle = document.querySelector('#chatgpt-notif-style'); // try to select existing style
1290
1301
  if (!notifStyle || parseInt(notifStyle.getAttribute('last-updated'), 10) < thisUpdated) { // if missing or outdated
1291
1302
  if (!notifStyle) { // outright missing, create/id/attr/append it first
@@ -1295,10 +1306,13 @@ const chatgpt = {
1295
1306
  }
1296
1307
  notifStyle.innerText = ( // update prev/new style contents
1297
1308
  '.chatgpt-notif {'
1309
+ + 'font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC",'
1310
+ + '"Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", sans-serif ;'
1298
1311
  + '.no-mobile-tap-outline { outline: none ; -webkit-tap-highlight-color: transparent }'
1299
1312
  + 'background-color: black ; padding: 10px 13px 10px 18px ; border-radius: 11px ; border: 1px solid #f5f5f7 ;' // bubble style
1300
1313
  + 'opacity: 0 ; position: fixed ; z-index: 9999 ; font-size: 1.8rem ; color: white ;' // visibility
1301
- + '-webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none ;'
1314
+ + 'user-select: none ; -webkit-user-select: none ; -moz-user-select: none ; -o-user-select: none ;'
1315
+ + '-ms-user-select: none ;'
1302
1316
  + `transform: translateX(${ !notificationDiv.isRight ? '-' : '' }35px) ;` // init off-screen for transition fx
1303
1317
  + ( shadow ? ( 'box-shadow: -8px 13px 25px 0 ' + ( /\b(?:shadow|on)\b/i.test(shadow) ? 'gray' : shadow )) : '' ) + '}'
1304
1318
  + '.notif-close-btn { cursor: pointer ; float: right ; position: relative ; right: -4px ; margin-left: -3px ;'
@@ -1518,7 +1532,7 @@ const chatgpt = {
1518
1532
  },
1519
1533
 
1520
1534
  getFromDOM(pos) {
1521
- const responseDivs = document.querySelectorAll('div[data-message-author-role="assistant"]'),
1535
+ const responseDivs = document.querySelectorAll('div[data-message-author-role=assistant]'),
1522
1536
  strPos = pos.toString().toLowerCase();
1523
1537
  let response = '';
1524
1538
  if (!responseDivs.length) return console.error('No conversation found!');
@@ -1810,7 +1824,7 @@ const chatgpt = {
1810
1824
 
1811
1825
 
1812
1826
  // Fix for blank background on dropdown elements
1813
- if (element == 'dropdown') newElement.style.backgroundColor = 'var(--gray-900, rgb(32, 33, 35))';
1827
+ if (element == 'dropdown') newElement.style.backgroundColor = 'var(--gray-900, rgb(32,33,35))';
1814
1828
 
1815
1829
  this.elements.push(newElement);
1816
1830
  this.activateObserver();
@@ -1825,7 +1839,7 @@ const chatgpt = {
1825
1839
  isOff() { return !this.isOn(); },
1826
1840
  isOn() {
1827
1841
  const sidebar = (() => {
1828
- return chatgpt.sidebar.exists() ? document.querySelector('[class*="sidebar"]') : null; })();
1842
+ return chatgpt.sidebar.exists() ? document.querySelector('[class*=sidebar]') : null; })();
1829
1843
  if (!sidebar) { console.error('Sidebar element not found!'); return false; }
1830
1844
  else return chatgpt.browser.isMobile() ?
1831
1845
  document.documentElement.style.overflow == 'hidden'
@@ -1833,7 +1847,7 @@ const chatgpt = {
1833
1847
  },
1834
1848
 
1835
1849
  toggle() {
1836
- const sidebarToggle = document.querySelector('button[data-testid*="sidebar-button"]');
1850
+ const sidebarToggle = document.querySelector('button[data-testid*=sidebar-button]');
1837
1851
  if (!sidebarToggle) console.error('Sidebar toggle not found!');
1838
1852
  sidebarToggle.click();
1839
1853
  },
@@ -1865,31 +1879,31 @@ const chatgpt = {
1865
1879
  return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest');
1866
1880
  },
1867
1881
 
1868
- speak(msg, options = {}) {
1869
- // Usage example: chatgpt.speak(await chatgpt.getLastResponse(), { voice: 1, pitch: 2, speed: 3 })
1870
- // options.voice = index of voices available on user device
1871
- // options.pitch = float for pitch of speech from 0 to 2
1872
- // options.speed = float for rate of speech from 0.1 to 10
1873
-
1874
- const { voice = 2, pitch = 2, speed = 1.1 } = options;
1882
+ speak(msg, { voice = 2, pitch = 2, speed = 1.1, onend } = {} ) { // eslint-disable-line no-unused-vars
1883
+ // Example call: chatgpt.speak(await chatgpt.getLastResponse(), { voice: 1, pitch: 2, speed: 3 })
1884
+ // - voice = index of voices available on user device
1885
+ // - pitch = float for pitch of speech from 0 to 2
1886
+ // - speed = float for rate of speech from 0.1 to 10
1887
+ // - onend = callback function invoked when speech finishes playing
1875
1888
 
1876
1889
  // Validate args
1877
- if (typeof msg !== 'string') return console.error('Message must be a string!');
1878
- for (let key in options) {
1879
- const value = options[key];
1880
- if (typeof value !== 'number' && !/^\d+$/.test(value))
1881
- return console.error(`Invalid ${ key } index '${ value }'. Must be a number!`);
1890
+ if (typeof msg != 'string') return console.error('Message must be a string!')
1891
+ const validOptionKeys = ['voice', 'pitch', 'speed', 'onend']
1892
+ for (const key in arguments[1]) {
1893
+ if (!validOptionKeys.includes(key))
1894
+ return console.error(`Invalid option '${key}'. Valid keys are: ${validOptionKeys}`)
1895
+ const val = arguments[1][key]
1896
+ if (key != 'onend' && typeof val != 'number' && !/^\d+$/.test(val))
1897
+ return console.error(`Invalid ${key} value '${val}'. Must be a number!`)
1898
+ else if (key == 'onend' && typeof val != 'function')
1899
+ return console.error(`Invalid ${key} value. Must be a function!`)
1882
1900
  }
1883
1901
 
1884
- try { // to speak msg using {options}
1885
- const voices = speechSynthesis.getVoices(),
1886
- utterance = new SpeechSynthesisUtterance();
1887
- utterance.text = msg;
1888
- utterance.voice = voices[voice];
1889
- utterance.pitch = pitch;
1890
- utterance.rate = speed;
1891
- speechSynthesis.speak(utterance);
1892
- } catch (err) { console.error( err); }
1902
+ try { // to speak msg
1903
+ const utterance = new SpeechSynthesisUtterance()
1904
+ Object.assign(utterance, { text: msg, ...arguments[1], voice: speechSynthesis.getVoices()[voice] })
1905
+ speechSynthesis.speak(utterance)
1906
+ } catch (err) { console.error(err) }
1893
1907
  },
1894
1908
 
1895
1909
  async summarize(text) {
@@ -1943,8 +1957,8 @@ const cjsBtnActions = ['click', 'get'], cjsTargetTypes = [ 'button', 'link', 'di
1943
1957
  for (const btnAction of cjsBtnActions) {
1944
1958
  chatgpt[btnAction + 'Button'] = function handleButton(buttonIdentifier) {
1945
1959
  const button = /^[.#]/.test(buttonIdentifier) ? document.querySelector(buttonIdentifier)
1946
- : /send/i.test(buttonIdentifier) ? document.querySelector('form button[class*="bottom"]')
1947
- : /scroll/i.test(buttonIdentifier) ? document.querySelector('button[class*="cursor"]')
1960
+ : /send/i.test(buttonIdentifier) ? document.querySelector('form button[class*=bottom]')
1961
+ : /scroll/i.test(buttonIdentifier) ? document.querySelector('button[class*=cursor]')
1948
1962
  : (function() { // get via text content
1949
1963
  for (const button of document.querySelectorAll('button')) { // try buttons
1950
1964
  if (button.textContent.toLowerCase().includes(buttonIdentifier.toLowerCase())) {
@@ -2019,8 +2033,9 @@ const cjsFuncSynonyms = [
2019
2033
  ['render', 'parse'],
2020
2034
  ['reply', 'response'],
2021
2035
  ['sentiment', 'attitude', 'emotion', 'feeling', 'opinion', 'perception'],
2022
- ['speak', 'say', 'speech', 'talk', 'tts'],
2036
+ ['speak', 'play', 'say', 'speech', 'talk', 'tts'],
2023
2037
  ['summarize', 'tldr'],
2038
+ ['typing', 'generating'],
2024
2039
  ['unminify', 'beautify', 'prettify', 'prettyPrint']
2025
2040
  ];
2026
2041
  const camelCaser = (words) => {
@@ -1,11 +1,51 @@
1
1
  window.dom = {
2
+
3
+ imports: {
4
+ import(deps) { // { config, env }
5
+ for (const depName in deps) this[depName] = deps[depName] }
6
+ },
7
+
8
+ addRisingParticles(targetNode, { lightScheme = 'gray', darkScheme = 'white' } = {}) {
9
+ // Requires https://assets.aiwebextensions.com/styles/rising-particles/dist/<lightScheme|darkScheme>.min.css
10
+
11
+ if (targetNode.querySelector('[id*=particles]')) return
12
+ const particlesDivsWrapper = document.createElement('div')
13
+ particlesDivsWrapper.style.cssText = (
14
+ 'position: absolute ; top: 0 ; left: 0 ;' // hug targetNode's top-left corner
15
+ + 'height: 100% ; width: 100% ; border-radius: 15px ; overflow: clip ;' // bound innards exactly by targetNode
16
+ + 'z-index: -1' ); // allow interactive elems to be clicked
17
+ ['sm', 'med', 'lg'].forEach(particleSize => {
18
+ const particlesDiv = document.createElement('div')
19
+ particlesDiv.id = this.imports.config?.bgAnimationsDisabled ? `particles-${particleSize}-off`
20
+ : `${( this.imports.env?.ui?.scheme || this.imports.env?.ui?.app?.scheme ) == 'dark' ? darkScheme
21
+ : lightScheme }-particles-${particleSize}`
22
+ particlesDivsWrapper.append(particlesDiv)
23
+ })
24
+ targetNode.prepend(particlesDivsWrapper)
25
+ },
26
+
2
27
  create: {
28
+ anchor(linkHref, displayContent, attrs = {}) {
29
+ const anchor = document.createElement('a'),
30
+ defaultAttrs = { href: linkHref, target: '_blank', rel: 'noopener' },
31
+ finalAttrs = { ...defaultAttrs, ...attrs }
32
+ Object.entries(finalAttrs).forEach(([attr, value]) => anchor.setAttribute(attr, value))
33
+ if (displayContent) anchor.append(displayContent)
34
+ return anchor
35
+ },
36
+
3
37
  elem(elemType, attrs = {}) {
4
38
  const elem = document.createElement(elemType)
5
39
  for (const attr in attrs) elem.setAttribute(attr, attrs[attr])
6
40
  return elem
7
41
  },
8
42
 
43
+ style(content) {
44
+ const style = document.createElement('style')
45
+ if (content) style.innerText = content
46
+ return style
47
+ },
48
+
9
49
  svgElem(type, attrs) {
10
50
  const elem = document.createElementNS('http://www.w3.org/2000/svg', type)
11
51
  for (const attr in attrs) elem.setAttributeNS(null, attr, attrs[attr])
@@ -13,16 +53,35 @@ window.dom = {
13
53
  }
14
54
  },
15
55
 
16
- fillStarryBG(targetNode) { // requires https://assets.aiwebextensions.com/styles/css/<black|white>-rising-stars.min.css
17
- const starsDivsContainer = document.createElement('div')
18
- starsDivsContainer.style.cssText = 'position: absolute ; top: 0 ; left: 0 ;' // hug targetNode's top-left corner
19
- + 'height: 100% ; width: 100% ; border-radius: 15px ; overflow: clip ;' // bound innards exactly by targetNode
20
- + 'z-index: -1'; // allow interactive elems to be clicked
21
- ['sm', 'med', 'lg'].forEach(starSize => {
22
- const starsDiv = document.createElement('div')
23
- starsDiv.id = `${ chatgpt.isDarkMode() ? 'white' : 'black' }-stars-${starSize}`
24
- starsDivsContainer.append(starsDiv)
25
- })
26
- targetNode.prepend(starsDivsContainer)
56
+ cssSelectorize(classList) {
57
+ return classList.toString()
58
+ .replace(/([:[\]\\])/g, '\\$1') // escape special chars :[]\
59
+ .replace(/^| /g, '.') // prefix w/ dot, convert spaces to dots
60
+ },
61
+
62
+ get: {
63
+ computedWidth(...elems) { // including margins
64
+ let totalWidth = 0
65
+ elems.map(arg => arg instanceof NodeList ? [...arg] : arg).flat().forEach(elem => {
66
+ if (!(elem instanceof Element)) return
67
+ const elemStyle = getComputedStyle(elem) ; if (elemStyle.display == 'none') return
68
+ totalWidth += elem.getBoundingClientRect().width + parseFloat(elemStyle.marginLeft)
69
+ + parseFloat(elemStyle.marginRight)
70
+ })
71
+ return totalWidth
72
+ },
73
+
74
+ loadedElem(selector, timeout = null) {
75
+ const timeoutPromise = timeout ? new Promise(resolve => setTimeout(() => resolve(null), timeout)) : null
76
+ const isLoadedPromise = new Promise(resolve => {
77
+ const elem = document.querySelector(selector)
78
+ if (elem) resolve(elem)
79
+ else new MutationObserver((_, obs) => {
80
+ const elem = document.querySelector(selector)
81
+ if (elem) { obs.disconnect() ; resolve(elem) }
82
+ }).observe(document.documentElement, { childList: true, subtree: true })
83
+ })
84
+ return ( timeoutPromise ? Promise.race([isLoadedPromise, timeoutPromise]) : isLoadedPromise )
85
+ }
27
86
  }
28
87
  };
@@ -6,6 +6,7 @@ window.settings = {
6
6
  // Add settings options as keys, with each key's value being an object that includes:
7
7
  // - 'type': the control type (e.g. 'toggle' or 'prompt')
8
8
  // - 'label': a descriptive label
9
+ // - 'defaultVal' (optional): default value of setting (true for toggles if unspecified, false otherwise)
9
10
  // - 'symbol' (optional): for icon display (e.g. ⌚)
10
11
  // NOTE: Toggles are disabled by default unless key name contains 'disabled' or 'hidden' (case insensitive)
11
12
  // NOTE: Controls are displayed in top-to-bottom order
@@ -14,17 +15,15 @@ window.settings = {
14
15
  // replyLanguage: { type: 'prompt', symbol: '🌐', label: 'Reply Language' }
15
16
  },
16
17
 
17
- load() {
18
- const keys = ( // original array if array, else new array from multiple args
19
- Array.isArray(arguments[0]) ? arguments[0] : Array.from(arguments))
20
- return Promise.all(keys.map(key => // resolve promise when all keys load
21
- new Promise(resolve => // resolve promise when single key value loads
22
- chrome.storage.sync.get(key, result => { // load from Chrome extension storage
23
- window.config[key] = result[key] || false ; resolve()
24
- }))))},
18
+ load(...keys) {
19
+ return Promise.all(keys.flat().map(async key => // resolve promise when all keys load
20
+ window.config[key] = (await chrome.storage.sync.get(key))[key]
21
+ ?? this.controls[key]?.defaultVal ?? this.controls[key]?.type == 'toggle'
22
+ ))
23
+ },
25
24
 
26
25
  save(key, val) {
27
26
  chrome.storage.sync.set({ [key]: val }) // save to Chrome extension storage
28
27
  window.config[key] = val // save to memory
29
28
  }
30
- }
29
+ };
@@ -3,7 +3,7 @@
3
3
  "name": "ChatGPT Extension",
4
4
  "short_name": "ChatGPT 🧩",
5
5
  "description": "A Chromium extension template to start using chatgpt.js like a boss!",
6
- "version": "2024.12.20",
6
+ "version": "2025.2.1",
7
7
  "author": "KudoAI",
8
8
  "homepage_url": "https://github.com/KudoAI/chatgpt.js-chrome-starter",
9
9
  "icons": {
@@ -18,10 +18,7 @@
18
18
  "matches": [ "<all_urls>" ],
19
19
  "resources": [ "components/modals.js", "lib/chatgpt.js", "lib/dom.js", "lib/settings.js" ]
20
20
  }],
21
- "content_scripts": [{
22
- "matches": [ "https://chatgpt.com/*" ],
23
- "js": [ "content.js" ]
24
- }],
21
+ "content_scripts": [{ "matches": [ "https://chatgpt.com/*" ], "js": [ "content.js" ] }],
25
22
  "background": { "service_worker": "service-worker.js" },
26
23
  "minimum_chrome_version": "88"
27
24
  }