@kudoai/chatgpt.js 3.7.1 → 3.8.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.
@@ -1,28 +1,28 @@
1
- // Requires lib/chatgpt.js + lib/dom.js + app + env
1
+ // Requires lib/<chatgpt|dom>.js + app + env
2
2
 
3
3
  window.modals = {
4
- import(deps) { Object.assign(this.imports = this.imports || {}, deps) },
5
4
 
6
5
  stack: [], // of types of undismissed modals
7
- get class() { return `${this.imports.app.cssPrefix}-modal` },
6
+ get class() { return `${app.cssPrefix}-modal` },
8
7
 
9
8
  about() {
9
+ const { ui: { scheme }, browser: { isPortrait }} = env
10
10
 
11
11
  // Show modal
12
12
  const labelStyles = 'text-transform: uppercase ; font-size: 17px ; font-weight: bold ;'
13
- + `color: ${ this.imports.env.ui.scheme == 'dark' ? 'white' : '#494141' }`
13
+ + `color: ${ scheme == 'dark' ? 'white' : '#494141' }`
14
14
  const aboutModal = this.alert(
15
- `${this.imports.app.symbol} ${chrome.runtime.getManifest().name}`, // title
15
+ `${app.symbol} ${chrome.runtime.getManifest().name}`, // title
16
16
  `<span style="${labelStyles}">🧠 Author:</span> `
17
- + `<a href="${this.imports.app.author.url}">${this.imports.app.author.name}</a> `
18
- + `& <a href="${this.imports.app.urls.contributors}">contributors</a>\n`
17
+ + `<a href="${app.author.url}">${app.author.name}</a> `
18
+ + `& <a href="${app.urls.contributors}">contributors</a>\n`
19
19
  + `<span style="${labelStyles}">🏷️ Version:</span> `
20
- + `<span class="about-em">${this.imports.app.version}</span>\n`
20
+ + `<span class="about-em">${app.version}</span>\n`
21
21
  + `<span style="${labelStyles}">📜 Open source code:</span> `
22
- + `<a href="${this.imports.app.urls.gitHub}" target="_blank" rel="nopener">`
23
- + this.imports.app.urls.gitHub + '</a>\n'
22
+ + `<a href="${app.urls.gitHub}" target="_blank" rel="nopener">`
23
+ + app.urls.gitHub + '</a>\n'
24
24
  + `<span style="${labelStyles}">⚡ Powered by:</span> `
25
- + `<a href="${this.imports.app.urls.chatgptJS}" target="_blank" rel="noopener">chatgpt.js</a>`,
25
+ + `<a href="${app.urls.chatgptJS}" target="_blank" rel="noopener">chatgpt.js</a>`,
26
26
  [ function getSupport(){}, function rateUs(){}, function moreAiExtensions(){} ], // button labels
27
27
  '', 656 // modal width
28
28
  )
@@ -32,7 +32,7 @@ window.modals = {
32
32
  'text-align: center ; font-size: 51px ; line-height: 46px ; padding: 15px 0' )
33
33
  aboutModal.querySelector('p').style.cssText = (
34
34
  'text-align: center ; overflow-wrap: anywhere ;'
35
- + `margin: ${ this.imports.env.browser.isPortrait ? '6px 0 -16px' : '3px 0 0' }` )
35
+ + `margin: ${ isPortrait ? '6px 0 -16px' : '3px 0 0' }` )
36
36
 
37
37
  // Hack buttons
38
38
  aboutModal.querySelector('.modal-buttons').style.justifyContent = 'center'
@@ -40,12 +40,11 @@ window.modals = {
40
40
  btn.style.cssText = 'height: 55px ; min-width: 136px ; text-align: center'
41
41
 
42
42
  // Replace buttons w/ clones that don't dismiss modal
43
- const btnClone = btn.cloneNode(true)
44
- btn.parentNode.replaceChild(btnClone, btn) ; btn = btnClone
43
+ btn.replaceWith(btn = btn.cloneNode(true))
45
44
  btn.onclick = () => this.safeWinOpen(
46
- btn.textContent == 'Get Support' ? `${modals.imports.app.urls.gitHub}/issues`
47
- : btn.textContent == 'Rate Us' ? `${modals.imports.app.urls.gitHub}/discussions`
48
- : modals.imports.app.urls.relatedExtensions
45
+ btn.textContent == 'Get Support' ? `${app.urls.gitHub}/issues`
46
+ : btn.textContent == 'Rate Us' ? `${app.urls.gitHub}/discussions`
47
+ : app.urls.relatedExtensions
49
48
  )
50
49
 
51
50
  // Prepend emoji
@@ -73,7 +72,7 @@ window.modals = {
73
72
  init(modal) {
74
73
  if (!modal) return // to support non-div this.open()s
75
74
  if (!this.styles) this.stylize() // to init/append stylesheet
76
- modal.classList.add('no-user-select', this.class) ; modal.parentNode.classList.add(`${this.class}-bg`)
75
+ modal.classList.add(this.class) ; modal.parentNode.classList.add(`${this.class}-bg`)
77
76
  dom.addRisingParticles(modal)
78
77
  },
79
78
 
@@ -104,34 +103,28 @@ window.modals = {
104
103
  safeWinOpen(url) { open(url, '_blank', 'noopener') }, // to prevent backdoor vulnerabilities
105
104
 
106
105
  stylize() {
107
- if (!this.styles) {
108
- this.styles = dom.create.elem('style') ; this.styles.id = `${this.class}-styles`
109
- document.head.append(this.styles)
110
- }
111
- this.styles.innerText = (
112
- `.no-user-select {
113
- user-select: none ; -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none }`
114
- + `.${this.class} {` // modals
106
+ const { ui: { scheme }, browser: { isMobile }} = env
107
+ if (!this.styles) document.head.append(this.styles = dom.create.elem('style'))
108
+ this.styles.textContent = (
109
+ `.${this.class} {` // modals
110
+ + 'user-select: none ; -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ;'
115
111
  + 'font-family: -apple-system, system-ui, BlinkMacSystemFont, Segoe UI, Roboto,'
116
112
  + 'Oxygen-Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif ;'
117
113
  + 'padding: 20px 25px 24px 25px !important ; font-size: 20px ;'
118
- + `color: ${ this.imports.env.ui.scheme == 'dark' ? 'white' : 'black' } !important ;`
114
+ + `color: ${ scheme == 'dark' ? 'white' : 'black' } !important ;`
119
115
  + `background-image: linear-gradient(180deg, ${
120
- this.imports.env.ui.scheme == 'dark' ? '#99a8a6 -200px, black 200px'
121
- : '#b6ebff -296px, white 171px' }) }`
116
+ scheme == 'dark' ? '#99a8a6 -200px, black 200px' : '#b6ebff -296px, white 171px' }) }`
122
117
  + `.${this.class} [class*=modal-close-btn] {`
123
118
  + 'position: absolute !important ; float: right ; top: 14px !important ; right: 16px !important ;'
124
119
  + 'cursor: pointer ; width: 33px ; height: 33px ; border-radius: 20px }'
125
120
  + `.${this.class} [class*=modal-close-btn] svg { height: 10px }`
126
121
  + `.${this.class} [class*=modal-close-btn] path {`
127
- + `${ this.imports.env.ui.scheme == 'dark' ? 'stroke: white ; fill: white'
128
- : 'stroke: #9f9f9f ; fill: #9f9f9f' }}`
129
- + ( this.imports.env.ui.scheme == 'dark' ? // invert dark mode hover paths
122
+ + `${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: #9f9f9f ; fill: #9f9f9f' }}`
123
+ + ( scheme == 'dark' ? // invert dark mode hover paths
130
124
  `.${this.class} [class*=modal-close-btn]:hover path { stroke: black ; fill: black }` : '' )
131
125
  + `.${this.class} [class*=modal-close-btn]:hover { background-color: #f2f2f2 }` // hover underlay
132
126
  + `.${this.class} [class*=modal-close-btn] svg { margin: 11.5px }` // center SVG for hover underlay
133
- + `.${this.class} a {`
134
- + `color: #${ this.imports.env.ui.scheme == 'dark' ? '00cfff' : '1e9ebb' } !important }`
127
+ + `.${this.class} a { color: #${ scheme == 'dark' ? '00cfff' : '1e9ebb' } !important }`
135
128
  + `.${this.class} h2 { font-weight: bold }`
136
129
  + `.${this.class} button {`
137
130
  + '--btn-transition: transform 0.1s ease-in-out, box-shadow 0.1s ease-in-out ;'
@@ -141,14 +134,13 @@ window.modals = {
141
134
  + '-webkit-transition: var(--btn-transition) ; -moz-transition: var(--btn-transition) ;'
142
135
  + '-o-transition: var(--btn-transition) ; -ms-transition: var(--btn-transition) ;'
143
136
  + 'cursor: pointer !important ;' // add finger cursor
144
- + `border: 1px solid ${ this.imports.env.ui.scheme == 'dark' ? 'white' : 'black' } !important ;`
137
+ + `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' } !important ;`
145
138
  + 'padding: 8px !important ; min-width: 102px }' // resize
146
139
  + `.${this.class} button:hover {` // add zoom, re-scheme
147
140
  + 'transform: scale(1.055) ; color: black !important ;'
148
- + `background-color: #${ this.imports.env.ui.scheme == 'dark' ? '00cfff' : '9cdaff' } !important }`
149
- + ( !this.imports.env.browser.isMobile ?
150
- `.${this.class} .modal-buttons { margin-left: -13px !important }` : '' )
151
- + `.about-em { color: ${ this.imports.env.ui.scheme == 'dark' ? 'white' : 'green' } !important }`
141
+ + `background-color: #${ scheme == 'dark' ? '00cfff' : '9cdaff' } !important }`
142
+ + ( !isMobile ? `.${this.class} .modal-buttons { margin-left: -13px !important }` : '' )
143
+ + `.about-em { color: ${ scheme == 'dark' ? 'white' : 'green' } !important }`
152
144
  )
153
145
  }
154
146
  };
@@ -4,29 +4,23 @@
4
4
  (async () => {
5
5
 
6
6
  // Import JS resources
7
- for (const resource of ['components/modals.js', 'lib/chatgpt.js', 'lib/dom.js', 'lib/settings.js'])
7
+ for (const resource of ['components/modals.js', 'lib/chatgpt.js', 'lib/dom.js', 'lib/settings.js', 'lib/ui.js'])
8
8
  await import(chrome.runtime.getURL(resource))
9
9
 
10
10
  // Init ENV context
11
- const env = { browser: { isMobile: chatgpt.browser.isMobile() }, ui: { scheme: getScheme() }}
11
+ window.env = { browser: { isMobile: chatgpt.browser.isMobile() }, ui: { scheme: ui.getScheme() }}
12
12
  env.browser.isPortrait = env.browser.isMobile && (window.innerWidth < window.innerHeight)
13
13
 
14
14
  // Import APP data
15
- const { app } = await chrome.storage.local.get('app')
16
-
17
- // Export DEPENDENCIES to imported resources
18
- dom.import({ env }) // for env.ui.scheme
19
- modals.import({ app, env }) // for app data + env.<browser|ui> flags
20
-
21
- // Add CHROME MSG listener
22
- chrome.runtime.onMessage.addListener(req => { // from service-worker.js + popup/index.html
23
- if (req.action == 'notify')
24
- notify(...['msg', 'pos', 'notifDuration', 'shadow'].map(arg => req.options[arg]))
25
- else if (req.action == 'alert')
26
- modals.alert(...['title', 'msg', 'btns', 'checkbox', 'width'].map(arg => req.options[arg]))
27
- else if (req.action == 'showAbout') {
28
- config.skipAlert = true ; chatgpt.isLoaded().then(() => modals.open('about'))
29
- } else if (req.action == 'syncConfigToUI') syncConfigToUI(req.options)
15
+ ;({ app: window.app } = await chrome.storage.local.get('app'))
16
+
17
+ chrome.runtime.onMessage.addListener(({ action, options }) => { // from service-worker.js + popup/index.html
18
+ ({
19
+ notify: () => notify(...['msg', 'pos', 'notifDuration', 'shadow'].map(arg => options[arg])),
20
+ alert: () => modals.alert(...['title', 'msg', 'btns', 'checkbox', 'width'].map(arg => options[arg])),
21
+ showAbout: () => { config.skipAlert = true ; chatgpt.isLoaded().then(() => modals.open('about')) },
22
+ syncConfigToUI: () => syncConfigToUI(options)
23
+ }[action]?.() || console.warn(`Received unsupported action: "${action}"`))
30
24
  })
31
25
 
32
26
  // Init SETTINGS
@@ -76,23 +70,19 @@
76
70
  }
77
71
  }
78
72
 
79
- function getScheme() {
80
- return document.documentElement.className
81
- || (window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light')
82
- }
83
-
84
73
  // Run MAIN routine
85
74
 
86
75
  chatgpt.printAllFunctions() // to console
87
76
 
88
77
  // CHILL a bit if your hacks depend on delayed DOM content
89
78
  await chatgpt.isLoaded()
90
- await new Promise(resolve => setTimeout(resolve, 500)); // sleep .5s
79
+ await new Promise(resolve => setTimeout(resolve, 500)) // sleep .5s
91
80
 
92
81
  // Add RISING PARTICLES styles for modals
93
- ['gray', 'white'].forEach(color => document.head.append(
82
+ ;['gray', 'white'].forEach(color => document.head.append(
94
83
  dom.create.elem('link', { rel: 'stylesheet',
95
- href: `https://assets.aiwebextensions.com/styles/rising-particles/dist/${color}.min.css?v=727feff`
84
+ href: `https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@727feff/assets/styles/rising-particles/dist/${
85
+ color}.min.css`
96
86
  })))
97
87
 
98
88
  if (config.extensionDisabled) return
@@ -112,7 +102,7 @@
112
102
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener( // for browser/system scheme pref changes
113
103
  'change', () => requestAnimationFrame(handleSchemePrefChange))
114
104
  function handleSchemePrefChange() {
115
- const displayedScheme = getScheme()
105
+ const displayedScheme = ui.getScheme()
116
106
  if (env.ui.scheme != displayedScheme) { env.ui.scheme = displayedScheme ; modals.stylize() }
117
107
  }
118
108
 
@@ -24,26 +24,34 @@ const chatgpt = {
24
24
 
25
25
  selectors: {
26
26
  btns: {
27
- continue: 'button:has([class*=rotate] [d^="M4.47189"])', login: '[data-testid*=login]',
28
- newChat: 'button[data-testid*=new-chat-button],' // sidebar button (when logged in)
29
- + 'button:has([d^="M3.06957"]),' // Cycle Arrows icon (Temp chat mode)
30
- + 'button:has([d^="M15.6729"])', // Pencil icon (recorded chat mode)
31
- regen: 'button[data-testid*="regenerate"],' // oval button in place of chatbar on errors
32
- + 'div[role=menuitem]:has([d^="M3.06957"])', // 'Try Again' entry of model selector below msg
33
- scroll: 'button:has([d^="M12 21C11.7348"])',
34
- send: '[data-testid=send-button]', sidebar: 'button[data-testid*=sidebar-button]',
35
- stop: 'button[data-testid=stop-button]', voice: 'button[data-testid*=composer-speech-button]'
27
+ continue: 'button:has(svg[class*=rotate] > path[d^="M4.47189"])',
28
+ createImage: 'button[data-testid="composer-create-image"]',
29
+ deepResearch: 'button[data-testid="composer-deep-research"]',
30
+ login: 'button[data-testid*=login]',
31
+ newChat: 'a[href="/"]:has(svg),' // Pencil button (when logged in)
32
+ + 'button:has([d^="M3.06957"])', // Cycle Arrows button (in temp chat logged out)
33
+ regen: 'button[data-testid*=regenerate],' // oval button in place of chatbar on errors
34
+ // 'Try Again' entry of model selector below msg
35
+ + 'div[role=menuitem] div:has(svg):has(path[d^="M3.06957"])',
36
+ scroll: 'button:has(> svg > path[d^="M12 21C11.7348"])',
37
+ search: 'button[data-testid="composer-button-search"]',
38
+ reason: 'button[data-testid="composer-button-reason"]',
39
+ send: 'button[data-testid=send-button]',
40
+ sidebar: 'button[data-testid*=sidebar-button]',
41
+ stop: 'button[data-testid=stop-button]',
42
+ upload: 'button:has(> svg > path[d^="M12 3C12.5523"])',
43
+ voice: 'button[data-testid*=composer-speech-button]'
36
44
  },
37
45
  chatDivs: {
38
- convo: 'main > div > div > div > div > div > div[class*=group]',
39
- msg: 'div[data-message-author-role]', reply: 'div[data-message-author-role=assistant]'
46
+ convo: 'div[class*=thread]', msg: 'div[data-message-author-role]',
47
+ reply: 'div[data-message-author-role=assistant]'
40
48
  },
41
- chatHistory: 'nav',
42
- errors: { txt: '[class*=text-error]' },
43
- footer: '.min-h-4',
44
- header: 'main .sticky',
49
+ chatHistory: 'div#history',
50
+ errors: { toast: 'div.toast-root', txt: 'div[class*=text-error]' },
51
+ footer: 'div#thread-bottom-container > div:last-of-type > div, span.text-sm.leading-none',
52
+ header: 'div#page-header, main div.sticky:first-of-type',
45
53
  links: { newChat: 'nav a[href="/"]', sidebarItem: 'nav a' },
46
- sidebar: 'div[class*=sidebar]',
54
+ sidebar: 'div.bg-token-sidebar-surface-primary',
47
55
  ssgManifest: 'script[src*="_ssgManifest.js"]'
48
56
  },
49
57
 
@@ -120,14 +128,16 @@ const chatgpt = {
120
128
  drag: {
121
129
  mousedown(event) { // find modal, update styles, attach listeners, init XY offsets
122
130
  if (event.button != 0) return // prevent non-left-click drag
123
- if (getComputedStyle(event.target).cursor == 'pointer') return // prevent drag on interactive elems
131
+ if (!/auto|default/.test(getComputedStyle(event.target).cursor))
132
+ return // prevent drag on interactive elems
124
133
  chatgpt.draggingModal = event.currentTarget
125
134
  event.preventDefault() // prevent sub-elems like icons being draggable
126
135
  Object.assign(chatgpt.draggingModal.style, {
127
- cursor: 'grabbing', transition: '0.1s', willChange: 'transform', transform: 'scale(1.05)' });
128
- [...chatgpt.draggingModal.children] // prevent hover FX if drag lags behind cursor
129
- .forEach(child => child.style.pointerEvents = 'none');
130
- ['mousemove', 'mouseup'].forEach(eventType => // add listeners
136
+ transition: '0.1s', willChange: 'transform', transform: 'scale(1.05)' })
137
+ document.body.style.cursor = 'grabbing' // update cursor
138
+ ;[...chatgpt.draggingModal.children] // prevent hover FX if drag lags behind cursor
139
+ .forEach(child => child.style.pointerEvents = 'none')
140
+ ;['mousemove', 'mouseup'].forEach(eventType => // add listeners
131
141
  document.addEventListener(eventType, handlers.drag[eventType]))
132
142
  const draggingModalRect = chatgpt.draggingModal.getBoundingClientRect()
133
143
  handlers.drag.offsetX = event.clientX - draggingModalRect.left +21
@@ -143,10 +153,11 @@ const chatgpt = {
143
153
 
144
154
  mouseup() { // restore styles/pointer events, remove listeners, reset chatgpt.draggingModal
145
155
  Object.assign(chatgpt.draggingModal.style, { // restore styles
146
- cursor: 'inherit', transition: 'inherit', willChange: 'auto', transform: 'scale(1)' });
147
- [...chatgpt.draggingModal.children] // restore pointer events
148
- .forEach(child => child.style.pointerEvents = '');
149
- ['mousemove', 'mouseup'].forEach(eventType => // remove listeners
156
+ cursor: 'inherit', transition: 'inherit', willChange: 'auto', transform: 'scale(1)' })
157
+ document.body.style.cursor = '' // restore cursor
158
+ ;[...chatgpt.draggingModal.children] // restore pointer events
159
+ .forEach(child => child.style.pointerEvents = '')
160
+ ;['mousemove', 'mouseup'].forEach(eventType => // remove listeners
150
161
  document.removeEventListener(eventType, handlers.drag[eventType]))
151
162
  chatgpt.draggingModal = null
152
163
  }
@@ -170,7 +181,7 @@ const chatgpt = {
170
181
  modalStyle.setAttribute('last-updated', thisUpdated.toString())
171
182
  document.head.append(modalStyle)
172
183
  }
173
- modalStyle.innerText = ( // update prev/new style contents
184
+ modalStyle.textContent = ( // update prev/new style contents
174
185
  `.chatgpt-modal { /* vars */
175
186
  --transition: opacity 0.65s cubic-bezier(.165,.84,.44,1), /* for fade-in */
176
187
  transform 0.55s cubic-bezier(.165,.84,.44,1) ; /* for move-in */
@@ -222,7 +233,7 @@ const chatgpt = {
222
233
  display: flex ; justify-content: flex-end ; margin: 20px -5px -3px 0 ;
223
234
  ${ isMobile ? 'flex-direction: column-reverse' : '' }}
224
235
  .chatgpt-modal button {
225
- font-size: 14px ; text-transform: uppercase ;
236
+ font-size: 14px ; text-transform: uppercase ; cursor: crosshair ;
226
237
  margin-left: ${ isMobile ? 0 : 10 }px ; padding: ${ isMobile ? 15 : 8 }px 18px ;
227
238
  ${ isMobile ? 'margin-top: 5px ; margin-bottom: 3px ;' : '' }
228
239
  border-radius: 0 ; border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' };
@@ -260,7 +271,7 @@ const chatgpt = {
260
271
  }
261
272
 
262
273
  // Insert text into elems
263
- modalTitle.innerText = title || '' ; modalMessage.innerText = msg || '' ; chatgpt.renderHTML(modalMessage)
274
+ modalTitle.textContent = title || '' ; modalMessage.innerText = msg || '' ; chatgpt.renderHTML(modalMessage)
264
275
 
265
276
  // Create/append buttons (if provided) to buttons div
266
277
  const modalButtons = document.createElement('div')
@@ -297,7 +308,7 @@ const chatgpt = {
297
308
  // Create/show label
298
309
  const checkboxLabel = document.createElement('label')
299
310
  checkboxLabel.onclick = () => { checkboxInput.checked = !checkboxInput.checked ; checkboxFn() }
300
- checkboxLabel.textContent = checkboxFn.name.charAt(0).toUpperCase() // capitalize first char
311
+ checkboxLabel.textContent = checkboxFn.name[0].toUpperCase() // capitalize first char
301
312
  + checkboxFn.name.slice(1) // format remaining chars
302
313
  .replace(/([A-Z])/g, (match, letter) => ' ' + letter.toLowerCase()) // insert spaces, convert to lowercase
303
314
  .replace(/\b(\w+)nt\b/gi, '$1n\'t') // insert apostrophe in 'nt' suffixes
@@ -646,7 +657,7 @@ const chatgpt = {
646
657
  const msgs = [] ; let isUserMsg = true
647
658
  chatDivs.forEach(div => {
648
659
  const sender = isUserMsg ? 'USER' : 'CHATGPT'; isUserMsg = !isUserMsg
649
- const msg = Array.from(div.childNodes).map(node => node.innerText)
660
+ const msg = [...div.childNodes].map(node => node.innerText)
650
661
  .join('\n\n') // insert double line breaks between paragraphs
651
662
  .replace('Copy code', '')
652
663
  msgs.push(`${sender}: ${msg}`)
@@ -1148,7 +1159,7 @@ const chatgpt = {
1148
1159
  }
1149
1160
  },
1150
1161
 
1151
- isDarkMode() { return document.documentElement.className.includes('dark') },
1162
+ isDarkMode() { return document.documentElement.classList.contains('dark') },
1152
1163
  isFullScreen() { return chatgpt.browser.isFullScreen() },
1153
1164
 
1154
1165
  async isIdle(timeout = null) {
@@ -1189,7 +1200,7 @@ const chatgpt = {
1189
1200
  return await ( timeoutPromise ? Promise.race([isLoadedPromise, timeoutPromise]) : isLoadedPromise )
1190
1201
  },
1191
1202
 
1192
- isLightMode() { return document.documentElement.classList.toString().includes('light') },
1203
+ isLightMode() { return document.documentElement.classList.contains('light') },
1193
1204
  isTempChat() { return location.search == '?temporary-chat=true' },
1194
1205
  isTyping() { return !!this.getStopButton() },
1195
1206
  login() { window.location.href = 'https://chat.openai.com/auth/login' },
@@ -1289,7 +1300,7 @@ const chatgpt = {
1289
1300
  const notificationDiv = document.createElement('div') // make div
1290
1301
  notificationDiv.id = Math.floor(chatgpt.randomFloat() * 1000000) + Date.now()
1291
1302
  notificationDiv.classList.add('chatgpt-notif')
1292
- notificationDiv.innerText = msg // insert msg
1303
+ notificationDiv.textContent = msg // insert msg
1293
1304
  document.body.append(notificationDiv) // insert into DOM
1294
1305
 
1295
1306
  // Create/append close button
@@ -1322,7 +1333,7 @@ const chatgpt = {
1322
1333
  notifStyle.setAttribute('last-updated', thisUpdated.toString())
1323
1334
  document.head.append(notifStyle)
1324
1335
  }
1325
- notifStyle.innerText = ( // update prev/new style contents
1336
+ notifStyle.textContent = ( // update prev/new style contents
1326
1337
  '.chatgpt-notif {'
1327
1338
  + 'font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC",'
1328
1339
  + '"Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", sans-serif ;'
@@ -1358,17 +1369,17 @@ const chatgpt = {
1358
1369
  notificationDiv.style.right = notificationDiv.isRight ? vpXoffset.toString() + 'px' : ''
1359
1370
  notificationDiv.style.left = !notificationDiv.isRight ? vpXoffset.toString() + 'px' : ''
1360
1371
 
1361
- // Reposition old notifications
1372
+ // Re-position old notifications
1362
1373
  const thisQuadrantQueue = notifyProps.queue[notificationDiv.quadrant]
1363
1374
  if (thisQuadrantQueue.length > 1) {
1364
1375
  try { // to move old notifications
1365
1376
  for (const divId of thisQuadrantQueue.slice(0, -1)) { // exclude new div
1366
1377
  const oldDiv = document.getElementById(divId),
1367
1378
  offsetProp = oldDiv.style.top ? 'top' : 'bottom', // pick property to change
1368
- vOffset = +/\d+/.exec(oldDiv.style[offsetProp])[0] + 5 + oldDiv.getBoundingClientRect().height
1379
+ vOffset = +parseInt(oldDiv.style[offsetProp]) +5 + oldDiv.getBoundingClientRect().height
1369
1380
  oldDiv.style[offsetProp] = `${ vOffset }px` // change prop
1370
1381
  }
1371
- } catch (err) {}
1382
+ } catch (err) { console.warn('Failed to re-position notification:', err) }
1372
1383
  }
1373
1384
 
1374
1385
  // Show notification
@@ -1496,7 +1507,7 @@ const chatgpt = {
1496
1507
  // Process text node
1497
1508
  if (childNode.nodeType == Node.TEXT_NODE) {
1498
1509
  const text = childNode.nodeValue,
1499
- elems = Array.from(text.matchAll(reTags))
1510
+ elems = [...text.matchAll(reTags)]
1500
1511
 
1501
1512
  // Process 1st element to render
1502
1513
  if (elems.length > 0) {
@@ -1505,7 +1516,7 @@ const chatgpt = {
1505
1516
  tagNode = document.createElement(tagName) ; tagNode.textContent = tagText
1506
1517
 
1507
1518
  // Extract/set attributes
1508
- const attrs = Array.from(tagAttrs.matchAll(reAttrs))
1519
+ const attrs = [...tagAttrs.matchAll(reAttrs)]
1509
1520
  attrs.forEach(attr => {
1510
1521
  const name = attr[1], value = attr[2].replace(/['"]/g, '')
1511
1522
  tagNode.setAttribute(name, value)
@@ -1541,9 +1552,8 @@ const chatgpt = {
1541
1552
  // responseToGet = index of response to get (defaults to latest if '' unpassed)
1542
1553
  // regenResponseToGet = index of regenerated response to get (defaults to latest if '' unpassed)
1543
1554
 
1544
- if (window.location.href.startsWith('https://chatgpt.com/c/'))
1545
- return this.getFromDOM.apply(null, arguments)
1546
- else return this.getFromAPI.apply(null, arguments)
1555
+ return this[`getFrom${ location.href.startsWith('https://chatgpt.com/c/') ? 'DOM' : 'API' }`]
1556
+ .apply(null, arguments)
1547
1557
  },
1548
1558
 
1549
1559
  getFromAPI(chatToGet, responseToGet) {
@@ -1604,7 +1614,7 @@ const chatgpt = {
1604
1614
  const textArea = chatgpt.getChatBox()
1605
1615
  if (!textArea) return console.error('Chatbar element not found!')
1606
1616
  const msgP = document.createElement('p'); msgP.textContent = msg
1607
- textArea.replaceChild(msgP, textArea.querySelector('p'))
1617
+ textArea.querySelector('p').replaceWith(msgP)
1608
1618
  textArea.dispatchEvent(new Event('input', { bubbles: true })) // enable send button
1609
1619
  setTimeout(function delaySend() {
1610
1620
  const sendBtn = chatgpt.getSendButton()
@@ -1753,8 +1763,7 @@ const chatgpt = {
1753
1763
  activateObserver() {
1754
1764
 
1755
1765
  // Stop the previous observer to preserve resources
1756
- if (this.observer instanceof MutationObserver)
1757
- try { this.observer.disconnect() } catch (e) {}
1766
+ if (this.observer instanceof MutationObserver) this.observer.disconnect()
1758
1767
 
1759
1768
  if (!this.elems.length) return console.error('🤖 chatgpt.js >> No elems to append!')
1760
1769
 
@@ -1857,7 +1866,7 @@ const chatgpt = {
1857
1866
  isOn() {
1858
1867
  const sidebar = (() => {
1859
1868
  return chatgpt.sidebar.exists() ? document.querySelector(chatgpt.selectors.sidebar) : null })()
1860
- if (!sidebar) { console.error('Sidebar element not found!'); return false }
1869
+ if (!sidebar) { return console.error('Sidebar element not found!') || false }
1861
1870
  else return chatgpt.browser.isMobile() ?
1862
1871
  document.documentElement.style.overflow == 'hidden'
1863
1872
  : sidebar.style.visibility != 'hidden' && sidebar.style.width != '0px'
@@ -2095,7 +2104,7 @@ const cjsFuncSynonyms = [
2095
2104
  // Define HELPER functions
2096
2105
 
2097
2106
  function toCamelCase(words) {
2098
- return words.map((word, idx) => idx == 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)).join('') }
2107
+ return words.map((word, idx) => idx == 0 ? word : word[0].toUpperCase() + word.slice(1)).join('') }
2099
2108
 
2100
2109
  // Prefix console logs w/ '🤖 chatgpt.js >> '
2101
2110
  const consolePrefix = '🤖 chatgpt.js >> ', ogError = console.error, ogInfo = console.info
@@ -2,7 +2,6 @@
2
2
  // Source: https://github.com/adamlui/ai-web-extensions/blob/main/assets/lib/dom.js/src/dom.js
3
3
 
4
4
  window.dom = {
5
- import(deps) { Object.assign(this.imports = this.imports || {}, deps) },
6
5
 
7
6
  addRisingParticles(targetNode, { lightScheme = 'gray', darkScheme = 'white' } = {}) {
8
7
  // * Requires https://assets.aiwebextensions.com/styles/rising-particles/dist/<lightScheme>.min.css
@@ -12,11 +11,11 @@ window.dom = {
12
11
  particlesDivsWrapper.style.cssText = (
13
12
  'position: absolute ; top: 0 ; left: 0 ;' // hug targetNode's top-left corner
14
13
  + '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 => {
14
+ + 'z-index: -1' ) // allow interactive elems to be clicked
15
+ ;['sm', 'med', 'lg'].forEach(particleSize => {
17
16
  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
17
+ particlesDiv.id = config?.bgAnimationsDisabled ? `particles-${particleSize}-off`
18
+ : `${( env?.ui?.scheme || env?.ui?.app?.scheme ) == 'dark' ? darkScheme
20
19
  : lightScheme }-particles-${particleSize}`
21
20
  particlesDivsWrapper.append(particlesDiv)
22
21
  })
@@ -41,8 +40,9 @@ window.dom = {
41
40
 
42
41
  style(content, attrs = {}) {
43
42
  const style = document.createElement('style')
43
+ style.setAttribute('type', 'text/css') // support older browsers
44
44
  for (const attr in attrs) style.setAttribute(attr, attrs[attr])
45
- if (content) style.innerText = content
45
+ if (content) style.textContent = content
46
46
  return style
47
47
  },
48
48
 
@@ -93,17 +93,19 @@ window.dom = {
93
93
  computedHeight(elems) { return this.computedSize(elems, { dimension: 'height' }) }, // including margins
94
94
  computedWidth(elems) { return this.computedSize(elems, { dimension: 'width' }) }, // including margins
95
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) => {
96
+ loadedElem(selector, { timeout = null } = {}) {
97
+ const raceEntries = [
98
+ new Promise(resolve => { // when elem loads
102
99
  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 )
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
+ ]
107
+ if (timeout) raceEntries.push(new Promise(resolve => setTimeout(() => resolve(null), timeout)))
108
+ return Promise.race(raceEntries)
107
109
  }
108
110
  }
109
111
  };
@@ -1,29 +1,55 @@
1
1
  window.config = {}
2
2
  window.settings = {
3
3
 
4
- // Init SETTINGS props (for popup menu)
5
4
  controls: {
6
5
  // Add settings options as keys, with each key's value being an object that includes:
7
- // - 'type': the control type (e.g. 'toggle' or 'prompt')
6
+ // - 'type': the control type (e.g. 'toggle', 'link' or 'prompt')
8
7
  // - 'label': a descriptive label
9
8
  // - 'defaultVal' (optional): default value of setting (true for toggles if unspecified, false otherwise)
9
+ // - 'category' (optional): string key from this.categories to group control under
10
10
  // - 'symbol' (optional): for icon display (e.g. ⌚)
11
- // NOTE: Toggles are disabled by default unless key name contains 'disabled' or 'hidden' (case insensitive)
12
- // NOTE: Controls are displayed in top-to-bottom order
11
+ // - 'helptip' (optional): tooltip to display on hover
12
+
13
+ // NOTE: Controls are displayed in top-to-bottom order (within categories and in top-level)
14
+ // NOTE: Toggles are disabled by default unless defaultVal is true
15
+ // ...or key name contains 'disabled' or 'hidden' (case insensitive)
16
+
13
17
  // EXAMPLES:
14
18
  // autoScrollDisabled: { type: 'toggle', label: 'Auto-Scroll' },
15
19
  // replyLanguage: { type: 'prompt', symbol: '🌐', label: 'Reply Language' }
16
20
  },
17
21
 
22
+ categories: {
23
+ // Add category entries as keys, with each key's value being an object that includes:
24
+ // - 'label': a descriptive label
25
+ // - 'symbol' (optional): for icon display (e.g. ⌚)
26
+ // - 'color' (optional): hex code (w/o #) of color for left-border
27
+ // - 'helptip' (optional): tooltip to display on hover
28
+ // - 'autoExpand' (optional): true/false to auto-expand categories on toolbar icon click
29
+
30
+ // NOTE: Categories are displayed in top-to-bottom order
31
+
32
+ // EXAMPLE:
33
+ // displaySettings: {
34
+ // symbol: '🖥️', color: '94fca2', label: 'Display Settings', helptip: 'Display-related settings' }
35
+ },
36
+
37
+ typeIsEnabled(key) { // for menu labels + notifs to return ON/OFF for type w/o suffix
38
+ const reInvertFlags = /disabled|hidden/i
39
+ return reInvertFlags.test(key) // flag in control key name
40
+ && !reInvertFlags.test(this.controls[key]?.label) // but not in label name
41
+ ? !config[key] : config[key] // so invert since flag reps opposite type state, else don't
42
+ },
43
+
18
44
  load(...keys) {
19
45
  return Promise.all(keys.flat().map(async key => // resolve promise when all keys load
20
- window.config[key] = (await chrome.storage.local.get(key))[key]
46
+ config[key] = (await chrome.storage.local.get(key))[key]
21
47
  ?? this.controls[key]?.defaultVal ?? this.controls[key]?.type == 'toggle'
22
48
  ))
23
49
  },
24
50
 
25
51
  save(key, val) {
26
52
  chrome.storage.local.set({ [key]: val }) // save to Chrome extension storage
27
- window.config[key] = val // save to memory
53
+ config[key] = val // save to memory
28
54
  }
29
55
  };
@@ -0,0 +1,6 @@
1
+ window.ui = {
2
+ getScheme() {
3
+ return /\b(light|dark)\b/.exec(document.documentElement.className)?.[1]
4
+ || ( window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' )
5
+ }
6
+ };