@kudoai/chatgpt.js 3.7.0 → 3.8.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.
@@ -1,28 +1,29 @@
1
1
  // Requires lib/chatgpt.js + lib/dom.js + app + env
2
2
 
3
3
  window.modals = {
4
- import(deps) { Object.assign(this.imports = this.imports || {}, deps) },
4
+ import(deps) { Object.assign(this.imports ||= {}, deps) },
5
5
 
6
6
  stack: [], // of types of undismissed modals
7
7
  get class() { return `${this.imports.app.cssPrefix}-modal` },
8
8
 
9
9
  about() {
10
+ const { app, env: { ui: { scheme }, browser: { isPortrait }}} = this.imports
10
11
 
11
12
  // Show modal
12
13
  const labelStyles = 'text-transform: uppercase ; font-size: 17px ; font-weight: bold ;'
13
- + `color: ${ this.imports.env.ui.scheme == 'dark' ? 'white' : '#494141' }`
14
+ + `color: ${ scheme == 'dark' ? 'white' : '#494141' }`
14
15
  const aboutModal = this.alert(
15
- `${this.imports.app.symbol} ${chrome.runtime.getManifest().name}`, // title
16
+ `${app.symbol} ${chrome.runtime.getManifest().name}`, // title
16
17
  `<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`
18
+ + `<a href="${app.author.url}">${this.imports.app.author.name}</a> `
19
+ + `& <a href="${app.urls.contributors}">contributors</a>\n`
19
20
  + `<span style="${labelStyles}">🏷️ Version:</span> `
20
- + `<span class="about-em">${this.imports.app.version}</span>\n`
21
+ + `<span class="about-em">${app.version}</span>\n`
21
22
  + `<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'
23
+ + `<a href="${app.urls.gitHub}" target="_blank" rel="nopener">`
24
+ + app.urls.gitHub + '</a>\n'
24
25
  + `<span style="${labelStyles}">⚡ Powered by:</span> `
25
- + `<a href="${this.imports.app.urls.chatgptJS}" target="_blank" rel="noopener">chatgpt.js</a>`,
26
+ + `<a href="${app.urls.chatgptJS}" target="_blank" rel="noopener">chatgpt.js</a>`,
26
27
  [ function getSupport(){}, function rateUs(){}, function moreAiExtensions(){} ], // button labels
27
28
  '', 656 // modal width
28
29
  )
@@ -32,7 +33,7 @@ window.modals = {
32
33
  'text-align: center ; font-size: 51px ; line-height: 46px ; padding: 15px 0' )
33
34
  aboutModal.querySelector('p').style.cssText = (
34
35
  'text-align: center ; overflow-wrap: anywhere ;'
35
- + `margin: ${ this.imports.env.browser.isPortrait ? '6px 0 -16px' : '3px 0 0' }` )
36
+ + `margin: ${ isPortrait ? '6px 0 -16px' : '3px 0 0' }` )
36
37
 
37
38
  // Hack buttons
38
39
  aboutModal.querySelector('.modal-buttons').style.justifyContent = 'center'
@@ -40,8 +41,7 @@ window.modals = {
40
41
  btn.style.cssText = 'height: 55px ; min-width: 136px ; text-align: center'
41
42
 
42
43
  // Replace buttons w/ clones that don't dismiss modal
43
- const btnClone = btn.cloneNode(true)
44
- btn.parentNode.replaceChild(btnClone, btn) ; btn = btnClone
44
+ btn.replaceWith(btn = btn.cloneNode(true))
45
45
  btn.onclick = () => this.safeWinOpen(
46
46
  btn.textContent == 'Get Support' ? `${modals.imports.app.urls.gitHub}/issues`
47
47
  : btn.textContent == 'Rate Us' ? `${modals.imports.app.urls.gitHub}/discussions`
@@ -73,7 +73,7 @@ window.modals = {
73
73
  init(modal) {
74
74
  if (!modal) return // to support non-div this.open()s
75
75
  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`)
76
+ modal.classList.add(this.class) ; modal.parentNode.classList.add(`${this.class}-bg`)
77
77
  dom.addRisingParticles(modal)
78
78
  },
79
79
 
@@ -104,34 +104,28 @@ window.modals = {
104
104
  safeWinOpen(url) { open(url, '_blank', 'noopener') }, // to prevent backdoor vulnerabilities
105
105
 
106
106
  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
- }
107
+ const { env: { ui: { scheme }, browser: { isMobile }}} = this.imports
108
+ if (!this.styles) document.head.append(this.styles = dom.create.elem('style'))
111
109
  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
110
+ `.${this.class} {` // modals
111
+ + 'user-select: none ; -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ;'
115
112
  + 'font-family: -apple-system, system-ui, BlinkMacSystemFont, Segoe UI, Roboto,'
116
113
  + 'Oxygen-Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif ;'
117
114
  + 'padding: 20px 25px 24px 25px !important ; font-size: 20px ;'
118
- + `color: ${ this.imports.env.ui.scheme == 'dark' ? 'white' : 'black' } !important ;`
115
+ + `color: ${ scheme == 'dark' ? 'white' : 'black' } !important ;`
119
116
  + `background-image: linear-gradient(180deg, ${
120
- this.imports.env.ui.scheme == 'dark' ? '#99a8a6 -200px, black 200px'
121
- : '#b6ebff -296px, white 171px' }) }`
117
+ scheme == 'dark' ? '#99a8a6 -200px, black 200px' : '#b6ebff -296px, white 171px' }) }`
122
118
  + `.${this.class} [class*=modal-close-btn] {`
123
119
  + 'position: absolute !important ; float: right ; top: 14px !important ; right: 16px !important ;'
124
120
  + 'cursor: pointer ; width: 33px ; height: 33px ; border-radius: 20px }'
125
121
  + `.${this.class} [class*=modal-close-btn] svg { height: 10px }`
126
122
  + `.${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
123
+ + `${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: #9f9f9f ; fill: #9f9f9f' }}`
124
+ + ( scheme == 'dark' ? // invert dark mode hover paths
130
125
  `.${this.class} [class*=modal-close-btn]:hover path { stroke: black ; fill: black }` : '' )
131
126
  + `.${this.class} [class*=modal-close-btn]:hover { background-color: #f2f2f2 }` // hover underlay
132
127
  + `.${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 }`
128
+ + `.${this.class} a { color: #${ scheme == 'dark' ? '00cfff' : '1e9ebb' } !important }`
135
129
  + `.${this.class} h2 { font-weight: bold }`
136
130
  + `.${this.class} button {`
137
131
  + '--btn-transition: transform 0.1s ease-in-out, box-shadow 0.1s ease-in-out ;'
@@ -141,14 +135,13 @@ window.modals = {
141
135
  + '-webkit-transition: var(--btn-transition) ; -moz-transition: var(--btn-transition) ;'
142
136
  + '-o-transition: var(--btn-transition) ; -ms-transition: var(--btn-transition) ;'
143
137
  + 'cursor: pointer !important ;' // add finger cursor
144
- + `border: 1px solid ${ this.imports.env.ui.scheme == 'dark' ? 'white' : 'black' } !important ;`
138
+ + `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' } !important ;`
145
139
  + 'padding: 8px !important ; min-width: 102px }' // resize
146
140
  + `.${this.class} button:hover {` // add zoom, re-scheme
147
141
  + '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 }`
142
+ + `background-color: #${ scheme == 'dark' ? '00cfff' : '9cdaff' } !important }`
143
+ + ( !isMobile ? `.${this.class} .modal-buttons { margin-left: -13px !important }` : '' )
144
+ + `.about-em { color: ${ scheme == 'dark' ? 'white' : 'green' } !important }`
152
145
  )
153
146
  }
154
147
  };
