Skip to content

Commit e4cd838

Browse files
committed
WIP, custom API Modes
1 parent 34cfb5c commit e4cd838

File tree

16 files changed

+368
-59
lines changed

16 files changed

+368
-59
lines changed

src/components/ConversationCard/index.jsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import PropTypes from 'prop-types'
33
import Browser from 'webextension-polyfill'
44
import InputBox from '../InputBox'
55
import ConversationItem from '../ConversationItem'
6-
import { createElementAtPosition, isFirefox, isMobile, isSafari } from '../../utils'
6+
import {
7+
createElementAtPosition,
8+
isFirefox,
9+
isMobile,
10+
isSafari,
11+
modelNameToDesc,
12+
} from '../../utils'
713
import {
814
ArchiveIcon,
915
DesktopDownloadIcon,
@@ -16,7 +22,7 @@ import FileSaver from 'file-saver'
1622
import { render } from 'preact'
1723
import FloatingToolbar from '../FloatingToolbar'
1824
import { useClampWindowSize } from '../../hooks/use-clamp-window-size'
19-
import { bingWebModelKeys, getUserConfig, ModelMode, Models } from '../../config/index.mjs'
25+
import { bingWebModelKeys, getUserConfig, Models } from '../../config/index.mjs'
2026
import { useTranslation } from 'react-i18next'
2127
import DeleteButton from '../DeleteButton'
2228
import { useConfig } from '../../hooks/use-config.mjs'
@@ -370,14 +376,7 @@ function ConversationCard(props) {
370376
}}
371377
>
372378
{config.activeApiModes.map((modelName) => {
373-
let desc
374-
if (modelName.includes('-')) {
375-
const splits = modelName.split('-')
376-
if (splits[0] in Models)
377-
desc = `${t(Models[splits[0]].desc)} (${t(ModelMode[splits[1]])})`
378-
} else {
379-
if (modelName in Models) desc = t(Models[modelName].desc)
380-
}
379+
const desc = modelNameToDesc(modelName, t)
381380
if (desc)
382381
return (
383382
<option

src/config/index.mjs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,73 @@ export const poeWebModelKeys = [
8282
]
8383
export const moonshotApiModelKeys = ['moonshot_v1_8k', 'moonshot_v1_32k', 'moonshot_v1_128k']
8484

85+
export const AlwaysCustomGroups = [
86+
'ollamaApiModelKeys',
87+
'customApiModelKeys',
88+
'azureOpenAiApiModelKeys',
89+
]
90+
export const CustomUrlGroups = ['customApiModelKeys']
91+
export const CustomApiKeyGroups = ['customApiModelKeys']
92+
export const ModelGroups = {
93+
chatgptWebModelKeys: {
94+
value: chatgptWebModelKeys,
95+
desc: 'ChatGPT (Web)',
96+
},
97+
claudeWebModelKeys: {
98+
value: claudeWebModelKeys,
99+
desc: 'Claude.ai (Web)',
100+
},
101+
moonshotWebModelKeys: {
102+
value: moonshotWebModelKeys,
103+
desc: 'Kimi.Moonshot (Web)',
104+
},
105+
bingWebModelKeys: {
106+
value: bingWebModelKeys,
107+
desc: 'Bing (Web)',
108+
},
109+
bardWebModelKeys: {
110+
value: bardWebModelKeys,
111+
desc: 'Gemini (Web)',
112+
},
113+
114+
chatgptApiModelKeys: {
115+
value: chatgptApiModelKeys,
116+
desc: 'ChatGPT (API)',
117+
},
118+
claudeApiModelKeys: {
119+
value: claudeApiModelKeys,
120+
desc: 'Claude.ai (API)',
121+
},
122+
moonshotApiModelKeys: {
123+
value: moonshotApiModelKeys,
124+
desc: 'Kimi.Moonshot (API)',
125+
},
126+
chatglmApiModelKeys: {
127+
value: chatglmApiModelKeys,
128+
desc: 'ChatGLM (API)',
129+
},
130+
ollamaApiModelKeys: {
131+
value: ollamaApiModelKeys,
132+
desc: 'Ollama (API)',
133+
},
134+
azureOpenAiApiModelKeys: {
135+
value: azureOpenAiApiModelKeys,
136+
desc: 'ChatGPT (Azure API)',
137+
},
138+
gptApiModelKeys: {
139+
value: gptApiModelKeys,
140+
desc: 'GPT Completion (API)',
141+
},
142+
githubThirdPartyApiModelKeys: {
143+
value: githubThirdPartyApiModelKeys,
144+
desc: 'Github Third Party Waylaidwanderer (API)',
145+
},
146+
customApiModelKeys: {
147+
value: customApiModelKeys,
148+
desc: 'Custom Model',
149+
},
150+
}
151+
85152
/**
86153
* @typedef {object} Model
87154
* @property {string} value
@@ -290,6 +357,17 @@ export const defaultConfig = {
290357
'ollamaModel',
291358
'azureOpenAi',
292359
],
360+
customApiModes: [
361+
{
362+
groupName: '',
363+
itemName: '',
364+
isCustom: false,
365+
customName: '',
366+
customUrl: '',
367+
apiKey: '',
368+
active: false,
369+
},
370+
],
293371
activeSelectionTools: ['translate', 'summary', 'polish', 'code', 'ask'],
294372
customSelectionTools: [
295373
{

src/popup/sections/ApiModes.jsx

Lines changed: 174 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,196 @@
11
import { useTranslation } from 'react-i18next'
22
import PropTypes from 'prop-types'
3-
import { ModelMode, Models } from '../../config/index.mjs'
3+
import { apiModeToModelName, modelNameToDesc } from '../../utils/index.mjs'
4+
import { PencilIcon, TrashIcon } from '@primer/octicons-react'
5+
import { useState } from 'react'
6+
import {
7+
AlwaysCustomGroups,
8+
CustomApiKeyGroups,
9+
CustomUrlGroups,
10+
ModelGroups,
11+
} from '../../config/index.mjs'
412

513
ApiModes.propTypes = {
614
config: PropTypes.object.isRequired,
715
updateConfig: PropTypes.func.isRequired,
816
}
917

18+
const defaultApiMode = {
19+
groupName: 'chatgptWebModelKeys',
20+
itemName: 'chatgptFree35',
21+
isCustom: false,
22+
customName: '',
23+
customUrl: '',
24+
apiKey: '',
25+
active: true,
26+
}
27+
1028
export function ApiModes({ config, updateConfig }) {
1129
const { t } = useTranslation()
30+
const [editing, setEditing] = useState(false)
31+
const [editingApiMode, setEditingApiMode] = useState(defaultApiMode)
32+
const [editingIndex, setEditingIndex] = useState(-1)
33+
34+
const editingComponent = (
35+
<div style={{ display: 'flex', flexDirection: 'column', '--spacing': '4px' }}>
36+
<div style={{ display: 'flex', gap: '12px' }}>
37+
<button
38+
onClick={(e) => {
39+
e.preventDefault()
40+
setEditing(false)
41+
}}
42+
>
43+
{t('Cancel')}
44+
</button>
45+
<button
46+
onClick={(e) => {
47+
e.preventDefault()
48+
if (editingIndex === -1) {
49+
updateConfig({
50+
customApiModes: [...config.customApiModes, editingApiMode],
51+
})
52+
} else {
53+
const customApiModes = [...config.customApiModes]
54+
customApiModes[editingIndex] = editingApiMode
55+
updateConfig({ customApiModes })
56+
}
57+
setEditing(false)
58+
}}
59+
>
60+
{t('Save')}
61+
</button>
62+
</div>
63+
<div style={{ display: 'flex', gap: '4px', alignItems: 'center', whiteSpace: 'noWrap' }}>
64+
{t('Type')}
65+
<select
66+
value={editingApiMode.groupName}
67+
onChange={(e) => {
68+
const groupName = e.target.value
69+
const itemName = ModelGroups[groupName].value[0]
70+
setEditingApiMode({ ...editingApiMode, groupName, itemName })
71+
}}
72+
>
73+
{Object.entries(ModelGroups).map(([groupName, { desc }]) => (
74+
<option key={groupName} value={groupName}>
75+
{t(desc)}
76+
</option>
77+
))}
78+
</select>
79+
</div>
80+
<div style={{ display: 'flex', gap: '4px', alignItems: 'center', whiteSpace: 'noWrap' }}>
81+
{t('Mode')}
82+
<select
83+
value={editingApiMode.itemName}
84+
onChange={(e) => {
85+
const itemName = e.target.value
86+
const isCustom = itemName === 'custom'
87+
setEditingApiMode({ ...editingApiMode, itemName, isCustom })
88+
}}
89+
>
90+
{ModelGroups[editingApiMode.groupName].value.map((itemName) => (
91+
<option key={itemName} value={itemName}>
92+
{modelNameToDesc(itemName, t)}
93+
</option>
94+
))}
95+
{!AlwaysCustomGroups.includes(editingApiMode.groupName) && (
96+
<option value="custom">{t('Custom')}</option>
97+
)}
98+
</select>
99+
{(editingApiMode.isCustom || AlwaysCustomGroups.includes(editingApiMode.groupName)) && (
100+
<input
101+
type="text"
102+
value={editingApiMode.customName}
103+
placeholder={t('Model Name')}
104+
onChange={(e) => setEditingApiMode({ ...editingApiMode, customName: e.target.value })}
105+
/>
106+
)}
107+
</div>
108+
{CustomUrlGroups.includes(editingApiMode.groupName) &&
109+
(editingApiMode.isCustom || AlwaysCustomGroups.includes(editingApiMode.groupName)) && (
110+
<input
111+
type="text"
112+
value={editingApiMode.customUrl}
113+
placeholder={t('API Url')}
114+
onChange={(e) => setEditingApiMode({ ...editingApiMode, customUrl: e.target.value })}
115+
/>
116+
)}
117+
{CustomApiKeyGroups.includes(editingApiMode.groupName) &&
118+
(editingApiMode.isCustom || AlwaysCustomGroups.includes(editingApiMode.groupName)) && (
119+
<input
120+
type="password"
121+
value={editingApiMode.apiKey}
122+
placeholder={t('API Key')}
123+
onChange={(e) => setEditingApiMode({ ...editingApiMode, apiKey: e.target.value })}
124+
/>
125+
)}
126+
</div>
127+
)
12128

13129
return (
14130
<>
15-
{config.apiModes.map((modelName) => {
16-
let desc
17-
if (modelName.includes('-')) {
18-
const splits = modelName.split('-')
19-
if (splits[0] in Models)
20-
desc = `${t(Models[splits[0]].desc)} (${t(ModelMode[splits[1]])})`
21-
} else {
22-
if (modelName in Models) desc = t(Models[modelName].desc)
23-
}
24-
if (desc)
25-
return (
26-
<label key={modelName}>
131+
{config.customApiModes.map(
132+
(apiMode, index) =>
133+
apiMode.groupName &&
134+
apiMode.itemName &&
135+
(editing && editingIndex === index ? (
136+
editingComponent
137+
) : (
138+
<label key={index} style={{ display: 'flex', alignItems: 'center' }}>
27139
<input
28140
type="checkbox"
29-
checked={config.activeApiModes.includes(modelName)}
141+
checked={apiMode.active}
30142
onChange={(e) => {
31-
const checked = e.target.checked
32-
const activeApiModes = config.activeApiModes.filter((i) => i !== modelName)
33-
if (checked) activeApiModes.push(modelName)
34-
updateConfig({ activeApiModes })
143+
const customApiModes = [...config.customApiModes]
144+
customApiModes[index] = { ...apiMode, active: e.target.checked }
145+
updateConfig({ customApiModes })
35146
}}
36147
/>
37-
{desc}
148+
{modelNameToDesc(apiModeToModelName(apiMode), t)}
149+
<div style={{ flexGrow: 1 }} />
150+
<div style={{ display: 'flex', gap: '12px' }}>
151+
<div
152+
style={{ cursor: 'pointer' }}
153+
onClick={(e) => {
154+
e.preventDefault()
155+
setEditing(true)
156+
setEditingApiMode(apiMode)
157+
setEditingIndex(index)
158+
}}
159+
>
160+
<PencilIcon />
161+
</div>
162+
<div
163+
style={{ cursor: 'pointer' }}
164+
onClick={(e) => {
165+
e.preventDefault()
166+
const customApiModes = [...config.customApiModes]
167+
customApiModes.splice(index, 1)
168+
updateConfig({ customApiModes })
169+
}}
170+
>
171+
<TrashIcon />
172+
</div>
173+
</div>
38174
</label>
39-
)
40-
})}
175+
)),
176+
)}
177+
<div style={{ height: '30px' }} />
178+
{editing ? (
179+
editingIndex === -1 ? (
180+
editingComponent
181+
) : undefined
182+
) : (
183+
<button
184+
onClick={(e) => {
185+
e.preventDefault()
186+
setEditing(true)
187+
setEditingApiMode(defaultApiMode)
188+
setEditingIndex(-1)
189+
}}
190+
>
191+
{t('New')}
192+
</button>
193+
)}
41194
</>
42195
)
43196
}

src/popup/sections/GeneralPart.jsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useTranslation } from 'react-i18next'
22
import { useState } from 'react'
33
import FileSaver from 'file-saver'
4-
import { openUrl } from '../../utils/index.mjs'
4+
import { openUrl, modelNameToDesc } from '../../utils/index.mjs'
55
import {
66
isUsingOpenAiApiKey,
77
isUsingAzureOpenAi,
@@ -13,7 +13,6 @@ import {
1313
isUsingGithubThirdPartyApi,
1414
isUsingMultiModeModel,
1515
ModelMode,
16-
Models,
1716
ThemeMode,
1817
TriggerMode,
1918
isUsingMoonshotApi,
@@ -161,14 +160,7 @@ export function GeneralPart({ config, updateConfig }) {
161160
}}
162161
>
163162
{config.activeApiModes.map((modelName) => {
164-
let desc
165-
if (modelName.includes('-')) {
166-
const splits = modelName.split('-')
167-
if (splits[0] in Models)
168-
desc = `${t(Models[splits[0]].desc)} (${t(ModelMode[splits[1]])})`
169-
} else {
170-
if (modelName in Models) desc = t(Models[modelName].desc)
171-
}
163+
const desc = modelNameToDesc(modelName, t)
172164
if (desc)
173165
return (
174166
<option

src/popup/sections/SelectionTools.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function SelectionTools({ config, updateConfig }) {
6262
</button>
6363
</div>
6464
{errorMessage && <div style={{ color: 'red' }}>{errorMessage}</div>}
65-
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
65+
<div style={{ display: 'flex', gap: '4px', alignItems: 'center', whiteSpace: 'noWrap' }}>
6666
{t('Name')}
6767
<input
6868
type="text"

0 commit comments

Comments
 (0)