@kudoai/chatgpt.js 3.8.4 → 3.9.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,16 +1,17 @@
1
1
  // NOTE: This script relies on the powerful chatgpt.js library @ https://chatgpt.js.org
2
- // © 2023–2025 KudoAI & contributors under the MIT license
2
+ // © 2023–2026 KudoAI & contributors under the MIT license
3
3
 
4
4
  (async () => {
5
5
 
6
6
  // Import JS resources
7
7
  for (const resource of [
8
- 'components/modals.js', 'lib/chatgpt.js', 'lib/dom.js', 'lib/feedback.js', 'lib/settings.js', 'lib/ui.js'
8
+ 'components/modals.js', 'lib/chatgpt.js', 'lib/css.min.js', 'lib/dom.min.js', 'lib/feedback.js',
9
+ 'lib/settings.js', 'lib/ui.js'
9
10
  ]) await import(chrome.runtime.getURL(resource))
10
11
 
11
12
  // Init ENV context
12
13
  window.env = { browser: { isMobile: chatgpt.browser.isMobile() }, ui: { scheme: ui.getScheme() }}
13
- env.browser.isPortrait = env.browser.isMobile && (window.innerWidth < window.innerHeight)
14
+ Object.assign(env.browser, { get isCompact() { return innerWidth <= 480 }})
14
15
 
15
16
  // Import APP data
16
17
  ;({ app: window.app } = await chrome.storage.local.get('app'))
@@ -21,7 +22,7 @@
21
22
  alert: () => modals.alert(...['title', 'msg', 'btns', 'checkbox', 'width'].map(arg => options[arg])),
22
23
  showAbout: () => {
23
24
  if (!source?.endsWith('service-worker.js')) return
24
- config.skipAlert = true
25
+ app.config.skipAlert = true
25
26
  chatgpt.isLoaded().then(() => modals.open('about'))
26
27
  },
27
28
  syncConfigToUI: () => syncConfigToUI(options)
@@ -30,20 +31,20 @@
30
31
 
31
32
  // Init SETTINGS
32
33
  await settings.load(Object.keys(settings.controls))
33
- if (!config.skipAlert) await settings.load('skipAlert') // only if not showing About modal
34
+ if (!app.config.skipAlert) await settings.load('skipAlert') // only if not showing About modal
34
35
 
35
36
  // Define FUNCTIONS
36
37
 
37
38
  async function syncConfigToUI(options = {}) { // eslint-disable-line
38
- await settings.load('extensionDisabled', Object.keys(settings.controls)) // load from Chrome storage to content.js config
39
- if (config.extensionDisabled) {
39
+ await settings.load('extensionDisabled', Object.keys(settings.controls)) // load from Chrome storage to app.config
40
+ if (app.config.extensionDisabled) {
40
41
  // Remove all hacks
41
42
  } else {
42
43
  // Add/remove hacks to reflect each potentially updated setting per settings.controls in lib/settings.mjs
43
44
  // e.g. if you created toolbar popup toggle to hide ChatGPT footer using hiddenFooter key...
44
- // ...here you would use options.updatedKey == 'hiddenFooter' && config.hiddenFooter...
45
+ // ...here you would use options.updatedKey == 'hiddenFooter' && app.config.hiddenFooter...
45
46
  // ...to conditionally append/remove hidden footer style...
46
- // ...(initial style creation + append if config.hiddenFooter would go in main routine)
47
+ // ...(initial style creation + append if app.config.hiddenFooter would go in main routine)
47
48
  }
48
49
  }
49
50
 
@@ -62,15 +63,15 @@
62
63
  color}.min.css`
63
64
  })))
64
65
 
65
- if (config.extensionDisabled) return
66
+ if (app.config.extensionDisabled) return
66
67
 
67
- if (!config.skipAlert) // alert to extension load
68
+ if (!app.config.skipAlert) // alert to extension load
68
69
  modals.alert('≫ ChatGPT extension loaded! 🚀', // title
69
70
  'Success! Press Ctrl+Shift+J to view all chatgpt.js methods.', // msg
70
71
  function getHelp() { // button
71
72
  open(`${app.urls.github}/issues`) },
72
73
  function dontShowAgain() { // checkbox
73
- settings.save('skipAlert', !config.skipAlert) }
74
+ settings.save('skipAlert', !app.config.skipAlert) }
74
75
  )
75
76
 
76
77
  // Monitor SCHEME PREF CHANGES to update modal colors + env.ui.scheme for your use
@@ -1,12 +1,8 @@
1
- // © 2023–2025 KudoAI & contributors under the MIT license.
1
+ // © 2023–2026 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
5
5
 
6
- // Init feedback props
7
- localStorage.alertQueue = JSON.stringify([])
8
- localStorage.notifyProps = JSON.stringify({ queue: { topRight: [], bottomRight: [], bottomLeft: [], topLeft: [] }})
9
-
10
6
  // Define chatgpt API
11
7
  const chatgpt = {
12
8
 
@@ -25,21 +21,21 @@ const chatgpt = {
25
21
  selectors: {
26
22
  btns: {
27
23
  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]',
24
+ createImage: 'button[data-testid=composer-button-create-image]',
30
25
  login: 'button[data-testid*=login]',
31
26
  newChat: 'a[href="/"]:has(svg),' // Pencil button (when logged in)
32
27
  + 'button:has([d^="M3.06957"])', // Cycle Arrows button (in temp chat logged out)
33
28
  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"])',
29
+ + 'button:has(use[href$="sprites-core-k5zux585.svg#ec66f0"])', // 'Try again...' button below msg
30
+ scroll: 'button:has(use[href$="sprites-core-k5zux585.svg#ac89a7"])',
37
31
  search: 'button[data-testid=composer-button-search]',
38
- reason: 'button[data-testid=composer-button-reason]',
39
32
  send: 'button[data-testid=send-button]',
40
- sidebar: 'div[style*=-sidebar-width] button[data-testid=close-sidebar-button], div[style*=-sidebar-rail-width] button[aria-controls=stage-slideover-sidebar]',
41
- stop: 'div:has(> svg > path[d^="M10 2.08496C14.3713"])',
42
- upload: 'button:has(> svg > path[d^="M12 3C12.5523"])',
33
+ sidebar: 'div[style*=-sidebar-width] button[data-testid=close-sidebar-button],'
34
+ + 'div[style*=-sidebar-rail-width] button[aria-controls=stage-slideover-sidebar]',
35
+ signup: 'button[data-testid=signup-button]',
36
+ stop: 'button[data-testid=stop-button]',
37
+ upload: 'div[data-testid=composer-action-file-upload],' // tall chatbar
38
+ + 'button#composer-plus-btn', // short chatbar
43
39
  voice: 'button[data-testid*=composer-speech-button]'
44
40
  },
45
41
  chatDivs: {
@@ -338,7 +334,7 @@ const chatgpt = {
338
334
  modalContainer.append(modal) ; document.body.append(modalContainer)
339
335
 
340
336
  // Enqueue alert
341
- let alertQueue = JSON.parse(localStorage.alertQueue)
337
+ let alertQueue = JSON.parse(localStorage.alertQueue ??= JSON.stringify([]))
342
338
  alertQueue.push(modalContainer.id)
343
339
  localStorage.alertQueue = JSON.stringify(alertQueue)
344
340
 
@@ -371,7 +367,7 @@ const chatgpt = {
371
367
  document.removeEventListener('keydown', handlers.dismiss.key) // prevent memory leaks
372
368
 
373
369
  // Check for pending alerts in queue
374
- if (alertQueue.length > 0) {
370
+ if (alertQueue.length) {
375
371
  const nextAlert = document.getElementById(alertQueue[0])
376
372
  setTimeout(() => {
377
373
  nextAlert.style.display = ''
@@ -409,7 +405,7 @@ const chatgpt = {
409
405
  }
410
406
  scheduleRefreshes(interval)
411
407
  }, (interval + randomDelay) * 1000)
412
- };
408
+ }
413
409
  scheduleRefreshes( interval ? parseInt(interval, 10) : 30 )
414
410
  console.log(`↻ ChatGPT >> [${chatgpt.autoRefresh.nowTimeStamp()}] Auto refresh activated`)
415
411
 
@@ -432,7 +428,7 @@ const chatgpt = {
432
428
  const now = new Date()
433
429
  const hours = now.getHours() % 12 || 12 // convert to 12h format
434
430
  let minutes = now.getMinutes(), seconds = now.getSeconds()
435
- if (minutes < 10) minutes = '0' + minutes; if (seconds < 10) seconds = '0' + seconds
431
+ if (minutes < 10) minutes = '0' + minutes ; if (seconds < 10) seconds = '0' + seconds
436
432
  const meridiem = now.getHours() < 12 ? 'AM' : 'PM'
437
433
  return `${hours}:${minutes}:${seconds} ${meridiem}`
438
434
  },
@@ -574,7 +570,7 @@ const chatgpt = {
574
570
 
575
571
  async refactor(code, objective) {
576
572
  if (!code) return console.error('Code (1st) argument not supplied. Pass some code!')
577
- for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] != 'string')
573
+ for (let i = 0 ; i < arguments.length ; i++) if (typeof arguments[i] != 'string')
578
574
  return console.error(`Argument ${ i + 1 } must be a string.`)
579
575
  chatgpt.send(`Refactor the following code for ${ objective || 'brevity' }:\n\n${code}`)
580
576
  console.info('Refactoring code...')
@@ -603,7 +599,7 @@ const chatgpt = {
603
599
  async write(prompt, outputLang) {
604
600
  if (!prompt) return console.error('Prompt (1st) argument not supplied. Pass a prompt!')
605
601
  if (!outputLang) return console.error('outputLang (2nd) argument not supplied. Pass a language!')
606
- for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] != 'string')
602
+ for (let i = 0 ; i < arguments.length ; i++) if (typeof arguments[i] != 'string')
607
603
  return console.error(`Argument ${ i + 1 } must be a string.`)
608
604
  chatgpt.send(`${prompt}\n\nWrite this as code in ${outputLang}`)
609
605
  console.info('Writing code...')
@@ -657,7 +653,7 @@ const chatgpt = {
657
653
  if (!chatDivs.length) return console.error('Chat is empty!')
658
654
  const msgs = [] ; let isUserMsg = true
659
655
  chatDivs.forEach(div => {
660
- const sender = isUserMsg ? 'USER' : 'CHATGPT'; isUserMsg = !isUserMsg
656
+ const sender = isUserMsg ? 'USER' : 'CHATGPT' ; isUserMsg = !isUserMsg
661
657
  const msg = [...div.childNodes].map(node => node.innerText)
662
658
  .join('\n\n') // insert double line breaks between paragraphs
663
659
  .replace('Copy code', '')
@@ -686,7 +682,7 @@ const chatgpt = {
686
682
  cssLinks.forEach(link => {
687
683
  const href = link.getAttribute('href')
688
684
  if (href?.startsWith('/')) link.setAttribute('href', 'https://chat.openai.com' + href)
689
- });
685
+ })
690
686
 
691
687
  // Serialize updated HTML to string
692
688
  transcript = new XMLSerializer().serializeToString(parsedHtml)
@@ -711,7 +707,7 @@ const chatgpt = {
711
707
 
712
708
  if (format == 'md') { // remove extraneous HTML + fix file extension
713
709
  const mdMatch = /<.*<h1(.|\n)*?href=".*?continue[^"]*".*?\/a>.*?<[^/]/.exec(transcript)[1]
714
- transcript = mdMatch || transcript; filename = filename.replace('.html', '.md')
710
+ transcript = mdMatch || transcript ; filename = filename.replace('.html', '.md')
715
711
  }
716
712
  const blob = new Blob([transcript],
717
713
  { type: 'text/' + ( format == 'html' ? 'html' : format == 'md' ? 'markdown' : 'plain' )})
@@ -769,7 +765,7 @@ const chatgpt = {
769
765
  }}
770
766
  if (!targetNames.includes(targetName.toLowerCase()))
771
767
  throw new Error(`Invalid targetName: ${targetName}. `
772
- + (targetNames.length > 0 ? 'Valid values are: ' + JSON.stringify(targetNames)
768
+ + (targetNames.length ? 'Valid values are: ' + JSON.stringify(targetNames)
773
769
  : 'targetType ' + targetType.toLowerCase() + ' does not require additional options.'))
774
770
 
775
771
  // Call target function using pre-validated name components
@@ -927,7 +923,7 @@ const chatgpt = {
927
923
 
928
924
  // Fill [userMessages]
929
925
  for (const key in data)
930
- if (data[key].message != null && data[key].message.author.role == 'user')
926
+ if (data[key]?.message?.author?.role == 'user')
931
927
  userMessages.push({ id: data[key].id, msg: data[key].message })
932
928
  userMessages.sort((a, b) => a.msg.create_time - b.msg.create_time) // sort in chronological order
933
929
 
@@ -938,12 +934,10 @@ const chatgpt = {
938
934
  // Fill [chatGPTMessages]
939
935
  for (const userMessage of userMessages) {
940
936
  let sub = []
941
- for (const key in data) {
942
- if (data[key].message != null && data[key].message.author.role == 'assistant'
943
- && isUserMsgAncestor(key, userMessage.id)) {
944
- sub.push(data[key].message)
945
- }
946
- }
937
+ for (const key in data)
938
+ if (data[key]?.message?.author?.role == 'assistant'
939
+ && isUserMsgAncestor(key, userMessage.id)
940
+ ) sub.push(data[key].message)
947
941
  sub.sort((a, b) => a.create_time - b.create_time) // sort in chronological order
948
942
  sub = sub.map(x => { // pull out msgs after sorting
949
943
  switch(x.content.content_type) {
@@ -961,7 +955,8 @@ const chatgpt = {
961
955
  msgsToReturn.push(userMessages[userMessage].msg.content.parts[0])
962
956
  else if (sender == 'chatgpt') // Fill [msgsToReturn] with ChatGPT responses
963
957
  for (const chatGPTMessage of chatGPTMessages)
964
- msgsToReturn.push(msgToGet == 'latest' ? chatGPTMessages[chatGPTMessages.length - 1] : chatGPTMessage );
958
+ msgsToReturn.push(msgToGet == 'latest' ? chatGPTMessages[chatGPTMessages.length - 1]
959
+ : chatGPTMessage )
965
960
  else { // Fill [msgsToReturn] with objects of user messages and chatgpt response(s)
966
961
  let i = 0
967
962
  for (const message in userMessages) {
@@ -1077,9 +1072,9 @@ const chatgpt = {
1077
1072
  else if (target == 'chatgpt') instructionsData.about_model_message += instruction
1078
1073
 
1079
1074
  await this.sendRequest('POST', token, instructionsData)
1080
- return resolve();
1081
- });
1082
- });
1075
+ return resolve()
1076
+ })
1077
+ })
1083
1078
  },
1084
1079
 
1085
1080
  clear(target) {
@@ -1134,7 +1129,7 @@ const chatgpt = {
1134
1129
  xhr.onload = () => {
1135
1130
  const responseData = JSON.parse(xhr.responseText)
1136
1131
  if (xhr.status == 422)
1137
- return reject('🤖 chatgpt.js >> Character limit exceeded. Custom instructions can have a maximum length of 1500 characters.');
1132
+ return reject('🤖 chatgpt.js >> Character limit exceeded. Custom instructions can have a maximum length of 1500 characters.')
1138
1133
  else if (xhr.status == 403 && responseData.detail.reason == 'content_policy')
1139
1134
  return reject('🤖 chatgpt.js >> ' + responseData.detail.description)
1140
1135
  else if (xhr.status != 200)
@@ -1259,7 +1254,7 @@ const chatgpt = {
1259
1254
  if (!attrs.items.every(el => typeof el == 'object')) // the entries of the array are not objects
1260
1255
  return console.error('\'items\' must be an array of objects!')
1261
1256
 
1262
- newElem.style = 'background-color: #000; width: 100%; border: none;'
1257
+ newElem.style = 'background-color: #000 ; width: 100% ; border: none'
1263
1258
 
1264
1259
  attrs.items.forEach(item => {
1265
1260
  const optionElement = document.createElement('option')
@@ -1302,7 +1297,7 @@ const chatgpt = {
1302
1297
  }
1303
1298
  },
1304
1299
 
1305
- minify() { chatgpt.code.minify(); },
1300
+ minify() { chatgpt.code.minify() },
1306
1301
 
1307
1302
  notify(...args) {
1308
1303
  const scheme = chatgpt.isDarkMode() ? 'dark' : 'light'
@@ -1310,7 +1305,7 @@ const chatgpt = {
1310
1305
  if (typeof args[0] == 'object' && !Array.isArray(args[0]))
1311
1306
  ({ msg, position, notifDuration, shadow, toast } = args[0])
1312
1307
  else [msg, position, notifDuration, shadow] = args
1313
- notifDuration = notifDuration ? +notifDuration : 1.75; // sec duration to maintain notification visibility
1308
+ notifDuration = notifDuration ? +notifDuration : 1.75 // sec duration to maintain notification visibility
1314
1309
  const fadeDuration = 0.35, // sec duration of fade-out
1315
1310
  vpYoffset = 23, vpXoffset = 27 // px offset from viewport border
1316
1311
 
@@ -1323,7 +1318,7 @@ const chatgpt = {
1323
1318
 
1324
1319
  // Create/append close button
1325
1320
  const closeBtn = document.createElement('div')
1326
- closeBtn.title = 'Dismiss'; closeBtn.classList.add('notif-close-btn', 'no-mobile-tap-outline')
1321
+ closeBtn.title = 'Dismiss' ; closeBtn.classList.add('notif-close-btn', 'no-mobile-tap-outline')
1327
1322
  const closeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
1328
1323
  closeSVG.setAttribute('height', '8px')
1329
1324
  closeSVG.setAttribute('viewBox', '0 0 14 14')
@@ -1333,7 +1328,7 @@ const chatgpt = {
1333
1328
  closeSVGpath.setAttribute('fill-rule', 'evenodd')
1334
1329
  closeSVGpath.setAttribute('clip-rule', 'evenodd')
1335
1330
  closeSVGpath.setAttribute('fill', 'white')
1336
- closeSVGpath.setAttribute('d', 'M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.0976312 12.6834 -0.0976312 12.2929 0.292893L7 5.58579L1.70711 0.292893C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292893C-0.0976312 0.683417 -0.0976312 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976312 12.6834 -0.0976312 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z');
1331
+ closeSVGpath.setAttribute('d', 'M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.0976312 12.6834 -0.0976312 12.2929 0.292893L7 5.58579L1.70711 0.292893C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292893C-0.0976312 0.683417 -0.0976312 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976312 12.6834 -0.0976312 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z')
1337
1332
  closeSVG.append(closeSVGpath) ; closeBtn.append(closeSVG) ; notificationDiv.append(closeBtn)
1338
1333
 
1339
1334
  // Determine div position/quadrant
@@ -1384,7 +1379,8 @@ const chatgpt = {
1384
1379
  }
1385
1380
 
1386
1381
  // Enqueue notification
1387
- let notifyProps = JSON.parse(localStorage.notifyProps)
1382
+ let notifyProps = JSON.parse(localStorage.notifyProps
1383
+ ??= JSON.stringify({ queue: { topRight: [], bottomRight: [], bottomLeft: [], topLeft: [] }}))
1388
1384
  notifyProps.queue[notificationDiv.quadrant].push(notificationDiv.id)
1389
1385
  localStorage.notifyProps = JSON.stringify(notifyProps)
1390
1386
 
@@ -1420,7 +1416,7 @@ const chatgpt = {
1420
1416
 
1421
1417
  // Add notification dismissal to timeout schedule + button clicks
1422
1418
  const dismissNotif = () => {
1423
- notificationDiv.style.animation = `notif-zoom-fade-out ${fadeDuration}s ease-out`;
1419
+ notificationDiv.style.animation = `notif-zoom-fade-out ${fadeDuration}s ease-out`
1424
1420
  clearTimeout(dismissFuncTID)
1425
1421
  }
1426
1422
  const dismissFuncTID = setTimeout(dismissNotif, hideDelay * 1000) // maintain visibility for `hideDelay` secs, then dismiss
@@ -1472,7 +1468,7 @@ const chatgpt = {
1472
1468
 
1473
1469
  // Print methods
1474
1470
  const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches,
1475
- baseFontStyles = 'font-family: monospace ; font-size: larger ; '
1471
+ baseFontStyles = 'font-family: monospace ; font-size: larger ;'
1476
1472
  console.log('\n%c🤖 chatgpt.js methods\n', 'font-family: sans-serif ; font-size: xxx-large ; font-weight: bold')
1477
1473
  for (const functionName of functionNames) {
1478
1474
  const isChatGptObjParent = /chatgpt|other/.test(functionName[0]),
@@ -1535,7 +1531,7 @@ const chatgpt = {
1535
1531
  elems = [...text.matchAll(reTags)]
1536
1532
 
1537
1533
  // Process 1st element to render
1538
- if (elems.length > 0) {
1534
+ if (elems.length) {
1539
1535
  const elem = elems[0],
1540
1536
  [tagContent, tagName, tagAttrs, tagText] = elem.slice(0, 4),
1541
1537
  tagNode = document.createElement(tagName) ; tagNode.textContent = tagText
@@ -1585,7 +1581,7 @@ const chatgpt = {
1585
1581
  // chatToGet = index|title|id of chat to get (defaults to latest if '' or unpassed)
1586
1582
  // responseToGet = index of response to get (defaults to latest if '' or unpassed)
1587
1583
 
1588
- chatToGet = chatToGet || 'latest'; responseToGet = responseToGet || 'latest'
1584
+ chatToGet = chatToGet || 'latest' ; responseToGet = responseToGet || 'latest'
1589
1585
  return chatgpt.getChatData(chatToGet, 'msg', 'chatgpt', responseToGet)
1590
1586
  },
1591
1587
 
@@ -1638,15 +1634,15 @@ const chatgpt = {
1638
1634
  return console.error(`Argument ${ i + 1 } must be a string!`)
1639
1635
  const textArea = chatgpt.getChatBox()
1640
1636
  if (!textArea) return console.error('Chatbar element not found!')
1641
- const msgP = document.createElement('p'); msgP.textContent = msg
1637
+ const msgP = document.createElement('p') ; msgP.textContent = msg
1642
1638
  textArea.querySelector('p').replaceWith(msgP)
1643
1639
  textArea.dispatchEvent(new Event('input', { bubbles: true })) // enable send button
1644
1640
  setTimeout(function delaySend() {
1645
1641
  const sendBtn = chatgpt.getSendButton()
1646
- if (!sendBtn?.hasAttribute('disabled')) { // send msg
1642
+ if (!sendBtn?.hasAttribute('disabled')) // send msg
1647
1643
  method.toLowerCase() == 'click' || chatgpt.browser.isMobile() ? sendBtn.click()
1648
1644
  : textArea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
1649
- } else setTimeout(delaySend, 222)
1645
+ else setTimeout(delaySend, 222)
1650
1646
  }, 222)
1651
1647
  },
1652
1648
 
@@ -1688,7 +1684,7 @@ const chatgpt = {
1688
1684
  },
1689
1685
 
1690
1686
  async sentiment(text, entity) {
1691
- for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] != 'string')
1687
+ for (let i = 0 ; i < arguments.length ; i++) if (typeof arguments[i] != 'string')
1692
1688
  return console.error(`Argument ${ i + 1 } must be a string.`)
1693
1689
  chatgpt.send('What is the sentiment of the following text'
1694
1690
  + ( entity ? ` towards the entity ${entity},` : '')
@@ -1840,15 +1836,13 @@ const chatgpt = {
1840
1836
  return console.error(`🤖 chatgpt.js >> Invalid element! Valid elems are [${validElems}]`)
1841
1837
 
1842
1838
  const newElem = document.createElement(elem == 'dropdown' ? 'select' : elem)
1843
- newElem.id = Math.floor(chatgpt.randomFloat() * 1000000) + Date.now() // Add random id to the element
1839
+ newElem.id = Math.floor(chatgpt.randomFloat() * 1000000) + Date.now()
1844
1840
 
1845
1841
  if (elem == 'button') {
1846
- newElem.textContent = attrs?.label && typeof attrs.label == 'string'
1847
- ? attrs.label
1848
- : 'chatgpt.js button'
1842
+ newElem.textContent = attrs?.label && typeof attrs.label == 'string' ? attrs.label : 'chatgpt.js button'
1849
1843
  const icon = document.createElement('img')
1850
- icon.src = attrs?.icon && typeof attrs.icon == 'string' // Can also be base64 encoded image string
1851
- ? attrs.icon // Add icon to button element if given, else default one
1844
+ icon.src = attrs?.icon && typeof attrs.icon == 'string' // can also be base64 encoded image string
1845
+ ? attrs.icon // add icon to button element if given, else default one
1852
1846
  : `${chatgpt.endpoints.assets}/starters/chrome/extension/icons/icon128.png`
1853
1847
  icon.width = 18
1854
1848
  newElem.firstChild.before(icon)
@@ -1856,9 +1850,9 @@ const chatgpt = {
1856
1850
  }
1857
1851
 
1858
1852
  else if (elem == 'dropdown') {
1859
- if (!attrs?.items || // There no are options to add
1853
+ if (!attrs?.items || // there no are options to add
1860
1854
  !Array.isArray(attrs.items) || // It's not an array
1861
- !attrs.items.length) // The array is empty
1855
+ !attrs.items.length) // the array is empty
1862
1856
  attrs.items = [{ text: '🤖 chatgpt.js option', value: 'chatgpt.js option value' }] // Set default dropdown entry
1863
1857
 
1864
1858
  if (!attrs.items.every(el => typeof el == 'object')) // The entries of the array are not objects
@@ -1883,7 +1877,7 @@ const chatgpt = {
1883
1877
  return newElem.id // Return the element id
1884
1878
  },
1885
1879
 
1886
- exists() { return !!chatgpt.getNewChatLink(); },
1880
+ exists() { return !!chatgpt.getNewChatLink() },
1887
1881
  hide() { this.isOn() ? this.toggle() : console.info('Sidebar already hidden!') },
1888
1882
  show() { this.isOff() ? this.toggle() : console.info('Sidebar already shown!') },
1889
1883
  isOff() { return !this.isOn() },
@@ -1917,12 +1911,12 @@ const chatgpt = {
1917
1911
  },
1918
1912
 
1919
1913
  startNewChat() { try { chatgpt.getNewChatBtn().click() } catch (err) { console.error(err.message) }},
1920
- stop() { chatgpt.response.stopGenerating(); },
1914
+ stop() { chatgpt.response.stopGenerating() },
1921
1915
 
1922
1916
  async suggest(ideaType, details) {
1923
1917
  if (!ideaType) return console.error('ideaType (1st argument) not supplied'
1924
1918
  + `(e.g. 'gifts', 'names', 'recipes', etc.)`)
1925
- for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] != 'string')
1919
+ for (let i = 0 ; i < arguments.length ; i++) if (typeof arguments[i] != 'string')
1926
1920
  return console.error(`Argument ${ i + 1 } must be a string.`)
1927
1921
  chatgpt.send('Suggest some names. ' + ( details || '' ))
1928
1922
  console.info(`Creating ${ideaType}...`)
@@ -1966,7 +1960,7 @@ const chatgpt = {
1966
1960
  return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest')
1967
1961
  },
1968
1962
 
1969
- toggleScheme() { chatgpt.settings.scheme.toggle(); },
1963
+ toggleScheme() { chatgpt.settings.scheme.toggle() },
1970
1964
 
1971
1965
  async translate(text, outputLang) {
1972
1966
  if (!text) return console.error('Text (1st) argument not supplied. Pass some text!')
@@ -1991,7 +1985,7 @@ const chatgpt = {
1991
1985
  const r = ( // generate random nibble
1992
1986
  ( d + (window.crypto.getRandomValues(new Uint32Array(1))[0] / (Math.pow(2, 32) - 1))*16)%16 | 0 )
1993
1987
  d = Math.floor(d/16) // correspond each UUID digit to unique 4-bit chunks of timestamp
1994
- return ( c == 'x' ? r : (r&0x3|0x8) ).toString(16); // generate random hexadecimal digit
1988
+ return ( c == 'x' ? r : (r&0x3|0x8) ).toString(16) // generate random hexadecimal digit
1995
1989
  })
1996
1990
  return uuid
1997
1991
  }
@@ -2092,8 +2086,8 @@ const cjsFuncSynonyms = [
2092
2086
  ['temp', 'temporary'],
2093
2087
  ['typing', 'generating'],
2094
2088
  ['unminify', 'beautify', 'prettify', 'prettyPrint']
2095
- ];
2096
- (function createCJSaliasFuncs(obj = chatgpt) {
2089
+ ]
2090
+ ;(function createCJSaliasFuncs(obj = chatgpt) {
2097
2091
  for (const prop in obj) {
2098
2092
  if (!Object.prototype.hasOwnProperty.call(obj, prop)) continue // skip inherited props
2099
2093
  if (typeof obj[prop] == 'object') createCJSaliasFuncs(obj[prop]) // recurse thru objs to find deeper functions
@@ -2134,11 +2128,13 @@ function toCamelCase(words) {
2134
2128
  // Prefix console logs w/ '🤖 chatgpt.js >> '
2135
2129
  const consolePrefix = '🤖 chatgpt.js >> ', ogError = console.error, ogInfo = console.info
2136
2130
  console.error = (...args) => {
2137
- if (!args[0].startsWith(consolePrefix)) ogError(consolePrefix + args[0], ...args.slice(1))
2138
- else ogError(...args)
2131
+ if (typeof args[0] == 'string') {
2132
+ if (!args[0].startsWith(consolePrefix)) ogError(consolePrefix + args[0], ...args.slice(1))
2133
+ else ogError(...args)
2134
+ } else ogError(consolePrefix, ...args)
2139
2135
  }
2140
2136
  console.info = (msg) => {
2141
- if (!msg.startsWith(consolePrefix)) ogInfo(consolePrefix + msg);
2137
+ if (!msg.startsWith(consolePrefix)) ogInfo(consolePrefix + msg)
2142
2138
  else ogInfo(msg)
2143
2139
  }
2144
2140
 
@@ -0,0 +1 @@
1
+ window.css={addRisingParticles(e,{lightScheme:i="gray",darkScheme:a="white"}={}){if(!e.querySelector("[id*=particles]")){let r=document.createElement("div");r.style.cssText="position: absolute ; top: 0 ; left: 0 ;height: 100% ; width: 100% ; border-radius: 15px ; overflow: clip ;z-index: -1",["sm","med","lg"].forEach(e=>{var t=document.createElement("div");t.id="undefined"!=typeof app&&app.config?.bgAnimationsDisabled?`particles-${e}-off`:`${"undefined"!=typeof env&&"dark"==(env.ui?.scheme||env.ui?.app?.scheme)?a:i}-particles-`+e,r.append(t)}),e.prepend(r)}},selectors:{extract(e,t="all"){if(!e||"object"!=typeof e)throw new TypeError("First parameter must be an object");var r=["all","css","xpath"];if(r.includes(t))return e=Object.values(e).flatMap(e=>e&&"object"==typeof e?this.extract(e,t):"string"==typeof e?[e]:[]),"css"==t?e.filter(e=>!e?.startsWith("//")):"xpath"==t?e.filter(e=>e?.startsWith("//")):e;throw new TypeError("Type must be one of: "+r.join(", "))},fromClasses(e){return e.toString().replace(/([:[\]\\])/g,"\\$1").replace(/^| /g,".")}}};
@@ -0,0 +1 @@
1
+ window.dom={create:{anchor(e,t,n={}){let r=document.createElement("a"),i={href:e,target:"_blank",rel:"noopener"},o={...i,...n};return Object.entries(o).forEach(([e,t])=>r.setAttribute(e,t)),t&&r.append(t),r},elem(e,t={}){var n,r=document.createElement(e);for(n in t)n in r?r[n]=t[n]:r.setAttribute(n,t[n]);return r},style(e,t={}){var n,r=document.createElement("style");for(n in r.setAttribute("type","text/css"),t)r.setAttribute(n,t[n]);return e&&(r.textContent=e),r},svgElem(e,t={}){var n,r=document.createElementNS("http://www.w3.org/2000/svg",e);for(n in t)r.setAttributeNS(null,n,t[n]);return r}},get:{computedSize(e,{dimension:t}={}){(e=e instanceof NodeList?[...e]:[].concat(e)).forEach(e=>{if(!(e instanceof Node))throw new Error(`Invalid elem: Element "${JSON.stringify(e)}" is not a valid DOM node`)});let n=["width","height"],r=[].concat(t||n),i=(r.forEach(e=>{if(!n.includes(e))throw new Error("Invalid dimension: Use 'width' or 'height'")}),{width:0,height:0});return e.forEach(t=>{let n=getComputedStyle(t);"none"!=n.display&&Object.keys(i).forEach(e=>{r.includes(e)&&(i[e]+=t.getBoundingClientRect()[e]+parseFloat(n["margin"+("width"==e?"Left":"Top")])+parseFloat(n["margin"+("width"==e?"Right":"Bottom")]))})}),1<r.length?i:i[r[0]]},computedHeight(e){return this.computedSize(e,{dimension:"height"})},computedWidth(e){return this.computedSize(e,{dimension:"width"})},loadedElem(i,{timeout:t=null}={}){var e=[new Promise(r=>{var e=document.querySelector(i);e?r(e):new MutationObserver((e,t)=>{var n=document.querySelector(i);n&&(t.disconnect(),r(n))}).observe(document.documentElement,{childList:!0,subtree:!0})})];return t&&e.push(new Promise(e=>setTimeout(()=>e(null),t))),Promise.race(e)}}};
@@ -1,4 +1,4 @@
1
- // Requires lib/<browser|chatgpt|dom|styles>.js + <app|env|configt>
1
+ // Requires lib/<browser|chatgpt|dom|styles>.js + <app|env>
2
2
 
3
3
  window.feedback = {
4
4
  notify(msg, pos = '', notifDuration = '', shadow = '') {
@@ -8,7 +8,7 @@ window.feedback = {
8
8
  if (foundState) msg = msg.replace(foundState, '')
9
9
 
10
10
  // Show notification
11
- chatgpt.notify(`${app.symbol} ${msg}`, pos ||( config.notifBottom ? 'bottom' : '' ),
11
+ chatgpt.notify(`${app.symbol} ${msg}`, pos ||( app.config.notifBottom ? 'bottom' : '' ),
12
12
  notifDuration, shadow || env.ui.scheme == 'light')
13
13
  const notif = document.querySelector('.chatgpt-notif:last-child')
14
14
  notif.classList.add(app.slug) // for styles.toast
@@ -1,4 +1,3 @@
1
- window.config = {}
2
1
  window.settings = {
3
2
 
4
3
  controls: {
@@ -40,15 +39,17 @@ window.settings = {
40
39
  },
41
40
 
42
41
  typeIsEnabled(key) { // for menu labels + notifs to return ON/OFF for type w/o suffix
42
+ app.config ??= {}
43
43
  const reInvertFlags = /disabled|hidden/i
44
44
  return reInvertFlags.test(key) // flag in control key name
45
45
  && !reInvertFlags.test(this.controls[key]?.label) // but not in label name
46
- ? !config[key] : config[key] // so invert since flag reps opposite type state, else don't
46
+ ? !app.config[key] : app.config[key] // so invert since flag reps opposite type state, else don't
47
47
  },
48
48
 
49
49
  load(...keys) {
50
+ app.config ??= {}
50
51
  return Promise.all(keys.flat().map(async key => // resolve promise when all keys load
51
- config[key] = processKey(key, (await chrome.storage.local.get(key))[key])))
52
+ app.config[key] = processKey(key, (await chrome.storage.local.get(key))[key])))
52
53
  function processKey(key, val) {
53
54
  const ctrl = settings.controls?.[key]
54
55
  if (val != undefined && ( // validate stored val
@@ -60,7 +61,8 @@ window.settings = {
60
61
  },
61
62
 
62
63
  save(key, val) {
64
+ app.config ??= {}
63
65
  chrome.storage.local.set({ [key]: val }) // save to Chrome extension storage
64
- config[key] = val // save to memory
66
+ app.config[key] = val // save to memory
65
67
  }
66
68
  };
@@ -2,8 +2,8 @@
2
2
  "manifest_version": 3,
3
3
  "name": "ChatGPT Extension",
4
4
  "short_name": "ChatGPT 🧩",
5
- "description": "A Chromium extension template to start using chatgpt.js like a boss!",
6
- "version": "2025.10.15",
5
+ "description": "A Chromium extension template to start using chatgpt.js like a champion",
6
+ "version": "2026.1.27",
7
7
  "homepage_url": "https://github.com/KudoAI/chatgpt.js-chrome-starter",
8
8
  "icons": { "16": "icons/icon16.png", "32": "icons/icon32.png", "64": "icons/icon64.png", "128": "icons/icon128.png" },
9
9
  "permissions": ["activeTab", "storage"],
@@ -5,7 +5,7 @@
5
5
  document.documentElement.classList.add('dark')
6
6
 
7
7
  // Import JS resources
8
- for (const resource of ['components/icons.js', 'lib/dom.js', 'lib/settings.js'])
8
+ for (const resource of ['components/icons.js', 'lib/css.min.js', 'lib/dom.min.js', 'lib/settings.js'])
9
9
  await import(chrome.runtime.getURL(resource))
10
10
 
11
11
  // Init DATA
@@ -54,7 +54,7 @@
54
54
 
55
55
  // Create/append slider elems
56
56
  entry.div.append(entry.slider = dom.create.elem('input', { class: 'slider', type: 'range',
57
- min: minVal, max: maxVal, value: config[entryData.key] }))
57
+ min: minVal, max: maxVal, value: app.config[entryData.key] }))
58
58
  entry.div.classList.remove('highlight-on-hover')
59
59
  if (entryData.step || env.browser.isFF) // use val from entryData or default to 2% in FF for being laggy
60
60
  entry.slider.step = entryData.step || ( 0.02 * entry.slider.max - entry.slider.min )
@@ -109,7 +109,7 @@
109
109
  category: () => toggleCategorySettingsVisiblity({ key: entryData.key }),
110
110
  toggle: () => {
111
111
  entry.leftElem.classList.toggle('on')
112
- settings.save(entryData.key, !config[entryData.key])
112
+ settings.save(entryData.key, !app.config[entryData.key])
113
113
  sync.configToUI({ updatedKey: entryData.key })
114
114
  requestAnimationFrame(() => notify(
115
115
  `${entryData.label} ${['OFF', 'ON'][+settings.typeIsEnabled(entryData.key)]}`))
@@ -162,7 +162,7 @@
162
162
  // Toolbar icon
163
163
  chrome.action.setIcon({ path: Object.fromEntries(
164
164
  Object.keys(chrome.runtime.getManifest().icons).map(dimension =>
165
- [dimension, `../icons/${ config.extensionDisabled ? 'faded/' : '' }icon${dimension}.png`]
165
+ [dimension, `../icons/${ app.config.extensionDisabled ? 'faded/' : '' }icon${dimension}.png`]
166
166
  ))})
167
167
 
168
168
  // Menu elems
@@ -170,8 +170,8 @@
170
170
  .forEach((elem, idx) => {
171
171
  if (elem.id && (elem.matches(`#${elem.id}:has(> div.link)`) || elem.id == 'aboutEntry'))
172
172
  return // never disable link/About entries
173
- elem.style.transition = config.extensionDisabled ? '' : 'opacity 0.15s ease-in'
174
- const toDisable = config.extensionDisabled || !depIsEnabled(elem.id)
173
+ elem.style.transition = app.config.extensionDisabled ? '' : 'opacity 0.15s ease-in'
174
+ const toDisable = app.config.extensionDisabled || !depIsEnabled(elem.id)
175
175
  if (elem.classList.contains('categorized-entries')) { // fade category strip
176
176
  elem.style.transition = toDisable ? 'none' : 'var(--border-transition)'
177
177
  elem.style.borderImage = elem.style.borderImage.replace(
@@ -223,7 +223,7 @@
223
223
  href: `https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@71695ca/assets/styles/rising-particles/dist/${
224
224
  color}.min.css`
225
225
  })))