@@ -18,15 +18,13 @@
18
18
  dom.import({ env }) // for env.ui.scheme
19
19
  modals.import({ app, env }) // for app data + env.<browser|ui> flags
20
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)
21
+ chrome.runtime.onMessage.addListener(({ action, options }) => { // from service-worker.js + popup/index.html
22
+ ({
23
+ notify: () => notify(...['msg', 'pos', 'notifDuration', 'shadow'].map(arg => options[arg])),
24
+ alert: () => modals.alert(...['title', 'msg', 'btns', 'checkbox', 'width'].map(arg => options[arg])),
25
+ showAbout: () => { config.skipAlert = true ; chatgpt.isLoaded().then(() => modals.open('about')) },
26
+ syncConfigToUI: () => syncConfigToUI(options)
27
+ }[action]?.())
30
28
  })
31
29
 
32
30
  // Init SETTINGS
@@ -92,7 +90,8 @@
92
90
  // Add RISING PARTICLES styles for modals
93
91
  ['gray', 'white'].forEach(color => document.head.append(
94
92
  dom.create.elem('link', { rel: 'stylesheet',
95
- href: `https://assets.aiwebextensions.com/styles/rising-particles/dist/${color}.min.css?v=727feff`
93
+ href: `https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@727feff/assets/styles/rising-particles/dist/${
94
+ color}.min.css`
96
95
  })))
