first commit

This commit is contained in:
josc146
2023-03-15 16:18:51 +08:00
commit e642f2b922
67 changed files with 14469 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
import './styles.scss'
import { render } from 'preact'
import DecisionCard from '../components/DecisionCard'
import { config as siteConfig } from './site-adapters'
import { config as toolsConfig } from './selection-tools'
import { clearOldAccessToken, getUserConfig, setAccessToken, getPreferredLanguage } from '../config'
import {
createElementAtPosition,
getPossibleElementByQuerySelector,
initSession,
isSafari,
} from '../utils'
import FloatingToolbar from '../components/FloatingToolbar'
import Browser from 'webextension-polyfill'
/**
* @param {SiteConfig} siteConfig
* @param {UserConfig} userConfig
*/
async function mountComponent(siteConfig, userConfig) {
if (
!getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) &&
!getPossibleElementByQuerySelector(siteConfig.appendContainerQuery) &&
!getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) &&
!getPossibleElementByQuerySelector([userConfig.prependQuery]) &&
!getPossibleElementByQuerySelector([userConfig.appendQuery])
)
return
document.querySelectorAll('.chat-gpt-container').forEach((e) => e.remove())
let question
if (userConfig.inputQuery) question = await getInput([userConfig.inputQuery])
if (!question && siteConfig) question = await getInput(siteConfig.inputQuery)
document.querySelectorAll('.chat-gpt-container').forEach((e) => e.remove())
const container = document.createElement('div')
container.className = 'chat-gpt-container'
render(
<DecisionCard
session={initSession()}
question={question}
siteConfig={siteConfig}
container={container}
/>,
container,
)
}
/**
* @param {string[]|function} inputQuery
* @returns {Promise<string>}
*/
async function getInput(inputQuery) {
if (typeof inputQuery === 'function') {
const input = await inputQuery()
if (input) return `Reply in ${await getPreferredLanguage()}.\n` + input
return input
}
const searchInput = getPossibleElementByQuerySelector(inputQuery)
if (searchInput && searchInput.value) {
return searchInput.value
}
}
async function prepareForSafari() {
await clearOldAccessToken()
if (location.hostname !== 'chat.openai.com' || location.pathname !== '/api/auth/session') return
const response = document.querySelector('pre').textContent
let data
try {
data = JSON.parse(response)
} catch (error) {
console.error('json error', error)
return
}
if (data.accessToken) {
await setAccessToken(data.accessToken)
}
}
let toolbarContainer
async function prepareForSelectionTools() {
document.addEventListener('mouseup', (e) => {
if (toolbarContainer && toolbarContainer.contains(e.target)) return
if (
toolbarContainer &&
window.getSelection()?.rangeCount > 0 &&
toolbarContainer.contains(window.getSelection()?.getRangeAt(0).endContainer.parentElement)
)
return
if (toolbarContainer) toolbarContainer.remove()
setTimeout(() => {
const selection = window.getSelection()?.toString()
if (selection) {
const position = { x: e.clientX + 15, y: e.clientY - 15 }
toolbarContainer = createElementAtPosition(position.x, position.y)
toolbarContainer.className = 'toolbar-container'
render(
<FloatingToolbar
session={initSession()}
selection={selection}
position={position}
container={toolbarContainer}
/>,
toolbarContainer,
)
}
})
})
document.addEventListener('mousedown', (e) => {
if (toolbarContainer && toolbarContainer.contains(e.target)) return
document.querySelectorAll('.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()) toolbarContainer.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 === 'MENU') {
const data = message.data
if (data.itemId === 'new') {
const position = { x: menuX, y: menuY }
const container = createElementAtPosition(position.x, position.y)
container.className = 'toolbar-container-not-queryable'
render(
<FloatingToolbar
session={initSession()}
selection=""
position={position}
container={container}
triggered={true}
closeable={true}
onClose={() => container.remove()}
/>,
container,
)
} else {
const position = { x: menuX, y: menuY }
const container = createElementAtPosition(position.x, position.y)
container.className = 'toolbar-container-not-queryable'
render(
<FloatingToolbar
session={initSession()}
selection={data.selectionText}
position={position}
container={container}
triggered={true}
closeable={true}
onClose={() => container.remove()}
prompt={await toolsConfig[data.itemId].genPrompt(data.selectionText)}
/>,
container,
)
}
}
})
}
async function prepareForStaticCard() {
let siteRegex
if (userConfig.userSiteRegexOnly) 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 (siteName in siteConfig) {
const siteAction = siteConfig[siteName].action
if (siteAction && siteAction.init) {
await siteAction.init(location.hostname, userConfig, getInput, mountComponent)
}
}
if (
userConfig.siteAdapters.includes(siteName) &&
!userConfig.activeSiteAdapters.includes(siteName)
)
return
mountComponent(siteConfig[siteName], userConfig)
}
}
let userConfig
async function run() {
userConfig = await getUserConfig()
if (isSafari()) await prepareForSafari()
prepareForSelectionTools()
prepareForStaticCard()
prepareForRightClickMenu()
}
run()
@@ -0,0 +1,60 @@
import {
CardHeading,
CardList,
EmojiSmile,
Palette,
QuestionCircle,
Translate,
} from 'react-bootstrap-icons'
import { getPreferredLanguage } from '../../config.mjs'
export const config = {
translate: {
icon: <Translate />,
label: 'Translate',
genPrompt: async (selection) => {
const preferredLanguage = await getPreferredLanguage()
return (
`Translate the following into ${preferredLanguage} and only show me the translated content.` +
`If it is already in ${preferredLanguage},` +
`translate it into English and only show me the translated content:\n"${selection}"`
)
},
},
summary: {
icon: <CardHeading />,
label: 'Summary',
genPrompt: async (selection) => {
const preferredLanguage = await getPreferredLanguage()
return `Reply in ${preferredLanguage}.Summarize the following as concisely as possible:\n"${selection}"`
},
},
polish: {
icon: <Palette />,
label: 'Polish',
genPrompt: async (selection) =>
`Check the following content for possible diction and grammar problems,and polish it carefully:\n"${selection}"`,
},
sentiment: {
icon: <EmojiSmile />,
label: 'Sentiment Analysis',
genPrompt: async (selection) => {
const preferredLanguage = await getPreferredLanguage()
return `Reply in ${preferredLanguage}.Analyze the sentiments expressed in the following content and make a brief summary of the sentiments:\n"${selection}"`
},
},
divide: {
icon: <CardList />,
label: 'Divide Paragraphs',
genPrompt: async (selection) =>
`Divide the following into paragraphs that are easy to read and understand:\n"${selection}"`,
},
ask: {
icon: <QuestionCircle />,
label: 'Ask',
genPrompt: async (selection) => {
const preferredLanguage = await getPreferredLanguage()
return `Reply in ${preferredLanguage}.Analyze the following content and express your opinion,or give your answer:\n"${selection}"`
},
},
}
@@ -0,0 +1 @@
//TODO
@@ -0,0 +1,26 @@
import { config } from '../index'
export default {
init: async (hostname, userConfig, getInput, mountComponent) => {
try {
const targetNode = document.getElementById('wrapper_wrapper')
const observer = new MutationObserver(async (records) => {
if (
records.some(
(record) =>
record.type === 'childList' &&
[...record.addedNodes].some((node) => node.id === 'container'),
)
) {
const searchValue = await getInput(config.baidu.inputQuery)
if (searchValue) {
mountComponent(config.baidu, userConfig)
}
}
})
observer.observe(targetNode, { childList: true })
} catch (e) {
/* empty */
}
},
}
@@ -0,0 +1,58 @@
import { cropText } from '../../../utils'
import { config } from '../index.mjs'
export default {
init: async (hostname, userConfig, getInput, mountComponent) => {
try {
let oldUrl = location.href
const checkUrlChange = async () => {
if (location.href !== oldUrl) {
oldUrl = location.href
mountComponent(config.bilibili, userConfig)
}
}
window.setInterval(checkUrlChange, 500)
} catch (e) {
/* empty */
}
},
inputQuery: async () => {
try {
const bvid = location.pathname.replace('video', '').replaceAll('/', '')
const p = Number(new URLSearchParams(location.search).get('p') || 1) - 1
const pagelistResponse = await fetch(
`https://api.bilibili.com/x/player/pagelist?bvid=${bvid}`,
)
const pagelistData = await pagelistResponse.json()
const videoList = pagelistData.data
const cid = videoList[p].cid
const title = videoList[p].part
const infoResponse = await fetch(
`https://api.bilibili.com/x/player/v2?bvid=${bvid}&cid=${cid}`,
{
credentials: 'include',
},
)
const infoData = await infoResponse.json()
const subtitleUrl = infoData.data.subtitle.subtitles[0].subtitle_url
const subtitleResponse = await fetch(subtitleUrl)
const subtitleData = await subtitleResponse.json()
const subtitles = subtitleData.body
let subtitleContent = ''
for (let i = 0; i < subtitles.length; i++) {
if (i === subtitles.length - 1) subtitleContent += subtitles[i].content
else subtitleContent += subtitles[i].content + ','
}
return cropText(
`用尽量简练的语言,联系视频标题,对视频进行内容摘要,视频标题为:"${title}",字幕内容为:\n${subtitleContent}`,
)
} catch (e) {
console.log(e)
}
},
}
@@ -0,0 +1,56 @@
import { cropText, limitedFetch } from '../../../utils'
import { config } from '../index.mjs'
const getPatchUrl = async () => {
const patchUrl = location.origin + location.pathname + '.patch'
const response = await fetch(patchUrl, { method: 'HEAD' })
if (response.ok) return patchUrl
return ''
}
const getPatchData = async (patchUrl) => {
if (!patchUrl) return
let patchData = await limitedFetch(patchUrl, 1024 * 40)
patchData = patchData.substring(patchData.indexOf('---'))
return patchData
}
export default {
init: async (hostname, userConfig, getInput, mountComponent) => {
try {
const targetNode = document.querySelector('body')
const observer = new MutationObserver(async (records) => {
if (
records.some(
(record) =>
record.type === 'childList' &&
[...record.addedNodes].some((node) => node.classList.contains('page-responsive')),
)
) {
const patchUrl = await getPatchUrl()
if (patchUrl) {
mountComponent(config.github, userConfig)
}
}
})
observer.observe(targetNode, { childList: true })
} catch (e) {
/* empty */
}
},
inputQuery: async () => {
try {
const patchUrl = await getPatchUrl()
const patchData = await getPatchData(patchUrl)
if (!patchData) return
return cropText(
`Analyze the contents of a git commit,provide a suitable commit message,and summarize the contents of the commit.` +
`The patch contents of this commit are as follows:\n${patchData}`,
)
} catch (e) {
console.log(e)
}
},
}
@@ -0,0 +1,33 @@
import { cropText, limitedFetch } from '../../../utils'
const getPatchUrl = async () => {
const patchUrl = location.origin + location.pathname + '.patch'
const response = await fetch(patchUrl, { method: 'HEAD' })
if (response.ok) return patchUrl
return ''
}
const getPatchData = async (patchUrl) => {
if (!patchUrl) return
let patchData = await limitedFetch(patchUrl, 1024 * 40)
patchData = patchData.substring(patchData.indexOf('---'))
return patchData
}
export default {
inputQuery: async () => {
try {
const patchUrl = await getPatchUrl()
const patchData = await getPatchData(patchUrl)
if (!patchData) return
return cropText(
`Analyze the contents of a git commit,provide a suitable commit message,and summarize the contents of the commit.` +
`The patch contents of this commit are as follows:\n${patchData}`,
)
} catch (e) {
console.log(e)
}
},
}
+163
View File
@@ -0,0 +1,163 @@
import baidu from './baidu'
import bilibili from './bilibili'
import youtube from './youtube'
import github from './github'
import gitlab from './gitlab'
import zhihu from './zhihu'
import reddit from './reddit'
import quora from './quora'
/**
* @typedef {object} SiteConfigAction
* @property {function} init
*/
/**
* @typedef {object} SiteConfig
* @property {string[]|function} inputQuery - for search box
* @property {string[]} sidebarContainerQuery - prepend child to
* @property {string[]} appendContainerQuery - if sidebarContainer not exists, append child to
* @property {string[]} resultsContainerQuery - prepend child to if insertAtTop is true
* @property {SiteConfigAction} action
*/
/**
* @type {Object.<string,SiteConfig>}
*/
export const config = {
google: {
inputQuery: ["input[name='q']"],
sidebarContainerQuery: ['#rhs'],
appendContainerQuery: ['#rcnt'],
resultsContainerQuery: ['#rso'],
},
bing: {
inputQuery: ["[name='q']"],
sidebarContainerQuery: ['#b_context'],
appendContainerQuery: [],
resultsContainerQuery: ['#b_results'],
},
yahoo: {
inputQuery: ["input[name='p']"],
sidebarContainerQuery: ['#right', '.Contents__inner.Contents__inner--sub'],
appendContainerQuery: ['#cols', '#contents__wrap'],
resultsContainerQuery: [
'#main-algo',
'.searchCenterMiddle',
'.Contents__inner.Contents__inner--main',
'#contents',
],
},
duckduckgo: {
inputQuery: ["input[name='q']"],
sidebarContainerQuery: ['.results--sidebar.js-results-sidebar'],
appendContainerQuery: ['#links_wrapper'],
resultsContainerQuery: ['.results'],
},
startpage: {
inputQuery: ["input[name='query']"],
sidebarContainerQuery: ['.layout-web__sidebar.layout-web__sidebar--web'],
appendContainerQuery: ['.layout-web__body.layout-web__body--desktop'],
resultsContainerQuery: ['.mainline-results'],
},
baidu: {
inputQuery: ["input[id='kw']"],
sidebarContainerQuery: ['#content_right'],
appendContainerQuery: ['#container'],
resultsContainerQuery: ['#content_left', '#results'],
action: {
init: baidu.init,
},
},
kagi: {
inputQuery: ["input[name='q']"],
sidebarContainerQuery: ['.right-content-box._0_right_sidebar'],
appendContainerQuery: ['#_0_app_content'],
resultsContainerQuery: ['#main', '#app'],
},
yandex: {
inputQuery: ["input[name='text']"],
sidebarContainerQuery: ['#search-result-aside'],
appendContainerQuery: [],
resultsContainerQuery: ['#search-result'],
},
naver: {
inputQuery: ["input[name='query']"],
sidebarContainerQuery: ['#sub_pack'],
appendContainerQuery: ['#content'],
resultsContainerQuery: ['#main_pack', '#ct'],
},
brave: {
inputQuery: ["input[name='q']"],
sidebarContainerQuery: ['#side-right'],
appendContainerQuery: [],
resultsContainerQuery: ['#results'],
},
searx: {
inputQuery: ["input[name='q']"],
sidebarContainerQuery: ['#sidebar_results', '#sidebar'],
appendContainerQuery: [],
resultsContainerQuery: ['#urls', '#main_results', '#results'],
},
ecosia: {
inputQuery: ["input[name='q']"],
sidebarContainerQuery: ['.sidebar.web__sidebar'],
appendContainerQuery: ['#main'],
resultsContainerQuery: ['.mainline'],
},
neeva: {
inputQuery: ["input[name='q']"],
sidebarContainerQuery: ['.result-group-layout__stickyContainer-iDIO8'],
appendContainerQuery: ['.search-index__searchHeaderContainer-2JD6q'],
resultsContainerQuery: ['.result-group-layout__component-1jzTe', '#search'],
},
bilibili: {
inputQuery: bilibili.inputQuery,
sidebarContainerQuery: ['#danmukuBox'],
appendContainerQuery: [],
resultsContainerQuery: [],
action: {
init: bilibili.init,
},
},
youtube: {
inputQuery: youtube.inputQuery,
sidebarContainerQuery: ['#secondary'],
appendContainerQuery: [],
resultsContainerQuery: [],
action: {
init: youtube.init,
},
},
github: {
inputQuery: github.inputQuery,
sidebarContainerQuery: ['#diff', '.commit'],
appendContainerQuery: [],
resultsContainerQuery: [],
action: {
init: github.init,
},
},
gitlab: {
inputQuery: gitlab.inputQuery,
sidebarContainerQuery: ['.js-commit-box-info'],
appendContainerQuery: [],
resultsContainerQuery: [],
},
zhihu: {
inputQuery: zhihu.inputQuery,
sidebarContainerQuery: ['.Question-sideColumn', '.Post-Header'],
appendContainerQuery: [],
resultsContainerQuery: [],
},
reddit: {
inputQuery: reddit.inputQuery,
sidebarContainerQuery: ['.side .spacer .linkinfo'],
appendContainerQuery: [],
resultsContainerQuery: [],
},
quora: {
inputQuery: quora.inputQuery,
sidebarContainerQuery: ['.q-box.PageContentsLayout___StyledBox-d2uxks-0'],
appendContainerQuery: [],
resultsContainerQuery: [],
},
}
@@ -0,0 +1,26 @@
import { cropText } from '../../../utils'
export default {
inputQuery: async () => {
try {
if (location.pathname === '/') return
const texts = document.querySelectorAll('.q-box.qu-userSelect--text')
let title
if (texts.length > 0) title = texts[0].textContent
let answers = ''
if (texts.length > 1)
for (let i = 1; i < texts.length; i++) {
answers += `answer${i}:${texts[i].textContent}|`
}
return cropText(
`Below is the content from a question and answer platform,giving the corresponding summary and your opinion on it.` +
`The question is:'${title}',` +
`Some answers are as follows:\n${answers}`,
)
} catch (e) {
console.log(e)
}
},
}
@@ -0,0 +1,25 @@
import { cropText } from '../../../utils'
export default {
inputQuery: async () => {
try {
const title = document.querySelector('.entry .title').textContent
const texts = document.querySelectorAll('.entry .usertext-body')
let description
if (texts.length > 0) description = texts[0].textContent
let answers = ''
if (texts.length > 1)
for (let i = 1; i < texts.length; i++) {
answers += `answer${i}:${texts[i].textContent}|`
}
return cropText(
`Below is the content from a social forum,giving the corresponding summary and your opinion on it.` +
`The title is:'${title}',and the further description of the title is:'${description}'.` +
`Some answers are as follows:\n${answers}`,
)
} catch (e) {
console.log(e)
}
},
}
@@ -0,0 +1 @@
//TODO
@@ -0,0 +1,53 @@
import { cropText } from '../../../utils'
import { config } from '../index.mjs'
export default {
init: async (hostname, userConfig, getInput, mountComponent) => {
try {
let oldUrl = location.href
const checkUrlChange = async () => {
if (location.href !== oldUrl) {
oldUrl = location.href
mountComponent(config.youtube, userConfig)
}
}
window.setInterval(checkUrlChange, 500)
} catch (e) {
/* empty */
}
},
inputQuery: async () => {
try {
const docText = await (
await fetch(location.href, {
credentials: 'include',
})
).text()
let subtitleUrl = docText.substring(docText.indexOf('https://www.youtube.com/api/timedtext'))
subtitleUrl = subtitleUrl.substring(0, subtitleUrl.indexOf('"'))
subtitleUrl = subtitleUrl.replaceAll('\\u0026', '&')
let title = docText.substring(docText.indexOf('"title":"') + '"title":"'.length)
title = title.substring(0, title.indexOf('","'))
const subtitleResponse = await fetch(subtitleUrl)
let subtitleData = await subtitleResponse.text()
let subtitleContent = ''
while (subtitleData.indexOf('">') !== -1) {
subtitleData = subtitleData.substring(subtitleData.indexOf('">') + 2)
subtitleContent += subtitleData.substring(0, subtitleData.indexOf('<')) + ','
}
await new Promise((r) => setTimeout(r, 1000))
return cropText(
`Provide a brief summary of the video using concise language and incorporating the video title.` +
`The video title is:"${title}".The subtitle content is as follows:\n${subtitleContent}`,
)
} catch (e) {
console.log(e)
}
},
}
@@ -0,0 +1,29 @@
import { cropText } from '../../../utils'
export default {
inputQuery: async () => {
try {
const title = document.querySelector('.QuestionHeader-title')?.textContent
if (title) {
const description = document.querySelector('.QuestionRichText')?.textContent
const answer = document.querySelector('.AnswerItem .RichText')?.textContent
return cropText(
`以下是一个问答平台的提问与回答内容,给出相应的摘要,以及你对此的看法.问题是:"${title}",问题的进一步描述是:"${description}".` +
`其中一个回答如下:\n${answer}`,
)
} else {
const title = document.querySelector('.Post-Title')?.textContent
const description = document.querySelector('.Post-RichText')?.textContent
if (title) {
return cropText(
`以下是一篇文章,给出相应的摘要,以及你对此的看法.标题是:"${title}",内容是:\n"${description}"`,
)
}
}
} catch (e) {
console.log(e)
}
},
}
+236
View File
@@ -0,0 +1,236 @@
[data-theme='auto'] {
@import 'github-markdown-css/github-markdown.css';
@media screen and (prefers-color-scheme: dark) {
@import 'highlight.js/scss/github-dark.scss';
--font-color: #c9d1d9;
--theme-color: #202124;
--theme-border-color: #3c4043;
--dragbar-color: #3c4043;
}
@media screen and (prefers-color-scheme: light) {
@import 'highlight.js/scss/github.scss';
--font-color: #24292f;
--theme-color: #eaecf0;
--theme-border-color: #aeafb2;
--dragbar-color: #dfe0e1;
}
}
[data-theme='dark'] {
@import 'highlight.js/scss/github-dark.scss';
@import 'github-markdown-css/github-markdown-dark.css';
--font-color: #c9d1d9;
--theme-color: #202124;
--theme-border-color: #3c4043;
--dragbar-color: #3c4043;
}
[data-theme='light'] {
@import 'highlight.js/scss/github.scss';
@import 'github-markdown-css/github-markdown-light.css';
--font-color: #24292f;
--theme-color: #eaecf0;
--theme-border-color: #aeafb2;
--dragbar-color: #ccced0;
}
.sidebar-free {
margin-left: 60px;
}
.chat-gpt-container {
width: 100%;
flex-basis: 0;
flex-grow: 1;
margin-bottom: 20px;
.gpt-inner {
border-radius: 8px;
border: 1px solid;
overflow: hidden;
border-color: var(--theme-border-color);
background-color: var(--theme-color);
margin: 0;
hr {
height: 1px;
background-color: var(--theme-border-color);
border: none;
}
}
.markdown-body {
padding: 5px 15px 10px;
background-color: var(--theme-color);
color: var(--font-color);
max-height: 800px;
overflow-y: auto;
ul,
ol {
padding-left: 1.5em;
}
ol {
list-style: none;
counter-reset: item;
li {
counter-increment: item;
&:before {
content: counter(item) '. ';
margin-left: -0.75em;
}
}
}
}
.icon-and-text {
color: var(--font-color);
display: flex;
align-items: center;
padding: 15px;
gap: 6px;
}
.manual-btn {
cursor: pointer;
}
.gpt-loading {
color: var(--font-color);
animation: gpt-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.code-copy-btn {
color: inherit;
position: absolute;
right: 10px;
top: 3px;
cursor: pointer;
}
:is(.answer, .question, .error) {
font-size: 15px;
line-height: 1.6;
border-radius: 8px;
word-break: break-all;
pre {
margin-top: 10px;
}
& > p {
margin-bottom: 10px;
}
code {
white-space: pre-wrap;
word-break: break-word;
border-radius: 8px;
.hljs {
padding: 0;
}
}
p {
margin: 0;
}
}
.gpt-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
color: var(--font-color);
p {
font-weight: bold;
}
.gpt-feedback {
display: flex;
gap: 6px;
cursor: pointer;
}
.gpt-feedback-selected {
color: #f08080;
}
.gpt-util-icon {
cursor: pointer;
}
}
.error {
color: #ec4336;
}
.interact-input {
box-sizing: border-box;
padding: 5px 15px;
border: 0;
border-top: 1px solid var(--theme-border-color);
width: 100%;
background-color: var(--theme-color);
color: var(--font-color);
resize: none;
max-height: 240px;
}
.dragbar {
cursor: move;
width: 250px;
height: 12px;
border-radius: 10px;
background-color: var(--dragbar-color);
}
}
@keyframes gpt-pulse {
0%,
100% {
opacity: 1;
}
50% {
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: 2px;
padding: 2px;
border-radius: 30px;
background-color: #ffffff;
color: #24292f;
cursor: pointer;
}
.gpt-selection-toolbar-button:hover {
background-color: #d4d5da;
}
.gpt-selection-window {
width: 600px;
height: auto;
border-radius: 8px;
background-color: var(--theme-color);
box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
}