Skip to content

Commit

Permalink
Allow users to add new prompt for different sites (#2)
Browse files Browse the repository at this point in the history
* Install @geist-ui/icons package

* Add PromptCard component

* Update <main> tag width from 500 to 600

* Update PromptCard save button style

* Display error toast when saving new prompt fails

* Handle onClick event of remove icon in PromptCard

* Display prompt overides using PromptCard

* Add AddNewPromptModal component

* Use matching prompt for current hostname

* Fix: PromptCard disappears when prompt is updated

* Validate input first when adding new prompt

* Initialize states in AddNewPromptModal before close
  • Loading branch information
joonhok-fittube authored Feb 11, 2023
1 parent 774608e commit 98dbeb4
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 45 deletions.
16 changes: 16 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 @@ -10,6 +10,7 @@
},
"dependencies": {
"@geist-ui/core": "^2.3.8",
"@geist-ui/icons": "^1.0.2",
"@primer/octicons-react": "^17.9.0",
"eventsource-parser": "^0.0.5",
"expiry-map": "^2.0.0",
Expand Down
11 changes: 1 addition & 10 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,12 @@ export interface SitePrompt {
prompt: string
}

export interface AllPrompt {
default: string
sites: SitePrompt[]
}

const promptDefaultValue = {
default: Prompt,
sites: [],
}

const userConfigWithDefaultValue = {
triggerMode: TriggerMode.Always,
theme: Theme.Auto,
language: Language.Auto,
prompt: Prompt,
promptOverrides: [] as SitePrompt[],
}

export type UserConfig = typeof userConfigWithDefaultValue
Expand Down
5 changes: 3 additions & 2 deletions src/content-script/ChatGPTCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ChatGPTQuery, { QueryStatus } from './ChatGPTQuery'

interface Props {
question: string
promptSource: string
triggerMode: TriggerMode
onStatusChange?: (status: QueryStatus) => void
}
Expand All @@ -13,10 +14,10 @@ function ChatGPTCard(props: Props) {
const [triggered, setTriggered] = useState(false)

if (props.triggerMode === TriggerMode.Always) {
return <ChatGPTQuery question={props.question} onStatusChange={props.onStatusChange} />
return <ChatGPTQuery {...props} />
}
if (triggered) {
return <ChatGPTQuery question={props.question} onStatusChange={props.onStatusChange} />
return <ChatGPTQuery {...props} />
}
return (
<p className="icon-and-text cursor-pointer" onClick={() => setTriggered(true)}>
Expand Down
2 changes: 2 additions & 0 deletions src/content-script/ChatGPTContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { QueryStatus } from './ChatGPTQuery'

interface Props {
question: string
promptSource: string
triggerMode: TriggerMode
}

Expand All @@ -22,6 +23,7 @@ function ChatGPTContainer(props: Props) {
<div className="chat-gpt-card">
<ChatGPTCard
question={props.question}
promptSource={props.promptSource}
triggerMode={props.triggerMode}
onStatusChange={setQueryStatus}
/>
Expand Down
2 changes: 2 additions & 0 deletions src/content-script/ChatGPTQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type QueryStatus = 'success' | 'error' | undefined

interface Props {
question: string
promptSource: string
onStatusChange?: (status: QueryStatus) => void
}

Expand Down Expand Up @@ -85,6 +86,7 @@ function ChatGPTQuery(props: Props) {
<span className="cursor-pointer leading-[0]" onClick={openOptionsPage}>
<GearIcon size={14} />
</span>
<span className="mx-2 text-base text-gray-500">{`"${props.promptSource}" prompt is used`}</span>
<ChatGPTFeedback
messageId={answer.messageId}
conversationId={answer.conversationId}
Expand Down
16 changes: 13 additions & 3 deletions src/content-script/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { config, SearchEngine } from './search-engine-configs'
import './styles.scss'
import { getPossibleElementByQuerySelector } from './utils'

async function mount(question: string, siteConfig: SearchEngine) {
async function mount(question: string, promptSource: string, siteConfig: SearchEngine) {
const container = document.createElement('div')
container.className = 'chat-gpt-container'

Expand Down Expand Up @@ -36,7 +36,11 @@ async function mount(question: string, siteConfig: SearchEngine) {
}

render(
<ChatGPTContainer question={question} triggerMode={userConfig.triggerMode || 'always'} />,
<ChatGPTContainer
question={question}
promptSource={promptSource}
triggerMode={userConfig.triggerMode || 'always'}
/>,
container,
)
}
Expand All @@ -58,7 +62,13 @@ async function run() {
console.log('Body: ' + bodyInnerText)
const userConfig = await getUserConfig()

mount(userConfig.prompt + bodyInnerText, siteConfig)
const found = userConfig.promptOverrides.find(
(override) => new URL(override.site).hostname === location.hostname,
)
const question = found?.prompt ?? userConfig.prompt
const promptSource = found?.site ?? 'default'

mount(question + bodyInnerText, promptSource, siteConfig)
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/content-script/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,13 @@ export async function shouldShowRatingTip() {
await Browser.storage.local.set({ ratingTipShowTimes: ratingTipShowTimes + 1 })
return ratingTipShowTimes >= 2
}

export function isValidHttpUrl(string: string) {
let url
try {
url = new URL(string)
} catch (_) {
return false
}
return url.protocol === 'http:' || url.protocol === 'https:'
}
93 changes: 93 additions & 0 deletions src/options/AddNewPromptModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Input, Modal, Text, Textarea, useToasts } from '@geist-ui/core'
import { useState } from 'preact/hooks'
import { isValidHttpUrl } from '../content-script/utils'

function AddNewPromptModal(props: {
visible: boolean
onClose: () => void
onSave: (newOverride: { site: string; prompt: string }) => Promise<void>
}) {
const { visible, onClose, onSave } = props
const [site, setSite] = useState<string>('')
const [siteError, setSiteError] = useState<boolean>(false)
const [prompt, setPrompt] = useState<string>('')
const [promptError, setPromptError] = useState<boolean>(false)
const { setToast } = useToasts()

function validateInput() {
const isSiteValid = isValidHttpUrl(site)
setSiteError(!isSiteValid)
if (!isSiteValid) {
return false
}
const isPromptValid = prompt.trim().length > 0
setPromptError(!isPromptValid)
return isPromptValid
}

function close() {
setSite('')
setSiteError(false)
setPrompt('')
setPromptError(false)
onClose()
}

return (
<Modal visible={visible} onClose={onClose}>
<Modal.Title>Add New Prompt</Modal.Title>
<Modal.Content>
<Input
width={'100%'}
clearable
label="site"
placeholder="https://arxiv.org/"
onChange={(e) => setSite(e.target.value)}
>
{siteError && (
<Text small type="error">
Site is not valid
</Text>
)}
</Input>
{promptError && (
<div className="mt-3 mb-2 px-1">
<Text small type="error">
Prompt cannot be empty
</Text>
</div>
)}
<Textarea
my={promptError ? 0 : 1}
value={prompt}
width="100%"
height="10em"
placeholder="Type prompt here"
onChange={(event) => setPrompt(event.target.value)}
/>
</Modal.Content>
<Modal.Action passive onClick={() => close()}>
Cancel
</Modal.Action>
<Modal.Action
onClick={() => {
if (!validateInput()) {
return
}
onSave({ site, prompt })
.then(() => {
setToast({ text: 'New Prompt saved', type: 'success' })
close()
})
.catch(() => {
setToast({ text: 'Failed to save prompt', type: 'error' })
})
}}
>
Save
</Modal.Action>
</Modal>
)
}

export default AddNewPromptModal
Loading

0 comments on commit 98dbeb4

Please sign in to comment.