97
96
 
98
97
  if (config.extensionDisabled) return
@@ -24,24 +24,34 @@ const chatgpt = {
24
24
 
25
25
  selectors: {
26
26
  btns: {
27
- continue: 'button.btn:has([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:has([d^="M3.06957"])', scroll: 'button:has([d^="M12 21C11.7348"])',
32
- send: '[data-testid=send-button]', sidebar: 'button[data-testid*=sidebar-button]',
33
- 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]'
34
44
  },
35
45
  chatDivs: {
36
- convo: 'main > div > div > div > div > div > div[class*=group]',
37
- 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]'
38
48
  },
39
- chatHistory: 'nav',
40
- errors: { txt: '[class*=text-error]' },
41
- footer: '.min-h-4',
42
- 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',
43
53
  links: { newChat: 'nav a[href="/"]', sidebarItem: 'nav a' },
44
- sidebar: 'div[class*=sidebar]',
54
+ sidebar: 'div[class*=sidebar]:has(nav > div#sidebar-header)',
45
55
  ssgManifest: 'script[src*="_ssgManifest.js"]'
46
56
  },
47
57
 
@@ -122,7 +132,8 @@ const chatgpt = {
122
132
  chatgpt.draggingModal = event.currentTarget
123
133
  event.preventDefault() // prevent sub-elems like icons being draggable
124
134
  Object.assign(chatgpt.draggingModal.style, {
125
- cursor: 'grabbing', transition: '0.1s', willChange: 'transform', transform: 'scale(1.05)' });
135
+ transition: '0.1s', willChange: 'transform', transform: 'scale(1.05)' })
136
+ document.body.style.cursor = 'grabbing'; // update cursor
126
137
  [...chatgpt.draggingModal.children] // prevent hover FX if drag lags behind cursor
127
138
  .forEach(child => child.style.pointerEvents = 'none');
