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