@kudoai/chatgpt.js 3.5.0 → 3.6.1

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 +147 -88
  3. package/chatgpt.js +127 -103
  4. package/dist/chatgpt.min.js +28 -4
  5. package/docs/README.md +154 -88
  6. package/docs/SECURITY.md +15 -15
  7. package/docs/USERGUIDE.md +19 -5
  8. package/package.json +10 -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 +2 -5
  13. package/starters/chrome/extension/components/modals.js +90 -87
  14. package/starters/chrome/extension/content.js +18 -11
  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 +127 -103
  20. package/starters/chrome/extension/lib/dom.js +90 -16
  21. package/starters/chrome/extension/lib/settings.js +7 -8
  22. package/starters/chrome/extension/manifest.json +1 -1
  23. package/starters/chrome/extension/popup/controller.js +7 -7
  24. package/starters/chrome/extension/popup/index.html +1 -1
  25. package/starters/chrome/extension/popup/style.css +3 -4
  26. package/starters/chrome/extension/service-worker.js +1 -1
  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
 
@@ -97,7 +97,8 @@ const chatgpt = {
97
97
  if (event.button != 0) return // prevent non-left-click drag
98
98
  if (getComputedStyle(event.target).cursor == 'pointer') return // prevent drag on interactive elems
99
99
  chatgpt.draggableElem = event.currentTarget
100
- chatgpt.draggableElem.style.cursor = 'grabbing'
100
+ Object.assign(chatgpt.draggableElem.style, {
101
+ cursor: 'grabbing', transition: '0.1s', willChange: 'transform', transform: 'scale(1.05)' })
101
102
  event.preventDefault(); // prevent sub-elems like icons being draggable
102
103
  ['mousemove', 'mouseup'].forEach(eventType =>
103
104
  document.addEventListener(eventType, handlers.drag[eventType]))
@@ -114,7 +115,8 @@ const chatgpt = {
114
115
  },
115
116
 
116
117
  mouseup() { // remove listeners, reset chatgpt.draggableElem
117
- chatgpt.draggableElem.style.cursor = 'inherit';
118
+ Object.assign(chatgpt.draggableElem.style, {
119
+ cursor: 'inherit', transition: 'inherit', willChange: 'auto', transform: 'scale(1)' });
118
120
  ['mousemove', 'mouseup'].forEach(eventType =>
119
121
  document.removeEventListener(eventType, handlers.drag[eventType]))
120
122
  chatgpt.draggableElem = null
@@ -131,7 +133,7 @@ const chatgpt = {
131
133
  modalMessage = document.createElement('p');
132
134
 
133
135
  // Create/append/update modal style (if missing or outdated)
134
- const thisUpdated = 1735475757891 // timestamp of last edit for this file's `modalStyle`
136
+ const thisUpdated = 1735768363880 // timestamp of last edit for this file's `modalStyle`
135
137
  let modalStyle = document.querySelector('#chatgpt-modal-style'); // try to select existing style
136
138
  if (!modalStyle || parseInt(modalStyle.getAttribute('last-updated'), 10) < thisUpdated) { // if missing or outdated
137
139
  if (!modalStyle) { // outright missing, create/id/attr/append it first
@@ -140,14 +142,21 @@ const chatgpt = {
140
142
  document.head.append(modalStyle);
141
143
  }
142
144
  modalStyle.innerText = ( // update prev/new style contents
143
- '.no-mobile-tap-outline { outline: none ; -webkit-tap-highlight-color: transparent }'
145
+ '.chatgpt-modal {' // vars
146
+ + '--transition: opacity 0.65s cubic-bezier(.165,.84,.44,1),' // for fade-in
147
+ + 'transform 0.55s cubic-bezier(.165,.84,.44,1) ;' // for move-in
148
+ + '--bg-transition: background-color 0.25s ease }' // for bg dim
149
+
150
+ + '.no-mobile-tap-outline { outline: none ; -webkit-tap-highlight-color: transparent }'
144
151
 
145
152
  // Background styles
146
153
  + '.chatgpt-modal {'
147
154
  + 'pointer-events: auto ;' // override any disabling from site modals (like guest login spam)
148
155
  + 'position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ;' // expand to full view-port
149
- + 'transition: background-color 0.25s ease !important ;' // speed to show bg dim
150
- + 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 }' // align
156
+ + 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 ;' // align
157
+ + 'transition: var(--bg-transition) ;' // for bg dim
158
+ + '-webkit-transition: var(--bg-transition) ; -moz-transition: var(--bg-transition) ;'
159
+ + '-o-transition: var(--bg-transition) ; -ms-transition: var(--bg-transition) }'
151
160
 
152
161
  // Alert styles
153
162
  + '.chatgpt-modal > div {'
@@ -157,18 +166,23 @@ const chatgpt = {
157
166
  + `color: ${ scheme == 'dark' ? 'white' : 'black' };`
158
167
  + `background-color: ${ scheme == 'dark' ? 'black' : 'white' };`
159
168
  + 'transform: translateX(-3px) translateY(7px) ;' // offset to move-in from
160
- + 'transition: opacity 0.65s cubic-bezier(.165,.84,.44,1),' // for fade-ins
161
- + 'transform 0.55s cubic-bezier(.165,.84,.44,1) ;' // for move-ins
162
- + 'max-width: 75vw ; word-wrap: break-word ;'
163
- + 'padding: 20px ; margin: 12px 23px ; border-radius: 15px ; box-shadow: 0 30px 60px rgba(0, 0, 0, .12) ;'
164
- + ' -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none ; }'
165
- + '.chatgpt-modal h2 { margin-bottom: 9px }'
166
- + `.chatgpt-modal a { color: ${ scheme == 'dark' ? '#00cfff' : '#1e9ebb' }}`
167
- + '.chatgpt-modal a:hover { text-decoration: underline }'
168
- + '.chatgpt-modal.animated > div { z-index: 13456 ; opacity: 0.98 ; transform: translateX(0) translateY(0) }'
169
- + '@keyframes alert-zoom-fade-out {'
170
- + '0% { opacity: 1 } 50% { opacity: 0.25 ; transform: scale(1.05) }'
171
- + '100% { opacity: 0 ; transform: scale(1.35) }}'
169
+ + 'max-width: 75vw ; word-wrap: break-word ; border-radius: 15px ;'
170
+ + 'padding: 20px ; margin: 12px 23px ;'
171
+ + `--shadow: 0 30px 60px rgba(0,0,0,0.12) ; box-shadow: var(--shadow) ;
172
+ -webkit-box-shadow: var(--shadow) ; -moz-box-shadow: var(--shadow) ;`
173
+ + 'user-select: none ; -webkit-user-select: none ; -moz-user-select: none ; -o-user-select: none ;'
174
+ + '-ms-user-select: none ;'
175
+ + 'transition: var(--transition) ;' // for fade-in + move-in
176
+ + '-webkit-transition: var(--transition) ; -moz-transition: var(--transition) ;'
177
+ + '-o-transition: var(--transition) ; -ms-transition: var(--transition) }'
178
+ + `.chatgpt-modal h2 { margin-bottom: 9px }
179
+ .chatgpt-modal a { color: ${ scheme == 'dark' ? '#00cfff' : '#1e9ebb' }}
180
+ .chatgpt-modal a:hover { text-decoration: underline }
181
+ .chatgpt-modal.animated > div {
182
+ z-index: 13456 ; opacity: 0.98 ; transform: translateX(0) translateY(0) }
183
+ @keyframes alert-zoom-fade-out {
184
+ 0% { opacity: 1 } 50% { opacity: 0.25 ; transform: scale(1.05) }
185
+ 100% { opacity: 0 ; transform: scale(1.35) }}`
172
186
 
173
187
  // Button styles
174
188
  + '.modal-buttons { display: flex ; justify-content: flex-end ; margin: 20px -5px -3px 0 ;'
@@ -181,9 +195,11 @@ const chatgpt = {
181
195
  + `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' } ;`
182
196
  + `background: ${ scheme == 'dark' ? 'white' : 'black' } ;`
183
197
  + `color: ${ scheme == 'dark' ? 'black' : 'white' }}`
184
- + '.chatgpt-modal button:hover { color: #3d5d71 ; border-color: #6d9cb9 ;'
185
- + 'background-color: ' + ( scheme == 'dark' ? '#00cfff' : '#9cdaff' ) + ';'
186
- + 'box-shadow: 2px 1px ' + ( scheme == 'dark' ? '54px #00cfff' : '30px #9cdaff' ) + '}'
198
+ + `.chatgpt-modal button:hover {
199
+ color: #3d5d71 ; border-color: #6d9cb9 ;
200
+ background-color: ${ scheme == 'dark' ? '#00cfff' : '#9cdaff' };
201
+ --shadow: 2px 1px ${ scheme == 'dark' ? '54px #00cfff' : '30px #9cdaff' };
202
+ box-shadow: var(--shadow) ; box-shadow: var(--shadow) ; box-shadow: var(--shadow) }`
187
203
  + '.modal-close-btn {'
188
204
  + 'cursor: pointer ; width: 29px ; height: 29px ; border-radius: 17px ;'
189
205
  + 'float: right ; position: relative ; right: -6px ; top: -5px }'
@@ -191,17 +207,18 @@ const chatgpt = {
191
207
  + `.modal-close-btn:hover { background-color: #f2f2f2${ scheme == 'dark' ? '00' : '' }}`
192
208
 
193
209
  // Checkbox styles
194
- + '.chatgpt-modal .checkbox-group { display: flex ; margin-top: -18px }'
195
- + '.chatgpt-modal .checkbox-group label {'
196
- + 'font-size: .7rem ; margin: -.04rem 0 0px .3rem ;'
197
- + `color: ${ scheme == 'dark' ? '#e1e1e1' : '#1e1e1e' }}`
198
- + '.chatgpt-modal input[type=checkbox] { transform: scale(0.7) ;'
199
- + `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' }}`
200
- + '.chatgpt-modal input[type=checkbox]:checked {'
201
- + `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' } ;`
202
- + 'background-color: black ; position: inherit }'
203
- + '.chatgpt-modal input[type=checkbox]:focus { outline: none ; box-shadow: none }'
204
- );
210
+ + `.chatgpt-modal .checkbox-group { margin-top: 15px }
211
+ .chatgpt-modal .checkbox-group label {
212
+ font-size: .7rem ; margin: -.04rem 0 0px .3rem
213
+ color: ${ scheme == 'dark' ? '#e1e1e1' : '#1e1e1e' }}
214
+ .chatgpt-modal input[type=checkbox] { transform: scale(0.7) ;
215
+ border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' }}
216
+ .chatgpt-modal input[type=checkbox]:checked {
217
+ border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' } ;
218
+ background-color: black ; position: inherit }
219
+ .chatgpt-modal input[type=checkbox]:focus {
220
+ outline: none ; box-shadow: none ; -webkit-box-shadow: none ; -moz-box-shadow: none }`
221
+ )
205
222
  }
206
223
 
207
224
  // Insert text into elements
@@ -268,7 +285,7 @@ const chatgpt = {
268
285
  closeSVG.append(closeSVGpath); closeBtn.append(closeSVG);
269
286
 
270
287
  // Assemble/append div
271
- const modalElems = [closeBtn, modalTitle, modalMessage, modalButtons, checkboxDiv];
288
+ const modalElems = [closeBtn, modalTitle, modalMessage, checkboxDiv, modalButtons ];
272
289
  modalElems.forEach((elem) => { modal.append(elem); });
273
290
  modal.style.width = `${ width || 458 }px`;
274
291
  modalContainer.append(modal); document.body.append(modalContainer);
@@ -283,7 +300,7 @@ const chatgpt = {
283
300
  if (alertQueue.length === 1) {
284
301
  modalContainer.style.display = '';
285
302
  setTimeout(() => { // dim bg
286
- modal.parentNode.style.backgroundColor = `rgba(67, 70, 72, ${ scheme == 'dark' ? 0.62 : 0.33 })`
303
+ modal.parentNode.style.backgroundColor = `rgba(67,70,72,${ scheme == 'dark' ? 0.62 : 0.33 })`
287
304
  modal.parentNode.classList.add('animated')
288
305
  }, 100) // delay for transition fx
289
306
  }
@@ -296,30 +313,29 @@ const chatgpt = {
296
313
 
297
314
  // Define alert dismisser
298
315
  const dismissAlert = () => {
299
- modalContainer.style.backgroundColor = 'transparent';
300
- modal.style.animation = 'alert-zoom-fade-out 0.135s ease-out';
301
- setTimeout(() => { // delay removal for fade-out
316
+ modalContainer.style.backgroundColor = 'transparent'
317
+ modal.style.animation = 'alert-zoom-fade-out 0.165s ease-out'
318
+ modal.onanimationend = () => {
302
319
 
303
320
  // Remove alert
304
- modalContainer.remove(); // ...from DOM
305
- alertQueue = JSON.parse(localStorage.alertQueue);
306
- alertQueue.shift(); // + memory
307
- localStorage.alertQueue = JSON.stringify(alertQueue); // + storage
308
- document.removeEventListener('keydown', handlers.dismiss.key); // prevent memory leaks
321
+ modalContainer.remove() // ...from DOM
322
+ alertQueue = JSON.parse(localStorage.alertQueue)
323
+ alertQueue.shift() // + memory
324
+ localStorage.alertQueue = JSON.stringify(alertQueue) // + storage
325
+ document.removeEventListener('keydown', handlers.dismiss.key) // prevent memory leaks
309
326
 
310
327
  // Check for pending alerts in queue
311
328
  if (alertQueue.length > 0) {
312
- const nextAlert = document.getElementById(alertQueue[0]);
329
+ const nextAlert = document.getElementById(alertQueue[0])
313
330
  setTimeout(() => {
314
- nextAlert.style.display = '';
315
- setTimeout(() => { nextAlert.classList.add('animated'); }, 100);
316
- }, 500);
331
+ nextAlert.style.display = ''
332
+ setTimeout(() => nextAlert.classList.add('animated'), 100)
333
+ }, 500)
317
334
  }
335
+ }
336
+ }
318
337
 
319
- }, 155);
320
- };
321
-
322
- return modalContainer.id; // if assignment used
338
+ return modalContainer.id // if assignment used
323
339
  },
324
340
 
325
341
  async askAndGetReply(query) {
@@ -592,7 +608,7 @@ const chatgpt = {
592
608
  // Create transcript from active chat
593
609
  if (chatToGet == 'active' && /\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/.test(window.location.href)) {
594
610
  const chatDivs = document.querySelectorAll('main > div > div > div > div > div > div[class*=group]');
595
- if (chatDivs.length === 0) return console.error('Chat is empty!');
611
+ if (!chatDivs.length) return console.error('Chat is empty!');
596
612
  const msgs = []; let isUserMsg = true;
597
613
  chatDivs.forEach((div) => {
598
614
  const sender = isUserMsg ? 'USER' : 'CHATGPT'; isUserMsg = !isUserMsg;
@@ -970,14 +986,14 @@ const chatgpt = {
970
986
 
971
987
  history: {
972
988
  async isLoaded(timeout = null) {
973
- const timeoutPromise = timeout ? new Promise(resolve => setTimeout(() => resolve(false), timeout)) : null;
989
+ const timeoutPromise = timeout ? new Promise(resolve => setTimeout(() => resolve(false), timeout)) : null
974
990
  const isLoadedPromise = new Promise(resolve => {
975
- if (document.querySelector('nav')) resolve(true);
991
+ if (document.querySelector('nav')) resolve(true)
976
992
  else new MutationObserver((_, obs) => {
977
- if (document.querySelector('nav')) { obs.disconnect(); resolve(true); }
978
- }).observe(document.body, { childList: true, subtree: true });
979
- });
980
- return await ( timeoutPromise ? Promise.race([isLoadedPromise, timeoutPromise]) : isLoadedPromise );
993
+ if (document.querySelector('nav')) { obs.disconnect() ; resolve(true) }
994
+ }).observe(document.documentElement, { childList: true, subtree: true })
995
+ })
996
+ return await ( timeoutPromise ? Promise.race([isLoadedPromise, timeoutPromise]) : isLoadedPromise )
981
997
  }
982
998
  },
983
999
 
@@ -1106,7 +1122,7 @@ const chatgpt = {
1106
1122
  }
1107
1123
  },
1108
1124
 
1109
- isDarkMode() { return document.documentElement.classList.toString().includes('dark'); },
1125
+ isDarkMode() { return document.documentElement.className.includes('dark') },
1110
1126
  isFullScreen() { return chatgpt.browser.isFullScreen(); },
1111
1127
 
1112
1128
  async isIdle(timeout = null) {
@@ -1138,17 +1154,18 @@ const chatgpt = {
1138
1154
  },
1139
1155
 
1140
1156
  async isLoaded(timeout = null) {
1141
- const timeoutPromise = timeout ? new Promise(resolve => setTimeout(() => resolve(false), timeout)) : null;
1157
+ const timeoutPromise = timeout ? new Promise(resolve => setTimeout(() => resolve(false), timeout)) : null
1142
1158
  const isLoadedPromise = new Promise(resolve => {
1143
- if (chatgpt.getNewChatBtn()) resolve(true);
1159
+ if (chatgpt.getNewChatBtn()) resolve(true)
1144
1160
  else new MutationObserver((_, obs) => {
1145
- if (chatgpt.getNewChatBtn()) { obs.disconnect(); resolve(true); }
1146
- }).observe(document.body, { childList: true, subtree: true });
1147
- });
1148
- return await ( timeoutPromise ? Promise.race([isLoadedPromise, timeoutPromise]) : isLoadedPromise );
1161
+ if (chatgpt.getNewChatBtn()) { obs.disconnect() ; resolve(true) }
1162
+ }).observe(document.documentElement, { childList: true, subtree: true })
1163
+ })
1164
+ return await ( timeoutPromise ? Promise.race([isLoadedPromise, timeoutPromise]) : isLoadedPromise )
1149
1165
  },
1150
1166
 
1151
1167
  isLightMode() { return document.documentElement.classList.toString().includes('light'); },
1168
+ isTyping() { return !!this.getStopButton() },
1152
1169
 
1153
1170
  logout() { window.location.href = 'https://chat.openai.com/auth/logout'; },
1154
1171
 
@@ -1287,7 +1304,7 @@ const chatgpt = {
1287
1304
  + (notificationDiv.isRight ? 'Right' : 'Left');
1288
1305
 
1289
1306
  // Create/append/update notification style (if missing or outdated)
1290
- const thisUpdated = 1735475527153 // timestamp of last edit for this file's `notifStyle`
1307
+ const thisUpdated = 1735767823541 // timestamp of last edit for this file's `notifStyle`
1291
1308
  let notifStyle = document.querySelector('#chatgpt-notif-style'); // try to select existing style
1292
1309
  if (!notifStyle || parseInt(notifStyle.getAttribute('last-updated'), 10) < thisUpdated) { // if missing or outdated
1293
1310
  if (!notifStyle) { // outright missing, create/id/attr/append it first
@@ -1300,12 +1317,18 @@ const chatgpt = {
1300
1317
  + 'font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC",'
1301
1318
  + '"Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", sans-serif ;'
1302
1319
  + '.no-mobile-tap-outline { outline: none ; -webkit-tap-highlight-color: transparent }'
1303
- + 'background-color: black ; padding: 10px 13px 10px 18px ; border-radius: 11px ; border: 1px solid #f5f5f7 ;' // bubble style
1320
+ + 'background-color: black ; padding: 10px 13px 10px 18px ;' // bubble style
1321
+ + 'border-radius: 11px ; border: 1px solid #f5f5f7 ;'
1304
1322
  + 'opacity: 0 ; position: fixed ; z-index: 9999 ; font-size: 1.8rem ; color: white ;' // visibility
1305
- + '-webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none ;'
1306
- + `transform: translateX(${ !notificationDiv.isRight ? '-' : '' }35px) ;` // init off-screen for transition fx
1307
- + ( shadow ? ( 'box-shadow: -8px 13px 25px 0 ' + ( /\b(?:shadow|on)\b/i.test(shadow) ? 'gray' : shadow )) : '' ) + '}'
1308
- + '.notif-close-btn { cursor: pointer ; float: right ; position: relative ; right: -4px ; margin-left: -3px ;'
1323
+ + 'user-select: none ; -webkit-user-select: none ; -moz-user-select: none ; -o-user-select: none ;'
1324
+ + '-ms-user-select: none ;'
1325
+ + `transform: translateX(${ // init off-screen for transition fx
1326
+ !notificationDiv.isRight ? '-' : '' }35px) ;`
1327
+ + ( shadow ? `--shadow: -8px 13px 25px 0 ${ /\b(?:shadow|on)\b/i.test(shadow) ? 'gray' : shadow };
1328
+ box-shadow: var(--shadow) ; -webkit-box-shadow: var(--shadow) ; -moz-box-shadow: var(--shadow)`
1329
+ : '' ) + '}'
1330
+ + `.notif-close-btn {
1331
+ cursor: pointer ; float: right ; position: relative ; right: -4px ; margin-left: -3px ;`
1309
1332
  + 'display: grid }' // top-align for non-OpenAI sites
1310
1333
  + '@keyframes notif-zoom-fade-out { 0% { opacity: 1 ; transform: scale(1) }' // transition out keyframes
1311
1334
  + '15% { opacity: 0.35 ; transform: rotateX(-27deg) scale(1.05) }'
@@ -1814,7 +1837,7 @@ const chatgpt = {
1814
1837
 
1815
1838
 
1816
1839
  // Fix for blank background on dropdown elements
1817
- if (element == 'dropdown') newElement.style.backgroundColor = 'var(--gray-900, rgb(32, 33, 35))';
1840
+ if (element == 'dropdown') newElement.style.backgroundColor = 'var(--gray-900, rgb(32,33,35))';
1818
1841
 
1819
1842
  this.elements.push(newElement);
1820
1843
  this.activateObserver();
@@ -1843,15 +1866,15 @@ const chatgpt = {
1843
1866
  },
1844
1867
 
1845
1868
  async isLoaded(timeout = 5000) {
1846
- await chatgpt.isLoaded();
1847
- const timeoutPromise = new Promise(resolve => setTimeout(() => { resolve(false); }, timeout));
1869
+ await chatgpt.isLoaded()
1870
+ const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(false), timeout))
1848
1871
  const isLoadedPromise = new Promise(resolve => {
1849
- if (chatgpt.getNewChatLink()) resolve(true);
1872
+ if (chatgpt.getNewChatLink()) resolve(true)
1850
1873
  else new MutationObserver((_, obs) => {
1851
- if (chatgpt.getNewChatLink()) { obs.disconnect(); resolve(true); }
1852
- }).observe(document.body, { childList: true, subtree: true });
1853
- });
1854
- return await Promise.race([isLoadedPromise, timeoutPromise]);
1874
+ if (chatgpt.getNewChatLink()) { obs.disconnect() ; resolve(true) }
1875
+ }).observe(document.documentElement, { childList: true, subtree: true })
1876
+ })
1877
+ return await Promise.race([isLoadedPromise, timeoutPromise])
1855
1878
  }
1856
1879
  },
