{conversationItemData.map((data, idx) => (
}
return (
-
setTriggered(true)}
- >
- {t('Ask ChatGPT')}
+
setTriggered(true)}>
+
+ {t('Ask ChatGPT')}
+
)
case 'questionMark':
@@ -95,18 +94,19 @@ function DecisionCard(props) {
return
}
return (
-
setTriggered(true)}
- >
- {t('Ask ChatGPT')}
+
setTriggered(true)}>
+
+ {t('Ask ChatGPT')}
+
)
}
else
return (
-
- {t('No Input Found')}
+
+
+ {t('No Input Found')}
+
)
})()}
diff --git a/src/components/DeleteButton/index.jsx b/src/components/DeleteButton/index.jsx
index 385ad03..7f0f561 100644
--- a/src/components/DeleteButton/index.jsx
+++ b/src/components/DeleteButton/index.jsx
@@ -6,9 +6,10 @@ import { TrashIcon } from '@primer/octicons-react'
DeleteButton.propTypes = {
onConfirm: PropTypes.func.isRequired,
size: PropTypes.number.isRequired,
+ text: PropTypes.string.isRequired,
}
-function DeleteButton({ onConfirm, size }) {
+function DeleteButton({ onConfirm, size, text }) {
const { t } = useTranslation()
const [waitConfirm, setWaitConfirm] = useState(false)
const confirmRef = useRef(null)
@@ -38,7 +39,7 @@ function DeleteButton({ onConfirm, size }) {
{t('Confirm')}
{
diff --git a/src/config/index.mjs b/src/config/index.mjs
index 94fce5b..cace2ce 100644
--- a/src/config/index.mjs
+++ b/src/config/index.mjs
@@ -139,18 +139,19 @@ export function getNavigatorLanguage() {
return navigator.language.substring(0, 2)
}
-export function isUsingApiKey(config) {
+export function isUsingApiKey(configOrSession) {
return (
- gptApiModelKeys.includes(config.modelName) || chatgptApiModelKeys.includes(config.modelName)
+ gptApiModelKeys.includes(configOrSession.modelName) ||
+ chatgptApiModelKeys.includes(configOrSession.modelName)
)
}
-export function isUsingMultiModeModel(config) {
- return bingWebModelKeys.includes(config.modelName)
+export function isUsingMultiModeModel(configOrSession) {
+ return bingWebModelKeys.includes(configOrSession.modelName)
}
-export function isUsingCustomModel(config) {
- return customApiModelKeys.includes(config.modelName)
+export function isUsingCustomModel(configOrSession) {
+ return customApiModelKeys.includes(configOrSession.modelName)
}
export async function getPreferredLanguageKey() {
diff --git a/src/config/localSession.mjs b/src/config/localSession.mjs
new file mode 100644
index 0000000..c7d9984
--- /dev/null
+++ b/src/config/localSession.mjs
@@ -0,0 +1,61 @@
+import Browser from 'webextension-polyfill'
+import { initSession } from '../utils/index.mjs'
+import { getUserConfig } from './index.mjs'
+
+export const initDefaultSession = async () => {
+ const config = await getUserConfig()
+ const modelName = config.modelName
+ return initSession({
+ sessionName: new Date().toLocaleString(),
+ modelName: modelName,
+ autoClean: false,
+ })
+}
+
+export const createSession = async (session) => {
+ const currentSessions = await getSessions()
+ if (!session) session = await initDefaultSession()
+ currentSessions.unshift(session)
+ await Browser.storage.local.set({ sessions: currentSessions })
+ return { session, currentSessions }
+}
+
+export const deleteSession = async (sessionId) => {
+ const currentSessions = await getSessions()
+ const index = currentSessions.findIndex((session) => session.sessionId === sessionId)
+ currentSessions.splice(index, 1)
+ if (currentSessions.length > 0) {
+ await Browser.storage.local.set({ sessions: currentSessions })
+ return currentSessions
+ }
+ return await resetSessions()
+}
+
+export const getSession = async (sessionId) => {
+ const currentSessions = await getSessions()
+ return {
+ session: currentSessions.find((session) => session.sessionId === sessionId),
+ currentSessions,
+ }
+}
+
+export const updateSession = async (newSession) => {
+ const currentSessions = await getSessions()
+ currentSessions[
+ currentSessions.findIndex((session) => session.sessionId === newSession.sessionId)
+ ] = newSession
+ await Browser.storage.local.set({ sessions: currentSessions })
+ return currentSessions
+}
+
+export const resetSessions = async () => {
+ const currentSessions = [await initDefaultSession()]
+ await Browser.storage.local.set({ sessions: currentSessions })
+ return currentSessions
+}
+
+export const getSessions = async () => {
+ const { sessions } = await Browser.storage.local.get('sessions')
+ if (sessions && sessions.length > 0) return sessions
+ return await resetSessions()
+}
diff --git a/src/content-script/styles.scss b/src/content-script/styles.scss
index 12771b3..6aafe12 100644
--- a/src/content-script/styles.scss
+++ b/src/content-script/styles.scss
@@ -1,4 +1,5 @@
@import '../fonts/styles.css';
+@import '../pages/styles.scss';
[data-theme='auto'] {
@import 'github-markdown-css/github-markdown.css';
@@ -54,6 +55,9 @@
margin-bottom: 20px;
.gpt-inner {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
border-radius: 8px;
border: 1px solid;
overflow: hidden;
@@ -73,7 +77,6 @@
padding: 5px 15px 10px;
background-color: var(--theme-color);
color: var(--font-color);
- resize: vertical;
overflow-y: auto;
ul,
@@ -206,6 +209,7 @@
background-color: var(--theme-color);
color: var(--font-color);
resize: none;
+ min-height: 70px;
max-height: 240px;
}
diff --git a/src/hooks/use-config.mjs b/src/hooks/use-config.mjs
index 13998be..784ea72 100644
--- a/src/hooks/use-config.mjs
+++ b/src/hooks/use-config.mjs
@@ -12,6 +12,8 @@ export function useConfig(initFn) {
}, [])
useEffect(() => {
const listener = (changes) => {
+ if (Object.keys(changes).length === 1 && 'sessions' in changes) return
+
const changedItems = Object.keys(changes)
let newConfig = {}
for (const key of changedItems) {
diff --git a/src/pages/IndependentPanel/App.jsx b/src/pages/IndependentPanel/App.jsx
new file mode 100644
index 0000000..acbfdb3
--- /dev/null
+++ b/src/pages/IndependentPanel/App.jsx
@@ -0,0 +1,156 @@
+import {
+ createSession,
+ resetSessions,
+ getSessions,
+ updateSession,
+ getSession,
+ deleteSession,
+} from '../../config/localSession'
+import { useEffect, useRef, useState } from 'react'
+import './styles.scss'
+import { useConfig } from '../../hooks/use-config.mjs'
+import { useTranslation } from 'react-i18next'
+import ConfirmButton from '../../components/ConfirmButton'
+import ConversationCard from '../../components/ConversationCard'
+import DeleteButton from '../../components/DeleteButton'
+
+function App() {
+ const { t } = useTranslation()
+ const [collapsed, setCollapsed] = useState(false)
+ const config = useConfig()
+ const [sessions, setSessions] = useState([])
+ const [sessionId, setSessionId] = useState(null)
+ const [currentSession, setCurrentSession] = useState(null)
+ const [renderContent, setRenderContent] = useState(false)
+ const currentPort = useRef(null)
+
+ const setSessionIdSafe = async (sessionId) => {
+ if (currentPort.current) {
+ try {
+ currentPort.current.postMessage({ stop: true })
+ currentPort.current.disconnect()
+ } catch (e) {
+ /* empty */
+ }
+ currentPort.current = null
+ }
+ const { session, currentSessions } = await getSession(sessionId)
+ if (session) setSessionId(sessionId)
+ else if (currentSessions.length > 0) setSessionId(currentSessions[0].sessionId)
+ }
+
+ useEffect(() => {
+ document.documentElement.dataset.theme = config.themeMode
+ }, [config.themeMode])
+
+ useEffect(() => {
+ // eslint-disable-next-line
+ ;(async () => {
+ const sessions = await getSessions()
+ setSessions(sessions)
+ await setSessionIdSafe(sessions[0].sessionId)
+ })()
+ }, [])
+
+ useEffect(() => {
+ // eslint-disable-next-line
+ ;(async () => {
+ if (sessions.length > 0) {
+ setCurrentSession((await getSession(sessionId)).session)
+ setRenderContent(false)
+ setTimeout(() => {
+ setRenderContent(true)
+ })
+ }
+ })()
+ }, [sessionId])
+
+ const toggleSidebar = () => {
+ setCollapsed(!collapsed)
+ }
+
+ const createNewChat = async () => {
+ const { session, currentSessions } = await createSession()
+ setSessions(currentSessions)
+ await setSessionIdSafe(session.sessionId)
+ }
+
+ const clearConversations = async () => {
+ const sessions = await resetSessions()
+ setSessions(sessions)
+ await setSessionIdSafe(sessions[0].sessionId)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {sessions.map(
+ (
+ session,
+ index, // TODO editable session name
+ ) => (
+
+ ),
+ )}
+
+
+
+
+
+
+
+
+ {renderContent && currentSession && currentSession.conversationRecords && (
+
+ {
+ currentPort.current = port
+ if (cData.length > 0 && cData[cData.length - 1].done) {
+ updateSession(session).then(setSessions)
+ setCurrentSession(session)
+ }
+ }}
+ />
+
+ )}
+
+
+
+ )
+}
+
+export default App
diff --git a/src/pages/IndependentPanel/index.html b/src/pages/IndependentPanel/index.html
new file mode 100644
index 0000000..86a27c9
--- /dev/null
+++ b/src/pages/IndependentPanel/index.html
@@ -0,0 +1,13 @@
+
+
+ ChatGPTBox
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/IndependentPanel/index.jsx b/src/pages/IndependentPanel/index.jsx
new file mode 100644
index 0000000..adcbebb
--- /dev/null
+++ b/src/pages/IndependentPanel/index.jsx
@@ -0,0 +1,19 @@
+import { render } from 'preact'
+import '../../_locales/i18n-react'
+import App from './App'
+import Browser from 'webextension-polyfill'
+import { changeLanguage } from 'i18next'
+import { getPreferredLanguageKey } from '../../config/index.mjs'
+
+document.body.style.margin = 0
+document.body.style.overflow = 'hidden'
+getPreferredLanguageKey().then((lang) => {
+ changeLanguage(lang)
+})
+Browser.runtime.onMessage.addListener(async (message) => {
+ if (message.type === 'CHANGE_LANG') {
+ const data = message.data
+ changeLanguage(data.lang)
+ }
+})
+render(, document.getElementById('app'))
diff --git a/src/pages/IndependentPanel/styles.scss b/src/pages/IndependentPanel/styles.scss
new file mode 100644
index 0000000..41a20fc
--- /dev/null
+++ b/src/pages/IndependentPanel/styles.scss
@@ -0,0 +1,131 @@
+[data-theme='auto'] {
+ @media screen and (prefers-color-scheme: dark) {
+ --font-color: #c9d1d9;
+ --font-active-color: #ffffff;
+ --theme-color: #202124;
+ --theme-border-color: #3c4043;
+ --active-color: #3c4043;
+ }
+ @media screen and (prefers-color-scheme: light) {
+ --font-color: #24292f;
+ --font-active-color: #cc3333;
+ --theme-color: #ffffff;
+ --theme-border-color: #aeafb2;
+ --active-color: #d0d4da;
+ }
+}
+
+[data-theme='dark'] {
+ --font-color: #c9d1d9;
+ --font-active-color: #ffffff;
+ --theme-color: #202124;
+ --theme-border-color: #3c4043;
+ --active-color: #3c4043;
+}
+
+[data-theme='light'] {
+ --font-color: #24292f;
+ --font-active-color: #cc3333;
+ --theme-color: #ffffff;
+ --theme-border-color: #aeafb2;
+ --active-color: #d0d4da;
+}
+
+.IndependentPanel * {
+ font-family: 'Cairo', sans-serif;
+ font-size: 14px;
+}
+
+.IndependentPanel {
+ .chat-container {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ }
+
+ .chat-sidebar {
+ display: flex;
+ flex-direction: column;
+ width: 250px;
+ background-color: var(--theme-color);
+ transition: width 0.3s;
+ padding: 10px;
+ }
+
+ .chat-sidebar.collapsed {
+ width: 60px;
+ }
+
+ .chat-sidebar:hover,
+ .chat-sidebar:not(.collapsed) {
+ width: 250px;
+ }
+
+ .chat-sidebar-button-group {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ background-color: var(--theme-color);
+ gap: 15px;
+ }
+
+ .chat-list {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ padding: 0;
+ background-color: var(--theme-color);
+ overflow-y: auto;
+ overflow-x: hidden;
+ gap: 15px;
+ }
+
+ .chat-content {
+ flex-grow: 1;
+ border: 1px solid var(--theme-border-color);
+ background-color: var(--theme-color);
+ }
+
+ .normal-button {
+ width: 100%;
+ min-height: 40px;
+ padding: 1px 6px;
+ border: 1px solid;
+ border-color: var(--theme-border-color);
+ background-color: var(--theme-color);
+ color: var(--font-color);
+ border-radius: 5px;
+ cursor: pointer;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ .gpt-util-group {
+ display: flex;
+ gap: 15px;
+ align-items: center;
+ }
+
+ .gpt-util-icon {
+ display: flex;
+ cursor: pointer;
+ align-items: center;
+ color: var(--font-color);
+ }
+ .gpt-util-icon:hover {
+ color: var(--font-active-color);
+ }
+
+ .normal-button.active,
+ .normal-button:hover {
+ background-color: var(--active-color);
+ }
+
+ hr {
+ height: 1px;
+ background-color: var(--theme-border-color);
+ border: none;
+ margin: 15px 0;
+ }
+}
diff --git a/src/pages/styles.scss b/src/pages/styles.scss
new file mode 100644
index 0000000..cc03b48
--- /dev/null
+++ b/src/pages/styles.scss
@@ -0,0 +1 @@
+@import 'IndependentPanel/styles.scss';
diff --git a/src/utils/init-session.mjs b/src/utils/init-session.mjs
index a69d26b..8058a59 100644
--- a/src/utils/init-session.mjs
+++ b/src/utils/init-session.mjs
@@ -1,3 +1,5 @@
+import { Models } from '../config/index.mjs'
+
/**
* @typedef {object} BingWeb
* @property {string|null} conversationSignature
@@ -9,9 +11,12 @@
* @typedef {object} Session
* @property {string|null} question
* @property {Object[]|null} conversationRecords
- * @property {boolean} isRetry
+ * @property {string|null} sessionName
+ * @property {string|null} sessionId
* @property {string|null} aiName
* @property {string|null} modelName
+ * @property {boolean|null} autoClean
+ * @property {boolean} isRetry
* @property {string|null} conversationId - chatGPT web mode
* @property {string|null} messageId - chatGPT web mode
* @property {string|null} parentMessageId - chatGPT web mode
@@ -20,16 +25,31 @@
/**
* @param {string|null} question
* @param {Object[]|null} conversationRecords
+ * @param {string|null} sessionName
+ * @param {string|null} modelName
+ * @param {boolean|null} autoClean
* @returns {Session}
*/
-export function initSession({ question = null, conversationRecords = [] } = {}) {
+export function initSession({
+ question = null,
+ conversationRecords = [],
+ sessionName = null,
+ modelName = null,
+ autoClean = true,
+} = {}) {
return {
// common
question,
conversationRecords,
+
+ sessionName,
+ sessionId: crypto.randomUUID(),
+
+ aiName: modelName ? Models[modelName].desc : null,
+ modelName,
+
+ autoClean,
isRetry: false,
- aiName: null,
- modelName: null,
// chatgpt-web
conversationId: null,