128
139
  ['mousemove', 'mouseup'].forEach(eventType => // add listeners
@@ -141,7 +152,8 @@ const chatgpt = {
141
152
 
142
153
  mouseup() { // restore styles/pointer events, remove listeners, reset chatgpt.draggingModal
143
154
  Object.assign(chatgpt.draggingModal.style, { // restore styles
144
- cursor: 'inherit', transition: 'inherit', willChange: 'auto', transform: 'scale(1)' });
155
+ cursor: 'inherit', transition: 'inherit', willChange: 'auto', transform: 'scale(1)' })
156
+ document.body.style.cursor = ''; // restore cursor
145
157
  [...chatgpt.draggingModal.children] // restore pointer events
146
158
  .forEach(child => child.style.pointerEvents = '');
147
159
  ['mousemove', 'mouseup'].forEach(eventType => // remove listeners
@@ -295,7 +307,7 @@ const chatgpt = {
295
307
  // Create/show label
296
308
  const checkboxLabel = document.createElement('label')
297
309
  checkboxLabel.onclick = () => { checkboxInput.checked = !checkboxInput.checked ; checkboxFn() }
298
- checkboxLabel.textContent = checkboxFn.name.charAt(0).toUpperCase() // capitalize first char
310
+ checkboxLabel.textContent = checkboxFn.name[0].toUpperCase() // capitalize first char
299
311
  + checkboxFn.name.slice(1) // format remaining chars
300
312
  .replace(/([A-Z])/g, (match, letter) => ' ' + letter.toLowerCase()) // insert spaces, convert to lowercase
301
313
  .replace(/\b(\w+)nt\b/gi, '$1n\'t') // insert apostrophe in 'nt' suffixes
@@ -644,7 +656,7 @@ const chatgpt = {
644
656
  const msgs = [] ; let isUserMsg = true
645
657
  chatDivs.forEach(div => {
646
658
  const sender = isUserMsg ? 'USER' : 'CHATGPT'; isUserMsg = !isUserMsg
647
- const msg = Array.from(div.childNodes).map(node => node.innerText)
659
+ const msg = [...div.childNodes].map(node => node.innerText)
648
660
  .join('\n\n') // insert double line breaks between paragraphs
649
661
  .replace('Copy code', '')
650
662
  msgs.push(`${sender}: ${msg}`)
@@ -1146,7 +1158,7 @@ const chatgpt = {
1146
1158
  }
1147
1159
  },
1148
1160
 
1149
- isDarkMode() { return document.documentElement.className.includes('dark') },
1161
+ isDarkMode() { return document.documentElement.classList.contains('dark') },
1150
1162
  isFullScreen() { return chatgpt.browser.isFullScreen() },
1151
1163
 
1152
1164
  async isIdle(timeout = null) {
@@ -1187,7 +1199,7 @@ const chatgpt = {
1187
1199
  return await ( timeoutPromise ? Promise.race([isLoadedPromise, timeoutPromise]) : isLoadedPromise )
1188
1200
  },
1189
1201
 
1190
- isLightMode() { return document.documentElement.classList.toString().includes('light') },
1202
+ isLightMode() { return document.documentElement.classList.contains('light') },
1191
1203
  isTempChat() { return location.search == '?temporary-chat=true' },
1192
1204
  isTyping() { return !!this.getStopButton() },
1193
1205
  login() { window.location.href = 'https://chat.openai.com/auth/login' },
@@ -1363,7 +1375,7 @@ const chatgpt = {
1363
1375
  for (const divId of thisQuadrantQueue.slice(0, -1)) { // exclude new div
1364
1376
  const oldDiv = document.getElementById(divId),
1365
1377
  offsetProp = oldDiv.style.top ? 'top' : 'bottom', // pick property to change
1366
- vOffset = +/\d+/.exec(oldDiv.style[offsetProp])[0] + 5 + oldDiv.getBoundingClientRect().height
1378
+ vOffset = +parseInt(oldDiv.style[offsetProp]) +5 + oldDiv.getBoundingClientRect().height
1367
1379
  oldDiv.style[offsetProp] = `${ vOffset }px` // change prop
1368
1380
  }
1369
1381
  } catch (err) {}
@@ -1494,7 +1506,7 @@ const chatgpt = {
1494
1506
  // Process text node
1495
1507
  if (childNode.nodeType == Node.TEXT_NODE) {
1496
1508
  const text = childNode.nodeValue,
1497
- elems = Array.from(text.matchAll(reTags))
1509
+ elems = [...text.matchAll(reTags)]
1498
1510
 
1499
1511
  // Process 1st element to render
1500
1512
  if (elems.length > 0) {
@@ -1503,7 +1515,7 @@ const chatgpt = {
1503
1515
  tagNode = document.createElement(tagName) ; tagNode.textContent = tagText
1504
1516
 
1505
1517
  // Extract/set attributes
1506
- const attrs = Array.from(tagAttrs.matchAll(reAttrs))
1518
+ const attrs = [...tagAttrs.matchAll(reAttrs)]
1507
1519
  attrs.forEach(attr => {
1508
1520
  const name = attr[1], value = attr[2].replace(/['"]/g, '')
1509
1521
  tagNode.setAttribute(name, value)
@@ -1539,9 +1551,8 @@ const chatgpt = {
1539
1551
  // responseToGet = index of response to get (defaults to latest if '' unpassed)
1540
1552
  // regenResponseToGet = index of regenerated response to get (defaults to latest if '' unpassed)
1541
1553
 
1542
- if (window.location.href.startsWith('https://chatgpt.com/c/'))
1543
- return this.getFromDOM.apply(null, arguments)
1544
- else return this.getFromAPI.apply(null, arguments)
1554
+ return this[`getFrom${ location.href.startsWith('https://chatgpt.com/c/') ? 'DOM' : 'API' }`]
1555
+ .apply(null, arguments)
1545
1556
  },
1546
1557
 
1547
1558
  getFromAPI(chatToGet, responseToGet) {
@@ -1602,7 +1613,7 @@ const chatgpt = {
1602
1613
  const textArea = chatgpt.getChatBox()
1603
1614
  if (!textArea) return console.error('Chatbar element not found!')
1604
1615
  const msgP = document.createElement('p'); msgP.textContent = msg
1605
- textArea.replaceChild(msgP, textArea.querySelector('p'))
1616
+ textArea.querySelector('p').replaceWith(msgP)
1606
1617
  textArea.dispatchEvent(new Event('input', { bubbles: true })) // enable send button
1607
1618
  setTimeout(function delaySend() {
1608
1619
  const sendBtn = chatgpt.getSendButton()
@@ -1851,10 +1862,11 @@ const chatgpt = {
1851
1862
  hide() { this.isOn() ? this.toggle() : console.info('Sidebar already hidden!') },
1852
1863
  show() { this.isOff() ? this.toggle() : console.info('Sidebar already shown!') },
1853
1864
  isOff() { return !this.isOn() },
1865
+
1854
1866
  isOn() {
1855
1867
  const sidebar = (() => {
1856
1868
  return chatgpt.sidebar.exists() ? document.querySelector(chatgpt.selectors.sidebar) : null })()
1857
- if (!sidebar) { console.error('Sidebar element not found!'); return false }
1869
+ if (!sidebar) { return console.error('Sidebar element not found!') || false }
1858
1870
  else return chatgpt.browser.isMobile() ?
1859
1871
  document.documentElement.style.overflow == 'hidden'
1860
1872
  : sidebar.style.visibility != 'hidden' && sidebar.style.width != '0px'
@@ -2089,11 +2101,10 @@ const cjsFuncSynonyms = [
2089
2101
  } while (aliasFuncCreated) // loop over new functions to encompass all variations
2090
2102
  })()
2091
2103
 
2092
-
2093
2104
  // Define HELPER functions
2094
2105
 
2095
2106
  function toCamelCase(words) {
2096
- 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('') }
2097
2108
 
2098
2109
  // Prefix console logs w/ '🤖 chatgpt.js >> '
2099
2110
  const consolePrefix = '🤖 chatgpt.js >> ', ogError = console.error, ogInfo = console.info
@@ -93,8 +93,9 @@ 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
96
+ loadedElem(selector, { timeout = null } = {}) {
97
+ const timeoutPromise = new Promise(resolve =>
98
+ timeout ? setTimeout(() => resolve(null), timeout) : undefined)
98
99
  const isLoadedPromise = new Promise(resolve => {
99
100
  const elem = document.querySelector(selector)
100
101
  if (elem) resolve(elem)
@@ -103,7 +104,7 @@ window.dom = {
103
104
  if (elem) { obs.disconnect() ; resolve(elem) }
104
105
  }).observe(document.documentElement, { childList: true, subtree: true })
105
106
  })
106
- return ( timeoutPromise ? Promise.race([isLoadedPromise, timeoutPromise]) : isLoadedPromise )
107
+ return Promise.race([isLoadedPromise, timeoutPromise])
107
108
  }
108
109
  }
109
110
  };
@@ -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
6
  // - 'type': the control type (e.g. 'toggle' 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
  };
@@ -3,21 +3,13 @@
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": "2025.2.21",
6
+ "version": "2025.4.28",
7
7
  "author": "KudoAI",
8
8
  "homepage_url": "https://github.com/KudoAI/chatgpt.js-chrome-starter",
9
- "icons": {
10
- "16": "icons/icon16.png",
11
- "32": "icons/icon32.png",
12
- "64": "icons/icon64.png",
13
- "128": "icons/icon128.png"
14
- },
9
+ "icons": { "16": "icons/icon16.png", "32": "icons/icon32.png", "64": "icons/icon64.png", "128": "icons/icon128.png" },
15
10
  "permissions": [ "activeTab", "storage" ],
16
11
  "action": { "default_popup": "popup/index.html" },
17
- "web_accessible_resources": [{
18
- "matches": [ "<all_urls>" ],
19
- "resources": [ "components/modals.js", "lib/chatgpt.js", "lib/dom.js", "lib/settings.js" ]
20
- }],
12
+ "web_accessible_resources": [{ "matches": [ "<all_urls>" ], "resources": [ "components/*", "lib/*" ]}],
21
13
  "content_scripts": [{ "matches": [ "https://chatgpt.com/*" ], "js": [ "content.js" ] }],
22
14
  "background": { "service_worker": "service-worker.js" },
23
15
  "minimum_chrome_version": "88"