Files
chatGPTBox/src/content-script/index.jsx
T

368 lines
11 KiB
React

import './styles.scss'
import { unmountComponentAtNode } from 'react-dom'
import { render } from 'preact'
import DecisionCard from '../components/DecisionCard'
import { config as siteConfig } from './site-adapters'
import { config as toolsConfig } from './selection-tools'
import { config as menuConfig } from './menu-tools'
import {
chatgptWebModelKeys,
getPreferredLanguageKey,
getUserConfig,
setAccessToken,
} from '../config/index.mjs'
import {
createElementAtPosition,
cropText,
getClientPosition,
getPossibleElementByQuerySelector,
} from '../utils'
import FloatingToolbar from '../components/FloatingToolbar'
import Browser from 'webextension-polyfill'
import { getPreferredLanguage } from '../config/language.mjs'
import '../_locales/i18n-react'
import { changeLanguage } from 'i18next'
import { initSession } from '../services/init-session.mjs'
import { getChatGptAccessToken, registerPortListener } from '../services/wrappers.mjs'
import { generateAnswersWithChatgptWebApi } from '../services/apis/chatgpt-web.mjs'
import NotificationForChatGPTWeb from '../components/NotificationForChatGPTWeb'
/**
* @param {SiteConfig} siteConfig
* @param {UserConfig} userConfig
*/
async function mountComponent(siteConfig, userConfig) {
const retry = 10
let oldUrl = location.href
for (let i = 1; i <= retry; i++) {
if (location.href !== oldUrl) {
console.log(`SiteAdapters Retry ${i}/${retry}: stop`)
return
}
const e =
(siteConfig &&
(getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) ||
getPossibleElementByQuerySelector(siteConfig.appendContainerQuery) ||
getPossibleElementByQuerySelector(siteConfig.resultsContainerQuery))) ||
getPossibleElementByQuerySelector([userConfig.prependQuery]) ||
getPossibleElementByQuerySelector([userConfig.appendQuery])
if (e) {
console.log(`SiteAdapters Retry ${i}/${retry}: found`)
console.log(e)
break
} else {
console.log(`SiteAdapters Retry ${i}/${retry}: not found`)
if (i === retry) return
else await new Promise((r) => setTimeout(r, 500))
}
}
document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => {
unmountComponentAtNode(e)
e.remove()
})
let question
if (userConfig.inputQuery) question = await getInput([userConfig.inputQuery])
if (!question && siteConfig) question = await getInput(siteConfig.inputQuery)
document.querySelectorAll('.chatgptbox-container,#chatgptbox-container').forEach((e) => {
unmountComponentAtNode(e)
e.remove()
})
const container = document.createElement('div')
container.id = 'chatgptbox-container'
render(
<DecisionCard
session={initSession({ modelName: (await getUserConfig()).modelName })}
question={question}
siteConfig={siteConfig}
container={container}
/>,
container,
)
}
/**
* @param {string[]|function} inputQuery
* @returns {Promise<string>}
*/
async function getInput(inputQuery) {
let input
if (typeof inputQuery === 'function') {
input = await inputQuery()
if (input) return `Reply in ${await getPreferredLanguage()}.\n` + input
return input
}
const searchInput = getPossibleElementByQuerySelector(inputQuery)
if (searchInput) {
if (searchInput.value) input = searchInput.value
else if (searchInput.textContent) input = searchInput.textContent
if (input)
return (
`Reply in ${await getPreferredLanguage()}.\nThe following is a search input in a search engine, ` +
`giving useful content or solutions and as much information as you can related to it, ` +
`use markdown syntax to make your answer more readable, such as code blocks, bold, list:\n` +
input
)
}
}
let toolbarContainer
const deleteToolbar = () => {
if (toolbarContainer && toolbarContainer.className === 'chatgptbox-toolbar-container')
toolbarContainer.remove()
}
const createSelectionTools = async (toolbarContainer, selection) => {
toolbarContainer.className = 'chatgptbox-toolbar-container'
render(
<FloatingToolbar
session={initSession({ modelName: (await getUserConfig()).modelName })}
selection={selection}
container={toolbarContainer}
dockable={true}
/>,
toolbarContainer,
)
}
async function prepareForSelectionTools() {
document.addEventListener('mouseup', (e) => {
if (toolbarContainer && toolbarContainer.contains(e.target)) return
const selectionElement =
window.getSelection()?.rangeCount > 0 &&
window.getSelection()?.getRangeAt(0).endContainer.parentElement
if (toolbarContainer && selectionElement && toolbarContainer.contains(selectionElement)) return
deleteToolbar()
setTimeout(async () => {
const selection = window
.getSelection()
?.toString()
.trim()
.replace(/^-+|-+$/g, '')
if (selection) {
let position
const config = await getUserConfig()
if (!config.selectionToolsNextToInputBox) position = { x: e.pageX + 20, y: e.pageY + 20 }
else {
const inputElement = selectionElement.querySelector('input, textarea')
if (inputElement) {
position = getClientPosition(inputElement)
position = {
x: position.x + window.scrollX + inputElement.offsetWidth + 50,
y: e.pageY + 30,
}
} else {
position = { x: e.pageX + 20, y: e.pageY + 20 }
}
}
toolbarContainer = createElementAtPosition(position.x, position.y)
await createSelectionTools(toolbarContainer, selection)
}
})
})
document.addEventListener('mousedown', (e) => {
if (toolbarContainer && toolbarContainer.contains(e.target)) return
document.querySelectorAll('.chatgptbox-toolbar-container').forEach((e) => e.remove())
})
document.addEventListener('keydown', (e) => {
if (
toolbarContainer &&
!toolbarContainer.contains(e.target) &&
(e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA')
) {
setTimeout(() => {
if (!window.getSelection()?.toString().trim()) deleteToolbar()
})
}
})
}
async function prepareForSelectionToolsTouch() {
document.addEventListener('touchend', (e) => {
if (toolbarContainer && toolbarContainer.contains(e.target)) return
if (
toolbarContainer &&
window.getSelection()?.rangeCount > 0 &&
toolbarContainer.contains(window.getSelection()?.getRangeAt(0).endContainer.parentElement)
)
return
deleteToolbar()
setTimeout(() => {
const selection = window
.getSelection()
?.toString()
.trim()
.replace(/^-+|-+$/g, '')
if (selection) {
toolbarContainer = createElementAtPosition(
e.changedTouches[0].pageX + 20,
e.changedTouches[0].pageY + 20,
)
createSelectionTools(toolbarContainer, selection)
}
})
})
document.addEventListener('touchstart', (e) => {
if (toolbarContainer && toolbarContainer.contains(e.target)) return
document.querySelectorAll('.chatgptbox-toolbar-container').forEach((e) => e.remove())
})
}
let menuX, menuY
async function prepareForRightClickMenu() {
document.addEventListener('contextmenu', (e) => {
menuX = e.clientX
menuY = e.clientY
})
Browser.runtime.onMessage.addListener(async (message) => {
if (message.type === 'CREATE_CHAT') {
const data = message.data
let prompt = ''
if (data.itemId in toolsConfig) {
prompt = await toolsConfig[data.itemId].genPrompt(data.selectionText)
} else if (data.itemId in menuConfig) {
const menuItem = menuConfig[data.itemId]
if (!menuItem.genPrompt) return
else prompt = await menuItem.genPrompt()
if (prompt) prompt = await cropText(`Reply in ${await getPreferredLanguage()}.\n` + prompt)
}
const position = data.useMenuPosition
? { x: menuX, y: menuY }
: { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 }
const container = createElementAtPosition(position.x, position.y)
container.className = 'chatgptbox-toolbar-container-not-queryable'
render(
<FloatingToolbar
session={initSession({ modelName: (await getUserConfig()).modelName })}
selection={data.selectionText}
container={container}
triggered={true}
closeable={true}
prompt={prompt}
/>,
container,
)
}
})
}
async function prepareForStaticCard() {
const userConfig = await getUserConfig()
let siteRegex
if (userConfig.useSiteRegexOnly) siteRegex = userConfig.siteRegex
else
siteRegex = new RegExp(
(userConfig.siteRegex && userConfig.siteRegex + '|') + Object.keys(siteConfig).join('|'),
)
const matches = location.hostname.match(siteRegex)
if (matches) {
const siteName = matches[0]
if (
userConfig.siteAdapters.includes(siteName) &&
!userConfig.activeSiteAdapters.includes(siteName)
)
return
if (siteName in siteConfig) {
const siteAction = siteConfig[siteName].action
if (siteAction && siteAction.init) {
await siteAction.init(location.hostname, userConfig, getInput, mountComponent)
}
}
mountComponent(siteConfig[siteName], userConfig)
}
}
async function overwriteAccessToken() {
if (location.hostname !== 'chat.openai.com') return
let data
if (location.pathname === '/api/auth/session') {
const response = document.querySelector('pre').textContent
try {
data = JSON.parse(response)
} catch (error) {
console.error('json error', error)
}
} else {
const resp = await fetch('https://chat.openai.com/api/auth/session')
data = await resp.json().catch(() => ({}))
}
if (data && data.accessToken) {
await setAccessToken(data.accessToken)
console.log(data.accessToken)
}
}
async function prepareForForegroundRequests() {
if (location.hostname !== 'chat.openai.com' || location.pathname === '/auth/login') return
const userConfig = await getUserConfig()
if (!chatgptWebModelKeys.some((model) => userConfig.activeApiModes.includes(model))) return
if (chatgptWebModelKeys.includes(userConfig.modelName)) {
const div = document.createElement('div')
document.body.append(div)
render(<NotificationForChatGPTWeb container={div} />, div)
}
if (location.pathname === '/') {
const input = document.querySelector('#prompt-textarea')
if (input) {
input.textContent = ' '
input.dispatchEvent(new Event('input', { bubbles: true }))
setTimeout(() => {
input.textContent = ''
input.dispatchEvent(new Event('input', { bubbles: true }))
}, 300)
}
}
await Browser.runtime.sendMessage({
type: 'SET_CHATGPT_TAB',
data: {},
})
registerPortListener(async (session, port) => {
if (chatgptWebModelKeys.includes(session.modelName)) {
const accessToken = await getChatGptAccessToken()
await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken)
}
})
}
async function run() {
await getPreferredLanguageKey().then((lang) => {
changeLanguage(lang)
})
Browser.runtime.onMessage.addListener(async (message) => {
if (message.type === 'CHANGE_LANG') {
const data = message.data
changeLanguage(data.lang)
}
})
await overwriteAccessToken()
await prepareForForegroundRequests()
prepareForSelectionTools()
prepareForSelectionToolsTouch()
prepareForStaticCard()
prepareForRightClickMenu()
}
run()