Skip to content

Commit 8706737

Browse files
committed
WIP, custom API Modes
1 parent 08f2c54 commit 8706737

File tree

8 files changed

+136
-52
lines changed

8 files changed

+136
-52
lines changed

src/components/ConversationCard/index.jsx

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { memo, useEffect, useMemo, useRef, useState } from 'react'
1+
import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
22
import PropTypes from 'prop-types'
33
import Browser from 'webextension-polyfill'
44
import InputBox from '../InputBox'
55
import ConversationItem from '../ConversationItem'
66
import {
7+
apiModeToModelName,
78
createElementAtPosition,
8-
getApiModesStringArrayFromConfig,
9+
getApiModesFromConfig,
10+
isApiModeSelected,
911
isFirefox,
1012
isMobile,
1113
isSafari,
@@ -61,33 +63,33 @@ function ConversationCard(props) {
6163
const [completeDraggable, setCompleteDraggable] = useState(false)
6264
// `.some` for multi mode models. e.g. bingFree4-balanced
6365
const useForegroundFetch = bingWebModelKeys.some((n) => session.modelName.includes(n))
66+
const [apiModes, setApiModes] = useState([])
6467

6568
/**
6669
* @type {[ConversationItemData[], (conversationItemData: ConversationItemData[]) => void]}
6770
*/
68-
const [conversationItemData, setConversationItemData] = useState(
69-
(() => {
70-
if (session.conversationRecords.length === 0)
71-
if (props.question && triggered)
72-
return [
73-
new ConversationItemData(
74-
'answer',
75-
`<p class="gpt-loading">${t(`Waiting for response...`)}</p>`,
76-
),
77-
]
78-
else return []
79-
else {
80-
const ret = []
81-
for (const record of session.conversationRecords) {
82-
ret.push(new ConversationItemData('question', record.question, true))
83-
ret.push(new ConversationItemData('answer', record.answer, true))
84-
}
85-
return ret
86-
}
87-
})(),
88-
)
71+
const [conversationItemData, setConversationItemData] = useState([])
8972
const config = useConfig()
9073

74+
useLayoutEffect(() => {
75+
if (session.conversationRecords.length === 0) {
76+
if (props.question && triggered)
77+
setConversationItemData([
78+
new ConversationItemData(
79+
'answer',
80+
`<p class="gpt-loading">${t(`Waiting for response...`)}</p>`,
81+
),
82+
])
83+
} else {
84+
const ret = []
85+
for (const record of session.conversationRecords) {
86+
ret.push(new ConversationItemData('question', record.question, true))
87+
ret.push(new ConversationItemData('answer', record.answer, true))
88+
}
89+
setConversationItemData(ret)
90+
}
91+
}, [])
92+
9193
useEffect(() => {
9294
setCompleteDraggable(!isSafari() && !isFirefox() && !isMobile())
9395
}, [])
@@ -118,6 +120,15 @@ function ConversationCard(props) {
118120
}
119121
}, [props.question, triggered]) // usually only triggered once
120122

123+
useLayoutEffect(() => {
124+
setApiModes(getApiModesFromConfig(config, true))
125+
}, [
126+
config.activeApiModes,
127+
config.customApiModes,
128+
config.azureDeploymentName,
129+
config.ollamaModelName,
130+
])
131+
121132
/**
122133
* @param {string} value
123134
* @param {boolean} appended
@@ -369,25 +380,32 @@ function ConversationCard(props) {
369380
className="normal-button"
370381
required
371382
onChange={(e) => {
372-
const modelName = e.target.value
373-
const newSession = { ...session, modelName, aiName: modelNameToDesc(modelName, t) }
383+
const apiMode = apiModes[e.target.value]
384+
const modelName = apiModeToModelName(apiMode)
385+
const newSession = {
386+
...session,
387+
modelName,
388+
apiMode,
389+
aiName: modelNameToDesc(modelName, t),
390+
}
374391
if (config.autoRegenAfterSwitchModel && conversationItemData.length > 0)
375392
getRetryFn(newSession)()
376393
else setSession(newSession)
377394
}}
378395
>
379-
{getApiModesStringArrayFromConfig(config, true).map((modelName) => {
396+
{apiModes.map((apiMode, index) => {
397+
const modelName = apiModeToModelName(apiMode)
380398
const desc = modelNameToDesc(modelName, t)
381-
if (desc)
399+
if (desc) {
400+
let selected
401+
if (isApiModeSelected(apiMode, session)) selected = true
402+
else selected = session.modelName === modelName
382403
return (
383-
<option
384-
value={modelName}
385-
key={modelName}
386-
selected={modelName === session.modelName}
387-
>
404+
<option value={index} key={index} selected={selected}>
388405
{desc}
389406
</option>
390407
)
408+
}
391409
})}
392410
</select>
393411
</span>

src/config/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ export const defaultConfig = {
286286
themeMode: 'auto',
287287
/** @type {keyof Models}*/
288288
modelName: getNavigatorLanguage() === 'zh' ? 'moonshotWebFree' : 'claude2WebFree',
289+
apiMode: null,
289290

