import { memo, useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import Browser from 'webextension-polyfill'
import InputBox from '../InputBox'
import ConversationItem from '../ConversationItem'
import { createElementAtPosition, isSafari } from '../../utils'
import { DownloadIcon, LinkExternalIcon, ArchiveIcon } from '@primer/octicons-react'
import { WindowDesktop, XLg, Pin } from 'react-bootstrap-icons'
import FileSaver from 'file-saver'
import { render } from 'preact'
import FloatingToolbar from '../FloatingToolbar'
import { useClampWindowSize } from '../../hooks/use-clamp-window-size'
import { ModelMode, Models } from '../../config/index.mjs'
import { useTranslation } from 'react-i18next'
import DeleteButton from '../DeleteButton'
import { useConfig } from '../../hooks/use-config.mjs'
import { createSession } from '../../services/local-session.mjs'
import { v4 as uuidv4 } from 'uuid'
import { initSession } from '../../services/init-session.mjs'
import { findLastIndex } from 'lodash-es'
const logo = Browser.runtime.getURL('logo.png')
class ConversationItemData extends Object {
/**
* @param {'question'|'answer'|'error'} type
* @param {string} content
* @param {bool} done
*/
constructor(type, content, done = false) {
super()
this.type = type
this.content = content
this.done = done
}
}
function ConversationCard(props) {
const { t } = useTranslation()
const [isReady, setIsReady] = useState(!props.question)
const [port, setPort] = useState(() => Browser.runtime.connect())
const [session, setSession] = useState(props.session)
const windowSize = useClampWindowSize([750, 1500], [250, 1100])
const bodyRef = useRef(null)
/**
* @type {[ConversationItemData[], (conversationItemData: ConversationItemData[]) => void]}
*/
const [conversationItemData, setConversationItemData] = useState(
(() => {
if (session.conversationRecords.length === 0)
if (props.question)
return [
new ConversationItemData(
'answer',
`
${t(`Waiting for response...`)}
`,
),
]
else return []
else {
const ret = []
for (const record of session.conversationRecords) {
ret.push(new ConversationItemData('question', record.question + '\n', true))
ret.push(new ConversationItemData('answer', record.answer + '\n', true))
}
return ret
}
})(),
)
const config = useConfig()
useEffect(() => {
if (props.onUpdate) props.onUpdate(port, session, conversationItemData)
}, [session, conversationItemData])
useEffect(() => {
bodyRef.current.scrollTop = bodyRef.current.scrollHeight
}, [session])
useEffect(() => {
if (config.lockWhenAnswer) bodyRef.current.scrollTop = bodyRef.current.scrollHeight
}, [conversationItemData])
useEffect(() => {
// when the page is responsive, session may accumulate redundant data and needs to be cleared after remounting and before making a new request
if (props.question) {
const newSession = initSession({ question: props.question })
setSession(newSession)
port.postMessage({ session: newSession })
}
}, [props.question]) // usually only triggered once
/**
* @param {string} value
* @param {boolean} appended
* @param {'question'|'answer'|'error'} newType
* @param {boolean} done
*/
const updateAnswer = (value, appended, newType, done = false) => {
setConversationItemData((old) => {
const copy = [...old]
const index = findLastIndex(copy, (v) => v.type === 'answer' || v.type === 'error')
if (index === -1) return copy
copy[index] = new ConversationItemData(
newType,
appended ? copy[index].content + value : value,
)
copy[index].done = done
return copy
})
}
useEffect(() => {
const listener = () => {
setPort(Browser.runtime.connect())
setIsReady(true)
}
port.onDisconnect.addListener(listener)
return () => {
port.onDisconnect.removeListener(listener)
}
}, [port])
useEffect(() => {
const listener = (msg) => {
if (msg.answer) {
updateAnswer(msg.answer, false, 'answer')
}
if (msg.session) {
if (msg.done) msg.session = { ...msg.session, isRetry: false }
setSession(msg.session)
}
if (msg.done) {
updateAnswer('\n', true, 'answer', true)
setIsReady(true)
}
if (msg.error) {
switch (msg.error) {
case 'UNAUTHORIZED':
updateAnswer(
`${t('UNAUTHORIZED')} ${t('Please login at https://chat.openai.com first')}${
isSafari() ? ` ${t('Then open https://chat.openai.com/api/auth/session')}` : ''
} ${t('And refresh this page or type you question again')}` +
`
${t(
'Consider creating an api key at https://platform.openai.com/account/api-keys',
)}\n`,
false,
'error',
)
break
case 'CLOUDFLARE':
updateAnswer(
`${t('OpenAI Security Check Required')} ${
isSafari()
? t('Please open https://chat.openai.com/api/auth/session')
: t('Please open https://chat.openai.com')
} ${t('And refresh this page or type you question again')}` +
`