226
- dom.addRisingParticles(document.body, { lightScheme: env.menu.isDark ? 'white' : 'gray' })
226
+ css.addRisingParticles(document.body, { lightScheme: env.menu.isDark ? 'white' : 'gray' })
227
227
 
228
228
  // Init MASTER TOGGLE
229
229
  const masterToggle = {
@@ -232,9 +232,9 @@
232
232
  track: dom.create.elem('span', { class: 'track', style: 'position: relative ; top: 7.5px' })
233
233
  }
234
234
  masterToggle.div.append(masterToggle.switch) ; masterToggle.switch.append(masterToggle.track)
235
- await settings.load('extensionDisabled') ; masterToggle.switch.classList.toggle('on', !config.extensionDisabled)
235
+ await settings.load('extensionDisabled') ; masterToggle.switch.classList.toggle('on', !app.config.extensionDisabled)
236
236
  masterToggle.div.onclick = () => {
237
- masterToggle.switch.classList.toggle('on') ; settings.save('extensionDisabled', !config.extensionDisabled)
237
+ masterToggle.switch.classList.toggle('on') ; settings.save('extensionDisabled', !app.config.extensionDisabled)
238
238
  Object.keys(sync).forEach(key => sync[key]()) // sync fade + storage to UI
239
239
  notify(`${app.name} ${ this.checked ? 'ON' : 'OFF' }`)
240
240
  }
@@ -13,8 +13,7 @@ body {
13
13
 
14
14
  /* Color/fade mods */
15
15
  .highlight-on-hover:hover { background: var(--entry-highlighted-bg) }
16
- .highlight-on-hover:hover span:not(.track), .highlight-on-hover:hover svg, span.menu-icon.highlight-on-hover:hover img {
17
- filter: invert(1) } /* invert setting labels on hover */
16
+ .highlight-on-hover:hover :where(span:not(.track), svg) { filter: invert(1) } /* invert setting labels on hover */
18
17
  .disabled { opacity: 0.3 !important ; pointer-events: none }
19
18
 
20
19
  /* Loader */