290291
preferredLanguage: getNavigatorLanguage(),
291292
clickIconAction: 'popup',

src/content-script/index.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ async function mountComponent(siteConfig) {
9191

9292
render(
9393
<FloatingToolbar
94-
session={initSession({ modelName: userConfig.modelName })}
94+
session={initSession({ modelName: userConfig.modelName, apiMode: userConfig.apiMode })}
9595
selection=""
9696
container={toolbarContainer}
9797
triggered={triggered}
@@ -107,7 +107,7 @@ async function mountComponent(siteConfig) {
107107
container.id = 'chatgptbox-container'
108108
render(
109109
<DecisionCard
110-
session={initSession({ modelName: userConfig.modelName })}
110+
session={initSession({ modelName: userConfig.modelName, apiMode: userConfig.apiMode })}
111111
question={question}
112112
siteConfig={siteConfig}
113113
container={container}
@@ -151,9 +151,10 @@ const deleteToolbar = () => {
151151

152152
const createSelectionTools = async (toolbarContainer, selection) => {
153153
toolbarContainer.className = 'chatgptbox-toolbar-container'
154+
const userConfig = await getUserConfig()
154155
render(
155156
<FloatingToolbar
156-
session={initSession({ modelName: (await getUserConfig()).modelName })}
157+
session={initSession({ modelName: userConfig.modelName, apiMode: userConfig.apiMode })}
157158
selection={selection}
158159
container={toolbarContainer}
159160
dockable={true}
@@ -276,9 +277,10 @@ async function prepareForRightClickMenu() {
276277
: { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 }
277278
const container = createElementAtPosition(position.x, position.y)
278279
container.className = 'chatgptbox-toolbar-container-not-queryable'
280+
const userConfig = await getUserConfig()
279281
render(
280282
<FloatingToolbar
281-
session={initSession({ modelName: (await getUserConfig()).modelName })}
283+
session={initSession({ modelName: userConfig.modelName, apiMode: userConfig.apiMode })}
282284
selection={data.selectionText}
283285
container={container}
284286
triggered={true}

src/popup/sections/ApiModes.jsx

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { useTranslation } from 'react-i18next'
22
import PropTypes from 'prop-types'
3-
import { apiModeToModelName, getApiModesFromConfig, modelNameToDesc } from '../../utils/index.mjs'
3+
import {
4+
apiModeToModelName,
5+
getApiModesFromConfig,
6+
isApiModeSelected,
7+
modelNameToDesc,
8+
} from '../../utils/index.mjs'
49
import { PencilIcon, TrashIcon } from '@primer/octicons-react'
5-
import { useState } from 'react'
10+
import { useLayoutEffect, useState } from 'react'
611
import {
712
AlwaysCustomGroups,
813
CustomApiKeyGroups,
@@ -30,7 +35,31 @@ export function ApiModes({ config, updateConfig }) {
3035
const [editing, setEditing] = useState(false)
3136
const [editingApiMode, setEditingApiMode] = useState(defaultApiMode)
3237
const [editingIndex, setEditingIndex] = useState(-1)
33-
const apiModes = getApiModesFromConfig(config)
38+
const [apiModes, setApiModes] = useState([])
39+
const [apiModeStringArray, setApiModeStringArray] = useState([])
40+
41+
useLayoutEffect(() => {
42+
const apiModes = getApiModesFromConfig(config)
43+
setApiModes(apiModes)
44+
setApiModeStringArray(apiModes.map(apiModeToModelName))
45+
}, [
46+
config.activeApiModes,
47+
config.customApiModes,
48+
config.azureDeploymentName,
49+
config.ollamaModelName,
50+
])
51+
52+
const updateWhenApiModeDisabled = (apiMode) => {
53+
if (isApiModeSelected(apiMode, config) || config.modelName === apiModeToModelName(apiMode))
54+
updateConfig({
55+
modelName:
56+
apiModeStringArray.includes(config.modelName) &&
57+
config.modelName !== apiModeToModelName(apiMode)
58+
? config.modelName
59+
: 'customModel',
60+
apiMode: null,
61+
})
62+
}
3463

3564
const editingComponent = (
3665
<div style={{ display: 'flex', flexDirection: 'column', '--spacing': '4px' }}>
@@ -52,6 +81,12 @@ export function ApiModes({ config, updateConfig }) {
5281
customApiModes: [...apiModes, editingApiMode],
5382
})
5483
} else {
84+
const apiMode = apiModes[editingIndex]
85+
if (
86+
isApiModeSelected(apiMode, config) ||
87+
config.modelName === apiModeToModelName(apiMode) // there is a minor bug here, but it's not a big issue
88+
)
89+
updateConfig({ apiMode: editingApiMode })
5590
const customApiModes = [...apiModes]
5691
customApiModes[editingIndex] = editingApiMode
5792
updateConfig({ activeApiModes: [], customApiModes })
@@ -142,6 +177,7 @@ export function ApiModes({ config, updateConfig }) {
142177
type="checkbox"
143178
checked={apiMode.active}
144179
onChange={(e) => {
180+
if (!e.target.checked) updateWhenApiModeDisabled(apiMode)
145181
const customApiModes = [...apiModes]
146182
customApiModes[index] = { ...apiMode, active: e.target.checked }
147183
updateConfig({ activeApiModes: [], customApiModes })
@@ -165,6 +201,7 @@ export function ApiModes({ config, updateConfig }) {
165201
style={{ cursor: 'pointer' }}
166202
onClick={(e) => {
167203
e.preventDefault()
204+
updateWhenApiModeDisabled(apiMode)
168205
const customApiModes = [...apiModes]
169206
customApiModes.splice(index, 1)
170207
updateConfig({ activeApiModes: [], customApiModes })

src/popup/sections/GeneralPart.jsx

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { useTranslation } from 'react-i18next'
2-
import { useState } from 'react'
2+
import { useLayoutEffect, useState } from 'react'
33
import FileSaver from 'file-saver'
4-
import { openUrl, modelNameToDesc, getApiModesStringArrayFromConfig } from '../../utils/index.mjs'
4+
import {
5+
openUrl,
6+
modelNameToDesc,
7+
isApiModeSelected,
8+
getApiModesFromConfig,
9+
apiModeToModelName,
10+
} from '../../utils/index.mjs'
511
import {
612
isUsingOpenAiApiKey,
713
isUsingAzureOpenAi,
@@ -83,6 +89,16 @@ async function checkBilling(apiKey, apiUrl) {
8389
export function GeneralPart({ config, updateConfig }) {
8490
const { t, i18n } = useTranslation()
8591
const [balance, setBalance] = useState(null)
92+
const [apiModes, setApiModes] = useState([])
93+
94+
useLayoutEffect(() => {
95+
setApiModes(getApiModesFromConfig(config, true))
96+
}, [
97+
config.activeApiModes,
98+
config.customApiModes,
99+
config.azureDeploymentName,
100+
config.ollamaModelName,
101+
])
86102

87103
const getBalance = async () => {
88104
const response = await fetch(`${config.customOpenAiApiUrl}/dashboard/billing/credit_grants`, {
@@ -155,22 +171,23 @@ export function GeneralPart({ config, updateConfig }) {
155171
}
156172
required
157173
onChange={(e) => {
158-
const modelName = e.target.value
159-
updateConfig({ modelName: modelName })
174+
const apiMode = apiModes[e.target.value]
175+
updateConfig({ apiMode: apiMode })
160176
}}
161177
>
162-
{getApiModesStringArrayFromConfig(config, true).map((modelName) => {
178+
{apiModes.map((apiMode, index) => {
179+
const modelName = apiModeToModelName(apiMode)
163180
const desc = modelNameToDesc(modelName, t)
164-
if (desc)
181+
if (desc) {
182+
let selected
183+
if (isApiModeSelected(apiMode, config)) selected = true
184+
else selected = config.modelName === modelName
165185
return (
166-
<option
167-
value={modelName}
168-
key={modelName}
169-
selected={modelName === config.modelName}
170-
>
186+
<option value={index} key={index} selected={selected}>
171187
{desc}
172188
</option>
173189
)
190+
}
174191
})}
175192
</select>
176193
{isUsingMultiModeModel(config) && (

src/services/init-session.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { t } from 'i18next'
3636
* @param {string|null} sessionName
3737
* @param {string|null} modelName
3838
* @param {boolean|null} autoClean
39+
* @param {Object|null} apiMode
3940
* @returns {Session}
4041
*/
4142
export function initSession({
@@ -44,6 +45,7 @@ export function initSession({
4445
sessionName = null,
4546
modelName = null,
4647
autoClean = false,
48+
apiMode = null,
4749
} = {}) {
4850
return {
4951
// common
@@ -57,6 +59,7 @@ export function initSession({
5759

5860
aiName: modelName ? modelNameToDesc(modelName, t) : null,
5961
modelName,
62+
apiMode,
6063

6164
autoClean,
6265
isRetry: false,

src/services/local-session.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { getUserConfig } from '../config/index.mjs'
44

55
export const initDefaultSession = async () => {
66
const config = await getUserConfig()
7-
const modelName = config.modelName
87
return initSession({
98
sessionName: new Date().toLocaleString(),
10-
modelName: modelName,
9+
modelName: config.modelName,
10+
apiMode: config.apiMode,
1111
autoClean: false,
1212
})
1313
}

src/utils/model-name-convert.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,9 @@ export function getApiModesFromConfig(config, onlyActive) {
105105
export function getApiModesStringArrayFromConfig(config, onlyActive) {
106106
return getApiModesFromConfig(config, onlyActive).map(apiModeToModelName)
107107
}
108+
109+
export function isApiModeSelected(apiMode, configOrSession) {
110+
return (
111+
configOrSession.apiMode && JSON.stringify(configOrSession.apiMode) === JSON.stringify(apiMode)
112+
)
113+
}

0 commit comments

Comments
 (0)