返回 oh-my-ppt
ModelConfigDialog.tsx
根目录 / src / renderer / src / components / settings / ModelConfigDialog.tsx
1 import { CircleHelp, ShieldCheck, X } from 'lucide-react'
2 import { Button } from '../ui/Button'
3 import { Input } from '../ui/Input'
4 import { Popover, PopoverContent, PopoverTrigger } from '../ui/Popover'
5 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/Select'
6 import type { ModelForm, SettingsTranslate } from './types'
7
8 const MODEL_PROVIDER_LINKS = [
9 { label: 'DeepSeek', url: 'https://platform.deepseek.com' },
10 { label: 'Moonshot (Kimi)', url: 'https://platform.moonshot.cn' },
11 { label: 'GLM (智谱)', url: 'https://open.bigmodel.cn' },
12 { label: 'Qwen (通义千问)', url: 'https://bailian.console.aliyun.com/' },
13 { label: 'Doubao (豆包)', url: 'https://console.volcengine.com/ark' },
14 { label: 'Mimo (小米)', url: 'https://platform.xiaomimimo.com' },
15 { label: 'MiniMax', url: 'https://www.minimaxi.com/' },
16 { label: 'OpenAI', url: 'https://platform.openai.com' },
17 { label: 'Claude (Anthropic)', url: 'https://console.anthropic.com' },
18 { label: 'Google Gemini', url: 'https://ai.google.dev' }
19 ]
20
21 interface ModelConfigDialogProps {
22 form: ModelForm
23 open: boolean
24 saving: boolean
25 verified: boolean
26 verifying: boolean
27 t: SettingsTranslate
28 onClose: () => void
29 onFormChange: (patch: Partial<ModelForm>) => void
30 onSave: () => void
31 onVerify: () => void
32 }
33
34 export function ModelConfigDialog({
35 form,
36 open,
37 saving,
38 verified,
39 verifying,
40 t,
41 onClose,
42 onFormChange,
43 onSave,
44 onVerify
45 }: ModelConfigDialogProps): React.JSX.Element | null {
46 if (!open) return null
47 return (
48 <div
49 className="fixed inset-0 z-50 flex items-center justify-center bg-[#2d291f]/42 p-4"
50 onMouseDown={(event) => {
51 if (event.target === event.currentTarget && !saving) onClose()
52 }}
53 >
54 <div className="max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-lg border border-[#d8ccb5]/85 bg-[#fffaf1] shadow-[0_24px_70px_rgba(53,44,32,0.28)]">
55 <div className="flex items-center justify-between border-b border-[#e3d8c5] px-5 py-3.5">
56 <h2 className="flex items-center gap-1.5 text-base font-semibold text-[#33402a]">
57 {form.id ? t('settings.editModel') : t('settings.addModel')}
58 <Popover>
59 <PopoverTrigger asChild>
60 <CircleHelp className="h-3.5 w-3.5 cursor-pointer text-muted-foreground/50 transition-colors hover:text-foreground" />
61 </PopoverTrigger>
62 <PopoverContent
63 side="bottom"
64 align="start"
65 className="w-auto max-w-xs border-[#d8cfbc]/80 bg-[#fffdf8] p-3"
66 >
67 <p className="mb-2 text-xs font-semibold text-[#3e4a32]">
68 {t('settings.modelHelpTitle')}
69 </p>
70 <div className="flex flex-wrap gap-1.5">
71 {MODEL_PROVIDER_LINKS.map((item) => (
72 <a
73 key={item.url}
74 href={item.url}
75 target="_blank"
76 rel="noopener noreferrer"
77 className="rounded-md border border-[#d8cfbc]/80 bg-[#f5efe2]/60 px-2 py-1 text-xs text-[#5b6b4d] transition-colors hover:border-[#96b77f]/60 hover:bg-[#e8f0de] hover:text-[#3e4a32]"
78 >
79 {item.label}
80 </a>
81 ))}
82 </div>
83 </PopoverContent>
84 </Popover>
85 </h2>
86 <Button size="sm" variant="ghost" onClick={onClose} disabled={saving}>
87 <X className="h-4 w-4" />
88 </Button>
89 </div>
90
91 <div className="space-y-3 p-5">
92 <div className="grid gap-2 sm:grid-cols-2">
93 <div>
94 <label className="mb-1 block text-sm font-medium">
95 {t('settings.providerPreset')}
96 </label>
97 <Select
98 value={form.provider}
99 onValueChange={(value) =>
100 onFormChange({
101 provider:
102 value === 'anthropic' ||
103 value === 'google' ||
104 value === 'openai-responses'
105 ? value
106 : 'openai'
107 })
108 }
109 >
110 <SelectTrigger className="h-8">
111 <SelectValue placeholder={t('settings.providerPlaceholder')} />
112 </SelectTrigger>
113 <SelectContent>
114 <SelectItem value="anthropic">Claude (Anthropic)</SelectItem>
115 <SelectItem value="openai">
116 {t('settings.providerOpenAIChatCompletions')}
117 </SelectItem>
118 <SelectItem value="openai-responses">
119 {t('settings.providerOpenAIResponses')}
120 </SelectItem>
121 <SelectItem value="google">Google Gemini</SelectItem>
122 </SelectContent>
123 </Select>
124 </div>
125 <div>
126 <label className="mb-1 block text-sm font-medium">{t('settings.modelName')}</label>
127 <Input
128 value={form.name}
129 onChange={(e) => onFormChange({ name: e.target.value })}
130 placeholder={t('settings.modelNamePlaceholder')}
131 className="h-8"
132 />
133 </div>
134 </div>
135
136 <div className="grid gap-2 sm:grid-cols-2">
137 <div>
138 <label className="mb-1 block text-sm font-medium">model</label>
139 <Input
140 placeholder={t('settings.modelPlaceholder')}
141 value={form.model}
142 onChange={(e) => onFormChange({ model: e.target.value })}
143 className="h-8"
144 />
145 <p className="mt-1 text-[12px] text-muted-foreground/50">
146 {t('settings.modelHint')}
147 </p>
148 </div>
149 <div>
150 <label className="mb-1 block text-sm font-medium">base_url</label>
151 <Input
152 placeholder={t('settings.baseUrlPlaceholder')}
153 value={form.baseUrl}
154 onChange={(e) => onFormChange({ baseUrl: e.target.value })}
155 className="h-8"
156 />
157 <p className="mt-1 text-[12px] text-muted-foreground/50">
158 {form.provider === 'google'
159 ? t('settings.baseUrlHintGoogle')
160 : t('settings.baseUrlHint')}
161 </p>
162 </div>
163 </div>
164
165 <div>
166 <label className="mb-1 block text-sm font-medium">api_key</label>
167 <Input
168 type="password"
169 placeholder={t('settings.apiKeyPlaceholder', {
170 provider:
171 form.provider === 'openai' || form.provider === 'openai-responses'
172 ? 'OpenAI'
173 : form.provider === 'google'
174 ? 'Google'
175 : 'Claude'
176 })}
177 value={form.apiKey}
178 onChange={(e) => onFormChange({ apiKey: e.target.value })}
179 className="h-8"
180 />
181 <p className="mt-1 text-[12px] text-muted-foreground/50">
182 {t('settings.verifyHint')}
183 </p>
184 </div>
185
186 <div>
187 <label className="mb-1 block text-sm font-medium">max_tokens</label>
188 <Input
189 inputMode="numeric"
190 pattern="[0-9]*"
191 value={form.maxTokens}
192 onChange={(e) => onFormChange({ maxTokens: e.target.value })}
193 className="h-8"
194 />
195 <p className="mt-1 text-[12px] text-muted-foreground/50">
196 {t('settings.maxTokensHint')}
197 </p>
198 </div>
199
200 <div className="flex items-center justify-between gap-4 rounded-lg border border-[#e3d8c5] bg-[#fffdf8]/70 p-3">
201 <span className="min-w-0">
202 <span className="block text-sm font-medium">{t('settings.disableTemperature')}</span>
203 <span className="mt-0.5 block text-[12px] text-muted-foreground/50">
204 {t('settings.disableTemperatureHint')}
205 </span>
206 </span>
207 <input
208 type="checkbox"
209 aria-label={t('settings.disableTemperature')}
210 checked={form.disableTemperature}
211 onChange={(event) =>
212 onFormChange({ disableTemperature: event.currentTarget.checked })
213 }
214 className="h-4 w-4 shrink-0 cursor-pointer accent-[#5d7b4d]"
215 />
216 </div>
217
218 {form.provider === 'openai' && (
219 <div className="rounded-lg border border-[#e3d8c5] bg-[#fffdf8]/70 p-3">
220 <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
221 <span className="min-w-0">
222 <span className="block text-sm font-medium">
223 {t('settings.thinkingParameterMode')}
224 </span>
225 <span className="mt-0.5 block text-[12px] text-muted-foreground/50">
226 {t('settings.thinkingParameterModeHint')}
227 </span>
228 </span>
229 <Select
230 value={form.thinkingParameterMode}
231 onValueChange={(value) =>
232 onFormChange({
233 thinkingParameterMode: value === 'omit' ? 'omit' : 'auto'
234 })
235 }
236 >
237 <SelectTrigger className="h-8 w-full shrink-0 sm:w-[180px]">
238 <SelectValue />
239 </SelectTrigger>
240 <SelectContent>
241 <SelectItem value="auto">
242 {t('settings.thinkingParameterModeAuto')}
243 </SelectItem>
244 <SelectItem value="omit">
245 {t('settings.thinkingParameterModeOmit')}
246 </SelectItem>
247 </SelectContent>
248 </Select>
249 </div>
250 </div>
251 )}
252 </div>
253
254 <div className="flex justify-end gap-2 border-t border-[#e3d8c5] px-5 py-3.5">
255 <Button
256 type="button"
257 variant="ghost"
258 onClick={onClose}
259 disabled={saving}
260 className="h-8 px-3 text-xs"
261 >
262 {t('common.cancel')}
263 </Button>
264 <Button
265 variant="secondary"
266 onClick={onVerify}
267 disabled={saving || verifying}
268 className="h-8 border border-[#7ea06f]/45 px-3 text-xs"
269 >
270 <ShieldCheck className="mr-1 h-3.5 w-3.5" />
271 {verifying ? t('settings.verifying') : t('settings.verify')}
272 </Button>
273 <Button
274 onClick={onSave}
275 disabled={saving || verifying || !verified}
276 className="h-8 px-3 text-xs"
277 >
278 {saving ? t('common.saving') : t('settings.saveModel')}
279 </Button>
280 </div>
281 </div>
282 </div>
283 )
284 }
285
285 lines Plain Text