Last active
May 4, 2024 08:31
-
-
Save ngseke/ed21fe10b575aae93746579e0139c068 to your computer and use it in GitHub Desktop.
ChatGPT as An English Tutor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name ChatGPT as An English Tutor | |
// @namespace https://ngseke.me/ | |
// @version 0.1 | |
// @description ChatGPT as An English Tutor | |
// @author ngseke | |
// @match https://chatgpt.com/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com | |
// @grant none | |
// ==/UserScript== | |
(function () { | |
const blank = '_'.repeat(5) | |
const sentences = [ | |
`幫我翻譯成中文並分析 collocations, phrases, sentence patterns:\n ${blank}`, | |
`幫我翻譯成中文:\n ${blank}`, | |
`${blank} 是什麼意思?請提供一些英文例句和對應的翻譯`, | |
`請提供一些以下在英文的表達方式:\n${blank}`, | |
`請問以下英文聽起來自然且通順嗎?若不通順請提供一些修改建議;若通順也請提供其他類似的說法:\n${blank}`, | |
`請問在英文當中有 ${blank} 這種說法嗎?`, | |
`${blank} 和 ${blank} 的差別是什麼?`, | |
`請提供一些 ${blank} 的英文同義詞`, | |
`請提供其他類似的英文說法: ${blank}`, | |
`請多解釋一點 ${blank}`, | |
`其中的 ${blank} 是什麼意思?它在英文中的用法或文法是什麼?`, | |
`什麼是 ${blank}`, | |
] | |
const debounce = (callback, wait) => { | |
let timerId = null | |
return (...args) => { | |
clearTimeout(timerId) | |
timerId = setTimeout(() => callback.apply(null, args), wait) | |
} | |
} | |
const namespace = `english-tutor-${crypto.randomUUID()}` | |
const hideClassName = 'hide' | |
const getTextArea = () => document.querySelector('.absolute.bottom-0 textarea') ?? document.querySelector('.border textarea') | |
const getContainer = () => getTextArea()?.parentNode?.parentNode | |
const getClipboard = () => navigator.clipboard.readText() | |
const insertText = async (text) => { | |
const textarea = getTextArea() | |
textarea.focus() | |
const textWithClipboard = text.replace(blank, await getClipboard()) | |
textarea.value = `${textWithClipboard}` | |
textarea.dispatchEvent(new Event('input', { bubbles: true })) | |
textarea.scrollTop = textarea.scrollHeight | |
} | |
const injectStyles = () => { | |
const css = ` | |
.${namespace} { | |
position: absolute; | |
left: 2.5rem; | |
right: 1rem; | |
bottom: 1rem; | |
z-index: 1; | |
pointer-events: none; | |
} | |
.${namespace} > * { | |
pointer-events: auto; | |
} | |
.${namespace} button.activator { | |
display: flex; | |
cursor: pointer; | |
font-size: 2rem; | |
line-height: 1; | |
transition: all .5s; | |
user-select: none; | |
text-shadow: 0 0 10px rgba(255, 255, 0, .3); | |
} | |
.${namespace} button.activator:hover { | |
text-shadow: 0 0 10px rgba(255, 255, 0, .5); | |
} | |
.${namespace} .${hideClassName} { | |
display: none; | |
} | |
.${namespace} ul { | |
display: flex; | |
flex-direction: column; | |
padding: .5rem 0; | |
gap: .25rem; | |
font-size: 14px; | |
position: absolute; | |
top: 0; | |
left: 0; | |
transform: translateY(calc(-100% - .5rem)); | |
border-radius: .5rem; | |
width: 35rem; | |
max-width: 100%; | |
background-color: rgb(32,33,35); | |
box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px; | |
} | |
.${namespace} li { | |
user-select: none; | |
} | |
.${namespace} li a { | |
display: block; | |
padding: .25rem 1rem; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
overflow: hidden; | |
} | |
.${namespace} li:hover { | |
background-color: rgba(255, 255, 255, .1) | |
} | |
#prompt-textarea { | |
padding-left: 5rem; | |
} | |
@media (max-width: 767.99px) { | |
#prompt-textarea { | |
padding-left: 3rem; | |
} | |
} | |
` | |
const style = document.createElement('style') | |
document.body.appendChild(style) | |
style.type = 'text/css' | |
style.appendChild(document.createTextNode(css)) | |
} | |
const generateMenu = () => { | |
const ul = document.createElement('ul') | |
ul.classList.add(hideClassName) | |
const lis = sentences.map((sentence) => { | |
const li = document.createElement('li') | |
const a = document.createElement('a') | |
a.href = '#' | |
a.innerText = sentence.replace('\n', '') | |
a.addEventListener('click', () => { | |
insertText(sentence) | |
ul.classList.add(hideClassName) | |
}) | |
li.appendChild(a) | |
return li | |
}) | |
ul.append(...lis) | |
return ul | |
} | |
const generateActivatorButton = (target) => { | |
const $button = document.createElement('button') | |
$button.innerHTML = '🪄' | |
$button.classList.add('activator') | |
$button.type = 'button' | |
let triggerType = 'click' | |
;['click', 'mouseover'].forEach((type) => { | |
$button.addEventListener(type, (e) => { | |
const shouldSkip = triggerType === 'click' && | |
!target.classList.contains(hideClassName) | |
if (shouldSkip) return | |
triggerType = e.type | |
target.classList.remove(hideClassName) | |
}) | |
}) | |
const handleHoverOutside = debounce((e) => { | |
const isOutside = ![target, $button].some($el => $el.contains(e.target)) | |
if (isOutside && triggerType === 'mouseover') target.classList.add(hideClassName) | |
}, 100) | |
document.body.addEventListener('mouseover', handleHoverOutside) | |
return $button | |
} | |
const listenClickOutside = ($button, $menu) => { | |
document.addEventListener('click', (event) => { | |
const withinBoundaries = [$button, $menu].some($el => ( | |
event.composedPath().includes($el) | |
)) | |
if (withinBoundaries) return | |
$menu.classList.add(hideClassName) | |
}) | |
} | |
const injectWrapper = () => { | |
const $wrapper = document.createElement('div') | |
$wrapper.classList.add(namespace) | |
const $menu = generateMenu() | |
const $activatorButton = generateActivatorButton($menu) | |
$wrapper.appendChild($menu) | |
$wrapper.appendChild($activatorButton) | |
const $container = getContainer() | |
$container?.prepend($wrapper) | |
listenClickOutside($activatorButton, $menu) | |
return { $menu } | |
} | |
const observeDom = () => { | |
const observer = new MutationObserver(() => { | |
if (!getContainer() || getContainer()?.querySelector(`.${namespace}`)) { | |
return | |
} | |
injectWrapper() | |
}) | |
observer.observe(document.body, { childList: true, subtree: true }) | |
} | |
injectStyles() | |
observeDom() | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment