返回 oh-my-ppt
ImageModelConfigDialog.tsx
根目录 / src / renderer / src / components / settings / ImageModelConfigDialog.tsx
1 import { CircleHelp, ExternalLink, ShieldCheck, X } from 'lucide-react'
2 import { Button } from '../ui/Button'
3 import { Input, Textarea } from '../ui/Input'
4 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/Select'
5 import { IMAGE_PROVIDER_OPTIONS } from './model-settings-utils'
6 import type { ImageModelForm, SettingsTranslate } from './types'
7
8 interface ImageModelConfigDialogProps {
9 form: ImageModelForm
10 open: boolean
11 saving: boolean
12 verifying: boolean
13 verified: boolean
14 verificationPreviewUrl: string | null
15 t: SettingsTranslate
16 onClose: () => void
17 onFormChange: (patch: Partial<ImageModelForm>) => void
18 onProviderChange: (value: string) => void
19 onSave: () => void
20 onVerify: () => void
21 }
22
23 const PROVIDER_DOCS: Record<ImageModelForm['provider'], { label: string; url: string }> = {
24 agnes: {
25 label: 'Agnes AI',
26 url: 'https://agnes-ai.com/'
27 },
28 jimeng: {
29 label: '即梦3.0',
30 url: 'https://www.volcengine.com/docs/85621/1616429?lang=zh'
31 },
32 jimeng4: {
33 label: '即梦4.0',
34 url: 'https://www.volcengine.com/docs/85621/1817045?lang=zh'
35 },
36 seedream: {
37 label: 'Seedream',
38 url: 'https://www.volcengine.com/docs/82379/1541523?lang=zh'
39 },
40 siliconflow: {
41 label: '硅基流动',
42 url: 'https://www.siliconflow.cn/'
43 },
44 openaiCompatible: {
45 label: 'OpenAI 兼容',
46 url: 'https://platform.openai.com/docs/api-reference/chat/create'
47 },
48 gemini: {
49 label: 'Gemini',
50 url: 'https://ai.google.dev/gemini-api/docs/image-generation?hl=zh-cn'
51 }
52 }
53
54 const PROVIDER_HINT_KEYS: Record<ImageModelForm['provider'], Parameters<SettingsTranslate>[0]> = {
55 agnes: 'settings.imageModelConfigHintAgnes',
56 jimeng: 'settings.imageModelConfigHintJimeng',
57 jimeng4: 'settings.imageModelConfigHintJimeng4',
58 siliconflow: 'settings.imageModelConfigHintSiliconflow',
59 openaiCompatible: 'settings.imageModelConfigHintOpenAICompatible',
60 gemini: 'settings.imageModelConfigHintGemini',
61 seedream: 'settings.imageModelConfigHintSeedream'
62 }
63
64 const SILICONFLOW_MODELS = [
65 'Tongyi-MAI/Z-Image-Turbo',
66 'Tongyi-MAI/Z-Image',
67 'baidu/ERNIE-Image-Turbo',
68 'Qwen/Qwen-Image',
69 'Kwai-Kolors/Kolors'
70 ]
71
72 export function ImageModelConfigDialog({
73 form,
74 open,
75 saving,
76 verifying,
77 verified,
78 verificationPreviewUrl,
79 t,
80 onClose,
81 onFormChange,
82 onProviderChange,
83 onSave,
84 onVerify
85 }: ImageModelConfigDialogProps): React.JSX.Element | null {
86 if (!open) return null
87 const providerDocs = PROVIDER_DOCS[form.provider]
88 const providerHint = t(PROVIDER_HINT_KEYS[form.provider])
89 return (
90 <div
91 className="fixed inset-0 z-50 flex items-center justify-center bg-[#2d291f]/42 p-4"
92 onMouseDown={(event) => {
93 if (event.target === event.currentTarget && !saving) onClose()
94 }}
95 >
96 <div className="max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg border border-[#d8ccb5]/85 bg-[#fffaf1] shadow-[0_24px_70px_rgba(53,44,32,0.28)]">
97 <div className="flex items-center justify-between border-b border-[#e3d8c5] px-4 py-2.5">
98 <h2 className="text-sm font-semibold text-[#33402a]">
99 {form.id ? t('settings.editImageModel') : t('settings.addImageModel')}
100 </h2>
101 <Button size="sm" variant="ghost" onClick={onClose} disabled={saving}>
102 <X className="h-3.5 w-3.5" />
103 </Button>
104 </div>
105
106 <div className="space-y-3 p-4">
107 <div className="grid gap-3 sm:grid-cols-2">
108 <div>
109 <label className="mb-1.5 block text-xs font-medium">{t('settings.modelName')}</label>
110 <Input
111 value={form.name}
112 onChange={(e) => onFormChange({ name: e.target.value })}
113 placeholder={t('settings.imageModelNamePlaceholder')}
114 className="h-7 text-xs"
115 />
116 </div>
117 <div>
118 <label className="mb-1.5 block text-xs font-medium">
119 {t('settings.providerPreset')}
120 </label>
121 <Select value={form.provider} onValueChange={onProviderChange}>
122 <SelectTrigger className="h-7 text-xs">
123 <SelectValue placeholder={t('settings.providerPlaceholder')} />
124 </SelectTrigger>
125 <SelectContent>
126 {IMAGE_PROVIDER_OPTIONS.map((option) => (
127 <SelectItem key={option.value} value={option.value}>
128 {option.label}
129 </SelectItem>
130 ))}
131 </SelectContent>
132 </Select>
133 </div>
134 </div>
135
136 <div>
137 <label className="mb-1.5 block text-xs font-medium">模型配置</label>
138 <Textarea
139 spellCheck={false}
140 rows={6}
141 value={form.modelConfig}
142 onChange={(e) => onFormChange({ modelConfig: e.target.value })}
143 className="min-h-[120px] resize-y font-mono text-xs leading-4"
144 />
145 <div className="mt-2 flex items-start gap-2 rounded-lg border border-[#d8ccb5]/80 bg-[#f8f1e6]/82 px-2.5 py-2 text-xs text-[#5f5649]">
146 <div className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-[#eef0e5] text-[#687a58]">
147 <CircleHelp className="h-3 w-3" />
148 </div>
149 <div className="min-w-0 flex-1">
150 <div className="flex flex-wrap items-center gap-1.5">
151 <span className="font-semibold text-[#33402a]">{providerDocs.label}</span>
152 <a
153 href={providerDocs.url}
154 target="_blank"
155 rel="noopener noreferrer"
156 className="inline-flex items-center gap-1 rounded-md border border-[#cbbfa8]/80 bg-[#fffaf1]/75 px-2 py-0.5 font-medium text-[#5d6b4d] transition-colors hover:border-[#aebd9a] hover:text-[#3e4a32]"
157 >
158 {t('settings.imageProviderOfficialDocs')}
159 <ExternalLink className="h-3 w-3" />
160 </a>
161 </div>
162 <p className="mt-1 leading-4 text-[#6d604d]">{providerHint}</p>
163 {form.provider === 'siliconflow' && (
164 <div className="mt-2 flex flex-wrap gap-1.5">
165 {SILICONFLOW_MODELS.map((model) => (
166 <span
167 key={model}
168 className="rounded-md border border-[#d8ccb5]/78 bg-[#fffaf1]/72 px-2 py-0.5 font-mono text-[11px] text-[#526044]"
169 >
170 {model}
171 </span>
172 ))}
173 </div>
174 )}
175 </div>
176 </div>
177 </div>
178
179 <div className="flex justify-end">
180 <Button
181 variant="secondary"
182 onClick={onVerify}
183 disabled={saving || verifying}
184 className="h-7 min-w-[72px] rounded-lg border border-[#7ea06f]/45 px-2.5 text-xs"
185 >
186 <ShieldCheck className="mr-1 h-3.5 w-3.5" />
187 {verifying ? t('settings.verifying') : t('settings.verify')}
188 </Button>
189 </div>
190
191 {verificationPreviewUrl && (
192 <div className="overflow-hidden rounded-lg border border-[#b5c7a4] bg-[#f4f8ed] p-2.5">
193 <div className="mb-2 flex items-center justify-between gap-3 text-xs">
194 <span className="font-medium text-[#3e4a32]">
195 {t('settings.imageVerificationPreview')}
196 </span>
197 <span className="text-[#647653]">{t('settings.imageVerificationPrompt')}</span>
198 </div>
199 <img
200 src={verificationPreviewUrl}
201 alt={t('settings.imageVerificationPreviewAlt')}
202 className="max-h-64 w-full rounded-md bg-[#e9e3d7] object-contain"
203 />
204 </div>
205 )}
206 </div>
207
208 <div className="flex justify-end gap-2 border-t border-[#e3d8c5] px-4 py-2.5">
209 <Button type="button" variant="ghost" onClick={onClose} disabled={saving}>
210 {t('common.cancel')}
211 </Button>
212 <Button onClick={onSave} disabled={saving || verifying || !verified}>
213 {saving ? t('common.saving') : t('settings.saveImageModel')}
214 </Button>
215 </div>
216 </div>
217 </div>
218 )
219 }
220
220 lines Plain Text