1857
1880
 
@@ -1869,31 +1892,31 @@ const chatgpt = {
1869
1892
  return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest');
1870
1893
  },
1871
1894
 
1872
- speak(msg, options = {}) {
1873
- // Usage example: chatgpt.speak(await chatgpt.getLastResponse(), { voice: 1, pitch: 2, speed: 3 })
1874
- // options.voice = index of voices available on user device
1875
- // options.pitch = float for pitch of speech from 0 to 2
1876
- // options.speed = float for rate of speech from 0.1 to 10
1877
-
1878
- const { voice = 2, pitch = 2, speed = 1.1 } = options;
1895
+ speak(msg, { voice = 2, pitch = 2, speed = 1.1, onend } = {} ) {
1896
+ // Example call: chatgpt.speak(await chatgpt.getLastResponse(), { voice: 1, pitch: 2, speed: 3 })
1897
+ // - voice = index of voices available on user device
1898
+ // - pitch = float for pitch of speech from 0 to 2
1899
+ // - speed = float for rate of speech from 0.1 to 10
1900
+ // - onend = callback function invoked when speech finishes playing
1879
1901
 
1880
1902
  // Validate args
1881
- if (typeof msg !== 'string') return console.error('Message must be a string!');
1882
- for (let key in options) {
1883
- const value = options[key];
1884
- if (typeof value !== 'number' && !/^\d+$/.test(value))
1885
- return console.error(`Invalid ${ key } index '${ value }'. Must be a number!`);
1903
+ if (typeof msg != 'string') return console.error('Message must be a string!')
1904
+ const validOptionKeys = ['voice', 'pitch', 'speed', 'onend']
1905
+ for (const key in arguments[1]) {
1906
+ if (!validOptionKeys.includes(key))
1907
+ return console.error(`Invalid option '${key}'. Valid keys are: ${validOptionKeys}`)
1908
+ const val = arguments[1][key]
1909
+ if (key != 'onend' && typeof val != 'number' && !/^\d+$/.test(val))
1910
+ return console.error(`Invalid ${key} value '${val}'. Must be a number!`)
1911
+ else if (key == 'onend' && typeof val != 'function')
1912
+ return console.error(`Invalid ${key} value. Must be a function!`)
1886
1913
  }
1887
1914
 
1888
- try { // to speak msg using {options}
1889
- const voices = speechSynthesis.getVoices(),
1890
- utterance = new SpeechSynthesisUtterance();
1891
- utterance.text = msg;
1892
- utterance.voice = voices[voice];
1893
- utterance.pitch = pitch;
1894
- utterance.rate = speed;
1895
- speechSynthesis.speak(utterance);
1896
- } catch (err) { console.error( err); }
1915
+ try { // to speak msg
1916
+ const utterance = new SpeechSynthesisUtterance(), voices = speechSynthesis.getVoices()
1917
+ Object.assign(utterance, { text: msg, voice: voices[voice], pitch: pitch, speed: speed, onend: onend })
1918
+ speechSynthesis.speak(utterance)
1919
+ } catch (err) { console.error(err) }
1897
1920
  },
1898
1921
 
1899
1922
  async summarize(text) {
@@ -2023,8 +2046,9 @@ const cjsFuncSynonyms = [
2023
2046
  ['render', 'parse'],
2024
2047
  ['reply', 'response'],
2025
2048
  ['sentiment', 'attitude', 'emotion', 'feeling', 'opinion', 'perception'],
2026
- ['speak', 'say', 'speech', 'talk', 'tts'],
2049
+ ['speak', 'play', 'say', 'speech', 'talk', 'tts'],
2027
2050
  ['summarize', 'tldr'],
2051
+ ['typing', 'generating'],
2028
2052
  ['unminify', 'beautify', 'prettify', 'prettyPrint']
2029
2053
  ];
2030
2054
  const camelCaser = (words) => {
@@ -1,35 +1,109 @@
1
+ // Copyright © 2023–2025 Adam Lui (https://github.com/adamlui) under the MIT license
2
+ // Source: https://github.com/adamlui/ai-web-extensions/blob/main/assets/lib/dom.js/src/dom.js
3
+
1
4
  window.dom = {
5
+ import(deps) { Object.assign(this.imports = this.imports || {}, deps) },
6
+
7
+ addRisingParticles(targetNode, { lightScheme = 'gray', darkScheme = 'white' } = {}) {
8
+ // * Requires https://assets.aiwebextensions.com/styles/rising-particles/dist/<lightScheme>.min.css
2
9
 
3
- imports: {
4
- import(deps) { // { env) }
5
- for (const depName in deps) this[depName] = deps[depName] }
10
+ if (targetNode.querySelector('[id*=particles]')) return
11
+ const particlesDivsWrapper = document.createElement('div')
12
+ particlesDivsWrapper.style.cssText = (
13
+ 'position: absolute ; top: 0 ; left: 0 ;' // hug targetNode's top-left corner
14
+ + 'height: 100% ; width: 100% ; border-radius: 15px ; overflow: clip ;' // bound innards exactly by targetNode
15
+ + 'z-index: -1' ); // allow interactive elems to be clicked
16
+ ['sm', 'med', 'lg'].forEach(particleSize => {
17
+ const particlesDiv = document.createElement('div')
18
+ particlesDiv.id = this.imports.config?.bgAnimationsDisabled ? `particles-${particleSize}-off`
19
+ : `${( this.imports.env?.ui?.scheme || this.imports.env?.ui?.app?.scheme ) == 'dark' ? darkScheme
20
+ : lightScheme }-particles-${particleSize}`
21
+ particlesDivsWrapper.append(particlesDiv)
22
+ })
23
+ targetNode.prepend(particlesDivsWrapper)
6
24
  },
7
25
 
8
26
  create: {
27
+ anchor(linkHref, displayContent, attrs = {}) {
28
+ const anchor = document.createElement('a'),
29
+ defaultAttrs = { href: linkHref, target: '_blank', rel: 'noopener' },
30
+ finalAttrs = { ...defaultAttrs, ...attrs }
31
+ Object.entries(finalAttrs).forEach(([attr, value]) => anchor.setAttribute(attr, value))
32
+ if (displayContent) anchor.append(displayContent)
33
+ return anchor
34
+ },
35
+
9
36
  elem(elemType, attrs = {}) {
10
37
  const elem = document.createElement(elemType)
11
38
  for (const attr in attrs) elem.setAttribute(attr, attrs[attr])
12
39
  return elem
13
40
  },
14
41
 
15
- svgElem(type, attrs) {
42
+ style(content, attrs = {}) {
43
+ const style = document.createElement('style')
44
+ for (const attr in attrs) style.setAttribute(attr, attrs[attr])
45
+ if (content) style.innerText = content
46
+ return style
47
+ },
48
+
49
+ svgElem(type, attrs = {}) {
16
50
  const elem = document.createElementNS('http://www.w3.org/2000/svg', type)
17
51
  for (const attr in attrs) elem.setAttributeNS(null, attr, attrs[attr])
18
52
  return elem
19
53
  }
20
54
  },
21
55
 
22
- fillStarryBG(targetNode) { // requires https://assets.aiwebextensions.com/styles/rising-stars/css/<black|white>.min.css
23
- if (targetNode.querySelector('[id*=stars]')) return
24
- const starsDivsContainer = document.createElement('div')
25
- starsDivsContainer.style.cssText = 'position: absolute ; top: 0 ; left: 0 ;' // hug targetNode's top-left corner
26
- + 'height: 100% ; width: 100% ; border-radius: 15px ; overflow: clip ;' // bound innards exactly by targetNode
27
- + 'z-index: -1'; // allow interactive elems to be clicked
28
- ['sm', 'med', 'lg'].forEach(starSize => {
29
- const starsDiv = document.createElement('div')
30
- starsDiv.id = `${ this.imports.env.ui.scheme == 'dark' ? 'white' : 'black' }-stars-${starSize}`
31
- starsDivsContainer.append(starsDiv)
32
- })
33
- 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
+
64
+ computedSize(elems, { dimension } = {}) { // total width/height of elems (including margins)
65
+ // * Returns { width: totalWidth, height: totalHeight } if no dimension passed
66
+ // * Returns float if { dimension: 'width' | 'height' } passed
67
+
68
+ // Validate args
69
+ elems = elems instanceof NodeList ? [...elems] : [].concat(elems)
70
+ elems.forEach(elem => { if (!(elem instanceof Node))
71
+ throw new Error(`Invalid elem: Element "${JSON.stringify(elem)}" is not a valid DOM node`) })
72
+ const validDimensions = ['width', 'height'], dimensionsToCompute = [].concat(dimension || validDimensions)
73
+ dimensionsToCompute.forEach(dimension => { if (!validDimensions.includes(dimension))
74
+ throw new Error('Invalid dimension: Use \'width\' or \'height\'') })
75
+
76
+ // Compute dimensions
77
+ const computedDimensions = { width: 0, height: 0 }
78
+ elems.forEach(elem => {
79
+ const elemStyle = getComputedStyle(elem) ; if (elemStyle.display == 'none') return
80
+ if (dimensionsToCompute.includes('width'))
81
+ computedDimensions.width += elem.getBoundingClientRect().width
82
+ + parseFloat(elemStyle.marginLeft) + parseFloat(elemStyle.marginRight)
83
+ if (dimensionsToCompute.includes('height'))
84
+ computedDimensions.height += elem.getBoundingClientRect().height
85
+ + parseFloat(elemStyle.marginTop) + parseFloat(elemStyle.marginBottom)
86
+ })
87
+
88
+ // Return computed dimensions
89
+ return dimensionsToCompute.length > 1 ? computedDimensions // obj w/ width/height
90
+ : computedDimensions[dimensionsToCompute[0]] // single total val
91
+ },
92
+
93
+ computedHeight(elems) { return this.computedSize(elems, { dimension: 'height' }) }, // including margins
94
+ computedWidth(elems) { return this.computedSize(elems, { dimension: 'width' }) }, // including margins
95
+
96
+ loadedElem(selector, timeout = null) {
97
+ const timeoutPromise = timeout ? new Promise(resolve => setTimeout(() => resolve(null), timeout)) : null
98
+ const isLoadedPromise = new Promise(resolve => {
99
+ const elem = document.querySelector(selector)
100
+ if (elem) resolve(elem)
101
+ else new MutationObserver((_, obs) => {
102
+ const elem = document.querySelector(selector)
103
+ if (elem) { obs.disconnect() ; resolve(elem) }
104
+ }).observe(document.documentElement, { childList: true, subtree: true })
105
+ })
106
+ return ( timeoutPromise ? Promise.race([isLoadedPromise, timeoutPromise]) : isLoadedPromise )
107
+ }
34
108
  }
35
109
  };
@@ -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,14 +15,12 @@ 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
@@ -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.29",
6
+ "version": "2025.2.10",
7
7
  "author": "KudoAI",
8
8
  "homepage_url": "https://github.com/KudoAI/chatgpt.js-chrome-starter",
9
9
  "icons": {
@@ -10,7 +10,7 @@
10
10
 
11
11
  // Import APP data
12
12
  const { app } = await chrome.storage.sync.get('app')
13
- icons.imports.import({ app }) // for src's using app.urls.assetHost
13
+ icons.import({ app }) // for srcs using app.urls.assetHost
14
14
 
15
15
  // Define FUNCTIONS
16
16
 
@@ -25,10 +25,10 @@
25
25
  fade() {
26
26
 
27
27
  // Update toolbar icon
28
- const iconDimensions = [16, 32, 64, 128], iconPaths = {}
29
- iconDimensions.forEach(dimension => iconPaths[dimension] = `../icons/${
30
- config.extensionDisabled ? 'faded/' : '' }icon${dimension}.png` )
31
- chrome.action.setIcon({ path: iconPaths })
28
+ chrome.action.setIcon({ path: Object.fromEntries(
29
+ Object.keys(chrome.runtime.getManifest().icons).map(dimension =>
30
+ [dimension, `../icons/${ config.extensionDisabled ? 'faded/' : '' }icon${dimension}.png`]
31
+ ))})
32
32
 
33
33
  // Update menu contents
34
34
  document.querySelectorAll('div.logo, div.menu-title, div.menu')
@@ -111,9 +111,9 @@
111
111
  const cjsDiv = dom.create.elem('div', { class: 'chatgpt-js' })
112
112
  const cjsLogo = dom.create.elem('img', {
113
113
  title: 'Powered by chatgpt.js',
114
- src: `${app.urls.cjsMediaHost}/images/badges/powered-by-chatgpt.js-faded.png?b2a1975` })
114
+ src: `${app.urls.cjsAssetHost}/images/badges/powered-by-chatgpt.js-faded.png?b2a1975` })
115
115
  cjsLogo.onmouseover = cjsLogo.onmouseout = event => cjsLogo.src = `${
116
- app.urls.cjsMediaHost}/images/badges/powered-by-chatgpt.js${
116
+ app.urls.cjsAssetHost}/images/badges/powered-by-chatgpt.js${
117
117
  event.type == 'mouseover' ? '' : '-faded' }.png?b2a1975`
118
118
  cjsLogo.onclick = () => chrome.tabs.create({ url: app.urls.chatgptJS })
119
119
  cjsDiv.append(cjsLogo) ; footer.append(cjsDiv)