Skip to content

Commit

Permalink
feat: floating selection toolbar for translate, summary, polish
Browse files Browse the repository at this point in the history
…, `sentiment analysis`, `divide paragraphs`, `ask`
  • Loading branch information
josStorer committed Mar 13, 2023
1 parent d681e00 commit 55a139a
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 6 deletions.
8 changes: 7 additions & 1 deletion build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ async function runWebpack(isWithoutKatex, callback) {
import: './src/popup/index.jsx',
dependOn: 'shared',
},
shared: ['preact', 'webextension-polyfill', '@primer/octicons-react', './src/utils'],
shared: [
'preact',
'webextension-polyfill',
'@primer/octicons-react',
'react-bootstrap-icons',
'./src/utils',
],
},
output: {
filename: '[name].js',
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"preact": "^10.11.3",
"prop-types": "^15.8.1",
"react": "npm:@preact/compat@^17.1.2",
"react-bootstrap-icons": "^1.10.2",
"react-dom": "npm:@preact/compat@^17.1.2",
"react-markdown": "^8.0.5",
"rehype-highlight": "^6.0.0",
Expand Down
5 changes: 5 additions & 0 deletions src/components/ConversationCardForSearch/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ function ConversationCardForSearch(props) {
const [isReady, setIsReady] = useState(false)
const [port, setPort] = useState(() => Browser.runtime.connect())

useEffect(() => {
if (props.onUpdate) props.onUpdate()
})

useEffect(() => {
// when the page is responsive, session may accumulate redundant data and needs to be cleared after remounting and before making a new request
props.session = initSession({ question: props.question })
Expand Down Expand Up @@ -177,6 +181,7 @@ function ConversationCardForSearch(props) {
ConversationCardForSearch.propTypes = {
session: PropTypes.object.isRequired,
question: PropTypes.string.isRequired,
onUpdate: PropTypes.func,
}

export default memo(ConversationCardForSearch)
86 changes: 86 additions & 0 deletions src/components/FloatingToolbar/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Browser from 'webextension-polyfill'
import { cloneElement, useEffect, useRef, useState } from 'react'
import ConversationCardForSearch from '../ConversationCardForSearch'
import PropTypes from 'prop-types'
import { defaultConfig, getUserConfig } from '../../config.mjs'
import { config as toolsConfig } from '../../content-script/selection-tools'
import { updateRefHeight } from '../../utils/update-ref-height.mjs'
import { initSession } from '../../utils/index.mjs'

const logo = Browser.runtime.getURL('logo.png')

function FloatingToolbar(props) {
const [prompt, setPrompt] = useState('')
const [triggered, setTriggered] = useState(false)
const [config, setConfig] = useState(defaultConfig)
const [render, setRender] = useState(false)
const [session] = useState(initSession())
const toolWindow = useRef(null)

useEffect(() => {
getUserConfig()
.then(setConfig)
.then(() => setRender(true))
}, [])

useEffect(() => {
const listener = (changes) => {
const changedItems = Object.keys(changes)
let newConfig = {}
for (const key of changedItems) {
newConfig[key] = changes[key].newValue
}
setConfig({ ...config, ...newConfig })
}
Browser.storage.local.onChanged.addListener(listener)
return () => {
Browser.storage.local.onChanged.removeListener(listener)
}
}, [config])

if (!render) return <div />

const tools = []

for (const key in toolsConfig) {
const toolConfig = toolsConfig[key]
tools.push(
cloneElement(toolConfig.icon, {
size: 20,
className: 'gpt-selection-toolbar-button',
title: toolConfig.label,
onClick: () => {
setPrompt(toolConfig.genPrompt(props.selection))
setTriggered(true)
},
}),
)
}

return (
<div data-theme={config.themeMode}>
{triggered ? (
<div className="gpt-selection-window" ref={toolWindow}>
<div className="chat-gpt-container">
<ConversationCardForSearch
session={session}
question={prompt}
onUpdate={() => updateRefHeight(toolWindow)}
/>
</div>
</div>
) : (
<div className="gpt-selection-toolbar">
<img src={logo} width="24" height="24" />
{tools}
</div>
)}
</div>
)
}

FloatingToolbar.propTypes = {
selection: PropTypes.string.isRequired,
}

export default FloatingToolbar
51 changes: 46 additions & 5 deletions src/content-script/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { render } from 'preact'
import DecisionCardForSearch from '../components/DecisionCardForSearch'
import { config as siteConfig } from './site-adapters'
import { clearOldAccessToken, getUserConfig, setAccessToken } from '../config'
import { getPossibleElementByQuerySelector, initSession, isSafari } from '../utils'

window.session = initSession()
import {
createElementAtPosition,
getPossibleElementByQuerySelector,
initSession,
isSafari,
} from '../utils'
import FloatingToolbar from '../components/FloatingToolbar'

/**
* @param {SiteConfig} siteConfig
Expand Down Expand Up @@ -72,9 +76,40 @@ async function prepareForSafari() {
}
}

async function run() {
if (isSafari()) await prepareForSafari()
let toolbarContainer

async function prepareForSelectionTools() {
document.addEventListener('mouseup', (e) => {
if (toolbarContainer && toolbarContainer.contains(e.target)) return

if (toolbarContainer) toolbarContainer.remove()
setTimeout(() => {
const selection = window.getSelection()?.toString()
if (selection) {
toolbarContainer = createElementAtPosition(e.pageX + 15, e.pageY - 15)
render(<FloatingToolbar selection={selection} />, toolbarContainer)
}
})
})
document.addEventListener('mousedown', (e) => {
if (toolbarContainer && toolbarContainer.contains(e.target)) return

if (toolbarContainer) toolbarContainer.remove()
})
document.addEventListener('keydown', (e) => {
if (
toolbarContainer &&
!toolbarContainer.contains(e.target) &&
(e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA')
) {
setTimeout(() => {
if (!window.getSelection()?.toString()) toolbarContainer.remove()
})
}
})
}

async function prepareForStaticCard() {
const userConfig = await getUserConfig()
let siteRegex
if (userConfig.userSiteRegexOnly) siteRegex = userConfig.siteRegex
Expand All @@ -96,4 +131,10 @@ async function run() {
}
}

async function run() {
if (isSafari()) await prepareForSafari()
prepareForSelectionTools()
prepareForStaticCard()
}

run()
45 changes: 45 additions & 0 deletions src/content-script/selection-tools/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
CardHeading,
CardList,
EmojiSmile,
Palette,
QuestionCircle,
Translate,
} from 'react-bootstrap-icons'

export const config = {
translate: {
icon: <Translate />,
label: 'Translate',
genPrompt: (selection) => `Translate the following into Chinese:"${selection}"`,
},
summary: {
icon: <CardHeading />,
label: 'Summary',
genPrompt: (selection) => `Summarize the following:"${selection}"`,
},
polish: {
icon: <Palette />,
label: 'Polish',
genPrompt: (selection) =>
`Check the following content for diction and grammar,and polish:"${selection}"`,
},
sentiment: {
icon: <EmojiSmile />,
label: 'Sentiment Analysis',
genPrompt: (selection) =>
`Analyze the sentiments expressed in the following content and make a brief summary:"${selection}"`,
},
divide: {
icon: <CardList />,
label: 'Divide Paragraphs',
genPrompt: (selection) =>
`Divide the following into paragraphs that are easy to read and understand:"${selection}"`,
},
ask: {
icon: <QuestionCircle />,
label: 'Ask',
genPrompt: (selection) =>
`Analyze the following content and express your opinion,or give your answer:"${selection}"`,
},
}
32 changes: 32 additions & 0 deletions src/content-script/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,35 @@
opacity: 0.5;
}
}

.gpt-selection-toolbar {
display: flex;
align-items: center;
border-radius: 15px;
padding: 2px;
background-color: #ffffff;
box-shadow: 4px 2px 4px rgba(0, 0, 0, 0.2);
}

.gpt-selection-toolbar-button {
margin-left: 5px;
padding: 2px;
border-radius: 30px;
background-color: #ffffff;
color: #24292f;
cursor: pointer;
}

.gpt-selection-toolbar-button:hover {
background-color: #d4d5da;
}

.gpt-selection-window {
width: 450px;
max-height: 800px;
padding: 10px;
overflow-y: auto;
border-radius: 20px;
background-color: var(--theme-color);
box-shadow: 8px 4px 4px rgba(0, 0, 0, 0.2);
}

0 comments on commit 55a139a

Please sign in to comment.