@dsh-plus/image-studio 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,17 @@
1
1
  /**
2
- * 配置卡片(injectPluginConfigCard 三槽位,legacy key = settings 命名空间,
3
- * 0.1.6-alpha.2 起为插件页 row/bundle config,view 分发 summary/page):
2
+ * 配置卡片(injectPluginConfigCard 两槽位 = 插件页 row/bundle config,
3
+ * view 分发 summary/page):
4
4
  * 三组预设(提供商/参数/提示词)+ 高级项(并发/超时/代理/画廊上限)的
5
5
  * staged draft 编辑;提供商预设逐条附 API Key 管理(凭据端点,值不回显)。
6
6
  * JSON 字段(extraHeaders/paramSpecs)以文本镜像编辑,保存时解析 +
7
7
  * validateParamSpecs 本地校验,非法即拦在保存前。
8
+ * 草稿类型与纯转换在 ./draft.ts,三组预设编辑区在 ./preset-sections.tsx。
8
9
  * @module image-studio/client/card
9
10
  */
10
11
  import {
12
+ type CardAction,
11
13
  CardChrome,
14
+ CardLoading,
12
15
  type CardStatusState,
13
16
  IDLE_STATUS,
14
17
  type NamespaceSettingsApi,
@@ -19,10 +22,21 @@ import {
19
22
  import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
20
23
 
21
24
  import type { ImageStudioConfig } from '../config.ts'
22
- import { credentialRefNameOf, isValidPresetId } from '../credentials.ts'
23
- import { validateParamSpecs } from '../params/spec.ts'
24
- import { fetchCredentialStatus, fetchProviders, setCredential, unsetCredential } from './api.ts'
25
+ import { fetchProviders } from './api.ts'
26
+ import {
27
+ type Draft,
28
+ draftOf,
29
+ type ParamDraft,
30
+ type PromptDraft,
31
+ type ProviderDraft,
32
+ payloadOf,
33
+ } from './draft.ts'
25
34
  import type { Translate } from './i18n.ts'
35
+ import {
36
+ ParamPresetSection,
37
+ PromptPresetSection,
38
+ ProviderPresetSection,
39
+ } from './preset-sections.tsx'
26
40
 
27
41
  export interface CardProps extends PluginConfigViewProps {
28
42
  t: Translate
@@ -30,172 +44,104 @@ export interface CardProps extends PluginConfigViewProps {
30
44
  api: NamespaceSettingsApi
31
45
  }
32
46
 
33
- /** 编辑态草稿:JSON 字段与数值以文本承载(保存时解析校验)。 */
34
- interface ProviderDraft {
35
- id: string
36
- name: string
37
- protocol: string
38
- baseUrl: string
39
- model: string
40
- extraHeadersText: string
41
- }
42
-
43
- interface ParamDraft {
44
- id: string
45
- name: string
46
- endpoint: string
47
- specsText: string
48
- }
49
-
50
- interface PromptDraft {
51
- id: string
52
- name: string
53
- text: string
54
- }
55
-
56
- interface Draft {
57
- providers: ProviderDraft[]
58
- params: ParamDraft[]
59
- prompts: PromptDraft[]
60
- maxConcurrent: string
61
- requestTimeoutMs: string
62
- proxy: string
63
- galleryMaxItems: string
64
- uploadTtlHours: string
65
- }
66
-
67
- /** 配置值 → 编辑草稿。 */
68
- function draftOf(value: ImageStudioConfig): Draft {
69
- return {
70
- providers: value.providerPresets.map((p) => ({
71
- id: p.id,
72
- name: p.name,
73
- protocol: p.protocol,
74
- baseUrl: p.baseUrl,
75
- model: p.model,
76
- extraHeadersText: JSON.stringify(p.extraHeaders ?? {}, null, 2),
77
- })),
78
- params: value.paramPresets.map((p) => ({
79
- id: p.id,
80
- name: p.name,
81
- endpoint: p.endpoint,
82
- specsText: JSON.stringify(p.paramSpecs ?? {}, null, 2),
83
- })),
84
- prompts: value.promptPresets.map((p) => ({ id: p.id, name: p.name, text: p.text })),
85
- maxConcurrent: String(value.maxConcurrent),
86
- requestTimeoutMs: String(value.requestTimeoutMs),
87
- proxy: value.proxy,
88
- galleryMaxItems: String(value.galleryMaxItems),
89
- uploadTtlHours: String(value.uploadTtlHours),
90
- }
91
- }
92
-
93
- function parseJson(text: string, label: string): unknown {
94
- try {
95
- return JSON.parse(text)
96
- } catch (error) {
97
- throw new Error(`${label} ${error instanceof Error ? error.message : String(error)}`)
98
- }
99
- }
100
-
101
- /** 草稿 → settings 载荷(边界校验:JSON 解析、参数表校验、预设 id 形态)。 */
102
- function payloadOf(draft: Draft): Record<string, unknown> {
103
- const providers = draft.providers.map((p) => {
104
- if (!isValidPresetId(p.id)) throw new Error(p.id)
105
- return {
106
- id: p.id,
107
- name: p.name,
108
- protocol: p.protocol,
109
- baseUrl: p.baseUrl,
110
- model: p.model,
111
- credentialRef: credentialRefNameOf(p.id),
112
- extraHeaders: parseJson(p.extraHeadersText, p.id) as Record<string, string>,
113
- }
114
- })
115
- const params = draft.params.map((p) => ({
116
- id: p.id,
117
- name: p.name,
118
- endpoint: p.endpoint,
119
- paramSpecs: validateParamSpecs(parseJson(p.specsText, p.id)),
120
- }))
121
- return {
122
- promptPresets: draft.prompts.map((p) => ({ id: p.id, name: p.name, text: p.text })),
123
- paramPresets: params,
124
- providerPresets: providers,
125
- maxConcurrent: Math.max(0, Math.floor(Number(draft.maxConcurrent) || 0)),
126
- requestTimeoutMs: Math.max(10_000, Math.floor(Number(draft.requestTimeoutMs) || 300_000)),
127
- proxy: draft.proxy,
128
- galleryMaxItems: Math.max(0, Math.floor(Number(draft.galleryMaxItems) || 0)),
129
- uploadTtlHours: Math.max(1, Math.floor(Number(draft.uploadTtlHours) || 24)),
130
- }
131
- }
132
-
133
- /** 单个提供商预设的 API Key 行(describe 徽标 + 设置/删除,值不回显)。 */
134
- function CredentialRow(props: { t: Translate; presetId: string; disabled: boolean }): ReactElement {
135
- const { t, presetId, disabled } = props
136
- const [configured, setConfigured] = useState<boolean | null>(null)
137
- const [value, setValue] = useState('')
138
- const [busy, setBusy] = useState(false)
139
-
47
+ /** 协议目录(服务端 /providers):驱动提供商预设的协议下拉。 */
48
+ function useProtocols(): Array<{ id: string; label: string }> {
49
+ const [protocols, setProtocols] = useState<Array<{ id: string; label: string }>>([])
140
50
  useEffect(() => {
141
- if (!isValidPresetId(presetId)) return
142
- let alive = true
143
- fetchCredentialStatus(presetId)
144
- .then((res) => {
145
- if (alive) setConfigured(res.configured)
146
- })
51
+ fetchProviders()
52
+ .then((res) => setProtocols(res.protocols.map((p) => ({ id: p.id, label: p.label }))))
147
53
  .catch(() => {})
148
- return () => {
149
- alive = false
150
- }
151
- }, [presetId])
54
+ }, [])
55
+ return protocols
56
+ }
152
57
 
153
- const act = (fn: () => Promise<unknown>): void => {
154
- setBusy(true)
155
- fn()
156
- .then(() => fetchCredentialStatus(presetId))
157
- .then((res) => {
158
- setConfigured(res.configured)
159
- setValue('')
160
- })
161
- .catch(() => {})
162
- .finally(() => setBusy(false))
163
- }
58
+ /** 卡片动作:放弃(回到 Host 值并重置基线)与保存。 */
59
+ function cardActions(options: {
60
+ t: Translate
61
+ dirty: boolean
62
+ saving: boolean
63
+ disabled: boolean
64
+ onDiscard(): void
65
+ onSave(): void
66
+ }): CardAction[] {
67
+ const { t } = options
68
+ return [
69
+ {
70
+ key: 'discard',
71
+ label: t('card.discard'),
72
+ disabled: !options.dirty || options.saving,
73
+ onClick: options.onDiscard,
74
+ },
75
+ {
76
+ key: 'save',
77
+ label: t(options.saving ? 'common.saving' : 'card.save'),
78
+ variant: 'primary',
79
+ disabled: !options.dirty || options.saving || options.disabled,
80
+ onClick: options.onSave,
81
+ },
82
+ ]
83
+ }
164
84
 
85
+ /** 高级项:并发 / 超时 / 代理 / 画廊上限 / 上传保留时长。 */
86
+ function AdvancedFields(props: {
87
+ t: Translate
88
+ draft: Draft
89
+ disabled: boolean
90
+ onPatch(patch: Partial<Draft>): void
91
+ }): ReactElement {
92
+ const { t, draft, disabled, onPatch } = props
165
93
  return (
166
- <div className="imsc-credRow">
167
- <input
168
- className="imsc-input"
169
- type="password"
170
- autoComplete="off"
171
- placeholder={t('card.credential.placeholder')}
172
- aria-label={t('card.credential.label')}
173
- value={value}
174
- disabled={disabled || busy}
175
- onChange={(event) => setValue(event.target.value)}
94
+ <>
95
+ <h4 className="imsc-groupTitle">{t('card.advanced')}</h4>
96
+ <div className="imsc-grid2">
97
+ <TextField
98
+ prefix="imsc"
99
+ id="imsc-conc"
100
+ label={t('card.maxConcurrent')}
101
+ numeric
102
+ value={draft.maxConcurrent}
103
+ disabled={disabled}
104
+ onEdit={(v) => onPatch({ maxConcurrent: v })}
105
+ />
106
+ <TextField
107
+ prefix="imsc"
108
+ id="imsc-timeout"
109
+ label={t('card.requestTimeoutMs')}
110
+ numeric
111
+ value={draft.requestTimeoutMs}
112
+ disabled={disabled}
113
+ onEdit={(v) => onPatch({ requestTimeoutMs: v })}
114
+ />
115
+ </div>
116
+ <div className="imsc-grid2">
117
+ <TextField
118
+ prefix="imsc"
119
+ id="imsc-proxy"
120
+ label={t('card.proxy')}
121
+ value={draft.proxy}
122
+ disabled={disabled}
123
+ onEdit={(v) => onPatch({ proxy: v })}
124
+ />
125
+ <TextField
126
+ prefix="imsc"
127
+ id="imsc-gmax"
128
+ label={t('card.galleryMaxItems')}
129
+ numeric
130
+ value={draft.galleryMaxItems}
131
+ disabled={disabled}
132
+ onEdit={(v) => onPatch({ galleryMaxItems: v })}
133
+ />
134
+ </div>
135
+ <TextField
136
+ prefix="imsc"
137
+ id="imsc-upload-ttl"
138
+ label={t('card.uploadTtlHours')}
139
+ numeric
140
+ value={draft.uploadTtlHours}
141
+ disabled={disabled}
142
+ onEdit={(v) => onPatch({ uploadTtlHours: v })}
176
143
  />
177
- {configured !== null ? (
178
- <span className={`imsc-badge ${configured ? 'imsc-badgeSet' : 'imsc-badgeUnset'}`}>
179
- {t(configured ? 'card.credential.configured' : 'card.credential.unconfigured')}
180
- </span>
181
- ) : null}
182
- <button
183
- type="button"
184
- className="imsc-btn imsc-btnGhost"
185
- disabled={disabled || busy || value === '' || !isValidPresetId(presetId)}
186
- onClick={() => act(() => setCredential(presetId, value))}
187
- >
188
- {t('card.credential.set')}
189
- </button>
190
- <button
191
- type="button"
192
- className="imsc-btn imsc-btnGhost"
193
- disabled={disabled || busy || configured !== true || !isValidPresetId(presetId)}
194
- onClick={() => act(() => unsetCredential(presetId))}
195
- >
196
- {t('card.credential.unset')}
197
- </button>
198
- </div>
144
+ </>
199
145
  )
200
146
  }
201
147
 
@@ -206,19 +152,11 @@ export function StudioConfigCard(props: CardProps): ReactElement | string | null
206
152
  () => scope.getSnapshot(),
207
153
  )
208
154
  const value = snapshot.value as ImageStudioConfig | undefined
209
- // 0.1.6-alpha.2 插件页 page 视图为表单落地页,默认展开;旧槽位无 view,保持折叠。
210
- const [open, setOpen] = useState(props.view === 'page')
211
155
  const [draft, setDraft] = useState<Draft | null>(null)
212
156
  const [baseline, setBaseline] = useState('')
213
157
  const [saving, setSaving] = useState(false)
214
158
  const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
215
- const [protocols, setProtocols] = useState<Array<{ id: string; label: string }>>([])
216
-
217
- useEffect(() => {
218
- fetchProviders()
219
- .then((res) => setProtocols(res.protocols.map((p) => ({ id: p.id, label: p.label }))))
220
- .catch(() => {})
221
- }, [])
159
+ const protocols = useProtocols()
222
160
 
223
161
  useEffect(() => {
224
162
  if (value === undefined || draft !== null) return
@@ -236,11 +174,7 @@ export function StudioConfigCard(props: CardProps): ReactElement | string | null
236
174
  if (props.view === 'summary') return t('card.summary')
237
175
 
238
176
  if (value === undefined || draft === null) {
239
- return (
240
- <li className="imsc-card">
241
- <p className="imsc-loading">{t('common.loading')}</p>
242
- </li>
243
- )
177
+ return <CardLoading prefix="imsc" text={t('common.loading')} />
244
178
  }
245
179
 
246
180
  const disabled = !snapshot.writable
@@ -277,20 +211,25 @@ export function StudioConfigCard(props: CardProps): ReactElement | string | null
277
211
  setDraft({ ...draft, ...next })
278
212
  setStatus(IDLE_STATUS)
279
213
  }
280
- const patchProvider = (index: number, next: Partial<ProviderDraft>): void =>
281
- patch({ providers: draft.providers.map((p, i) => (i === index ? { ...p, ...next } : p)) })
282
- const patchParam = (index: number, next: Partial<ParamDraft>): void =>
283
- patch({ params: draft.params.map((p, i) => (i === index ? { ...p, ...next } : p)) })
284
- const patchPrompt = (index: number, next: Partial<PromptDraft>): void =>
285
- patch({ prompts: draft.prompts.map((p, i) => (i === index ? { ...p, ...next } : p)) })
214
+ const sectionProps = {
215
+ t,
216
+ draft,
217
+ disabled,
218
+ protocols,
219
+ onPatch: patch,
220
+ onPatchProvider: (index: number, next: Partial<ProviderDraft>): void =>
221
+ patch({ providers: draft.providers.map((p, i) => (i === index ? { ...p, ...next } : p)) }),
222
+ onPatchParam: (index: number, next: Partial<ParamDraft>): void =>
223
+ patch({ params: draft.params.map((p, i) => (i === index ? { ...p, ...next } : p)) }),
224
+ onPatchPrompt: (index: number, next: Partial<PromptDraft>): void =>
225
+ patch({ prompts: draft.prompts.map((p, i) => (i === index ? { ...p, ...next } : p)) }),
226
+ }
286
227
 
287
228
  return (
288
229
  <CardChrome
289
230
  prefix="imsc"
290
231
  title={t('card.title')}
291
232
  description={t('card.description')}
292
- open={open}
293
- onToggle={setOpen}
294
233
  statusBadge={{
295
234
  text:
296
235
  draft.providers.length > 0
@@ -302,301 +241,24 @@ export function StudioConfigCard(props: CardProps): ReactElement | string | null
302
241
  dirtyLabel={t('common.unsaved')}
303
242
  readOnlyNotice={disabled ? t('card.readOnly') : undefined}
304
243
  status={status}
305
- actions={[
306
- {
307
- key: 'discard',
308
- label: t('card.discard'),
309
- disabled: !dirty || saving,
310
- onClick: () => {
311
- const restored = draftOf(value)
312
- setDraft(restored)
313
- setBaseline(JSON.stringify(restored))
314
- setStatus(IDLE_STATUS)
315
- },
244
+ actions={cardActions({
245
+ t,
246
+ dirty,
247
+ saving,
248
+ disabled,
249
+ onDiscard: () => {
250
+ const restored = draftOf(value)
251
+ setDraft(restored)
252
+ setBaseline(JSON.stringify(restored))
253
+ setStatus(IDLE_STATUS)
316
254
  },
317
- {
318
- key: 'save',
319
- label: t(saving ? 'common.saving' : 'card.save'),
320
- variant: 'primary',
321
- disabled: !dirty || saving || disabled,
322
- onClick: onSave,
323
- },
324
- ]}
255
+ onSave,
256
+ })}
325
257
  >
326
- <h4 className="imsc-groupTitle">{t('card.providers')}</h4>
327
- {draft.providers.map((provider, index) => (
328
- // biome-ignore lint/suspicious/noArrayIndexKey: 草稿行无稳定 id(id 字段本身可编辑),行序即身份
329
- <div className="imsc-presetBox" key={`provider-${index}`}>
330
- <div className="imsc-presetHead">
331
- <span className="imsc-presetName">{provider.name || provider.id || '—'}</span>
332
- <button
333
- type="button"
334
- className="imsc-btn imsc-btnGhost"
335
- disabled={disabled}
336
- onClick={() => patch({ providers: draft.providers.filter((_, i) => i !== index) })}
337
- >
338
- {t('common.delete')}
339
- </button>
340
- </div>
341
- <div className="imsc-grid2">
342
- <TextField
343
- prefix="imsc"
344
- id={`imsc-pid-${index}`}
345
- label={t('card.field.id')}
346
- hint={t('card.idHint')}
347
- value={provider.id}
348
- invalid={provider.id !== '' && !isValidPresetId(provider.id)}
349
- invalidLabel={t('card.idInvalid')}
350
- disabled={disabled}
351
- onEdit={(v) => patchProvider(index, { id: v })}
352
- />
353
- <TextField
354
- prefix="imsc"
355
- id={`imsc-pname-${index}`}
356
- label={t('card.field.name')}
357
- value={provider.name}
358
- disabled={disabled}
359
- onEdit={(v) => patchProvider(index, { name: v })}
360
- />
361
- </div>
362
- <div className="imsc-grid2">
363
- <div className="imsc-field">
364
- <div className="imsc-head">
365
- <label className="imsc-label" htmlFor={`imsc-pproto-${index}`}>
366
- {t('card.field.protocol')}
367
- </label>
368
- </div>
369
- <select
370
- id={`imsc-pproto-${index}`}
371
- className="imsc-select"
372
- value={provider.protocol}
373
- disabled={disabled}
374
- onChange={(event) => patchProvider(index, { protocol: event.target.value })}
375
- >
376
- {(protocols.length > 0
377
- ? protocols
378
- : [{ id: provider.protocol, label: provider.protocol }]
379
- ).map((protocol) => (
380
- <option key={protocol.id} value={protocol.id}>
381
- {protocol.label}
382
- </option>
383
- ))}
384
- </select>
385
- </div>
386
- <TextField
387
- prefix="imsc"
388
- id={`imsc-pmodel-${index}`}
389
- label={t('card.field.model')}
390
- value={provider.model}
391
- disabled={disabled}
392
- onEdit={(v) => patchProvider(index, { model: v })}
393
- />
394
- </div>
395
- <TextField
396
- prefix="imsc"
397
- id={`imsc-purl-${index}`}
398
- label={t('card.field.baseUrl')}
399
- value={provider.baseUrl}
400
- disabled={disabled}
401
- onEdit={(v) => patchProvider(index, { baseUrl: v })}
402
- />
403
- <div className="imsc-field">
404
- <div className="imsc-head">
405
- <label className="imsc-label" htmlFor={`imsc-pheaders-${index}`}>
406
- {t('card.field.extraHeaders')}
407
- </label>
408
- </div>
409
- <textarea
410
- id={`imsc-pheaders-${index}`}
411
- className="imsc-textarea"
412
- value={provider.extraHeadersText}
413
- disabled={disabled}
414
- onChange={(event) => patchProvider(index, { extraHeadersText: event.target.value })}
415
- />
416
- </div>
417
- <CredentialRow t={t} presetId={provider.id} disabled={disabled} />
418
- </div>
419
- ))}
420
- <button
421
- type="button"
422
- className="imsc-btn imsc-btnGhost"
423
- disabled={disabled}
424
- onClick={() =>
425
- patch({
426
- providers: [
427
- ...draft.providers,
428
- {
429
- id: '',
430
- name: '',
431
- protocol: protocols[0]?.id ?? 'openai-images',
432
- baseUrl: '',
433
- model: '',
434
- extraHeadersText: '{}',
435
- },
436
- ],
437
- })
438
- }
439
- >
440
- {t('card.add')} · {t('card.providers')}
441
- </button>
442
-
443
- <h4 className="imsc-groupTitle">{t('card.prompts')}</h4>
444
- {draft.prompts.map((prompt, index) => (
445
- // biome-ignore lint/suspicious/noArrayIndexKey: 草稿行无稳定 id,行序即身份
446
- <div className="imsc-presetBox" key={`prompt-${index}`}>
447
- <div className="imsc-presetHead">
448
- <input
449
- className="imsc-input"
450
- aria-label={t('card.field.name')}
451
- value={prompt.name}
452
- disabled={disabled}
453
- onChange={(event) => patchPrompt(index, { name: event.target.value })}
454
- />
455
- <button
456
- type="button"
457
- className="imsc-btn imsc-btnGhost"
458
- disabled={disabled}
459
- onClick={() => patch({ prompts: draft.prompts.filter((_, i) => i !== index) })}
460
- >
461
- {t('common.delete')}
462
- </button>
463
- </div>
464
- <textarea
465
- className="imsc-textarea"
466
- aria-label={t('card.field.text')}
467
- value={prompt.text}
468
- disabled={disabled}
469
- onChange={(event) => patchPrompt(index, { text: event.target.value })}
470
- />
471
- </div>
472
- ))}
473
- <button
474
- type="button"
475
- className="imsc-btn imsc-btnGhost"
476
- disabled={disabled}
477
- onClick={() =>
478
- patch({
479
- prompts: [
480
- ...draft.prompts,
481
- { id: `prompt-${Date.now().toString(36)}`, name: '', text: '' },
482
- ],
483
- })
484
- }
485
- >
486
- {t('card.add')} · {t('card.prompts')}
487
- </button>
488
-
489
- <h4 className="imsc-groupTitle">{t('card.params')}</h4>
490
- {draft.params.map((param, index) => (
491
- // biome-ignore lint/suspicious/noArrayIndexKey: 草稿行无稳定 id,行序即身份
492
- <div className="imsc-presetBox" key={`param-${index}`}>
493
- <div className="imsc-presetHead">
494
- <input
495
- className="imsc-input"
496
- aria-label={t('card.field.name')}
497
- value={param.name}
498
- disabled={disabled}
499
- onChange={(event) => patchParam(index, { name: event.target.value })}
500
- />
501
- <select
502
- className="imsc-select"
503
- aria-label={t('card.field.endpoint')}
504
- value={param.endpoint}
505
- disabled={disabled}
506
- onChange={(event) => patchParam(index, { endpoint: event.target.value })}
507
- >
508
- <option value="generation">{t('tab.generation')}</option>
509
- <option value="edit">{t('tab.edit')}</option>
510
- </select>
511
- <button
512
- type="button"
513
- className="imsc-btn imsc-btnGhost"
514
- disabled={disabled}
515
- onClick={() => patch({ params: draft.params.filter((_, i) => i !== index) })}
516
- >
517
- {t('common.delete')}
518
- </button>
519
- </div>
520
- <textarea
521
- className="imsc-textarea"
522
- aria-label={t('card.field.paramSpecs')}
523
- placeholder='{ "size": { "enabled": true, "value": "1024x1024" } }'
524
- value={param.specsText}
525
- disabled={disabled}
526
- onChange={(event) => patchParam(index, { specsText: event.target.value })}
527
- />
528
- </div>
529
- ))}
530
- <button
531
- type="button"
532
- className="imsc-btn imsc-btnGhost"
533
- disabled={disabled}
534
- onClick={() =>
535
- patch({
536
- params: [
537
- ...draft.params,
538
- {
539
- id: `param-${Date.now().toString(36)}`,
540
- name: '',
541
- endpoint: 'generation',
542
- specsText: '{}',
543
- },
544
- ],
545
- })
546
- }
547
- >
548
- {t('card.add')} · {t('card.params')}
549
- </button>
550
-
551
- <h4 className="imsc-groupTitle">{t('card.advanced')}</h4>
552
- <div className="imsc-grid2">
553
- <TextField
554
- prefix="imsc"
555
- id="imsc-conc"
556
- label={t('card.maxConcurrent')}
557
- numeric
558
- value={draft.maxConcurrent}
559
- disabled={disabled}
560
- onEdit={(v) => patch({ maxConcurrent: v })}
561
- />
562
- <TextField
563
- prefix="imsc"
564
- id="imsc-timeout"
565
- label={t('card.requestTimeoutMs')}
566
- numeric
567
- value={draft.requestTimeoutMs}
568
- disabled={disabled}
569
- onEdit={(v) => patch({ requestTimeoutMs: v })}
570
- />
571
- </div>
572
- <div className="imsc-grid2">
573
- <TextField
574
- prefix="imsc"
575
- id="imsc-proxy"
576
- label={t('card.proxy')}
577
- value={draft.proxy}
578
- disabled={disabled}
579
- onEdit={(v) => patch({ proxy: v })}
580
- />
581
- <TextField
582
- prefix="imsc"
583
- id="imsc-gmax"
584
- label={t('card.galleryMaxItems')}
585
- numeric
586
- value={draft.galleryMaxItems}
587
- disabled={disabled}
588
- onEdit={(v) => patch({ galleryMaxItems: v })}
589
- />
590
- </div>
591
- <TextField
592
- prefix="imsc"
593
- id="imsc-upload-ttl"
594
- label={t('card.uploadTtlHours')}
595
- numeric
596
- value={draft.uploadTtlHours}
597
- disabled={disabled}
598
- onEdit={(v) => patch({ uploadTtlHours: v })}
599
- />
258
+ <ProviderPresetSection {...sectionProps} />
259
+ <PromptPresetSection {...sectionProps} />
260
+ <ParamPresetSection {...sectionProps} />
261
+ <AdvancedFields t={t} draft={draft} disabled={disabled} onPatch={patch} />
600
262
  </CardChrome>
601
263
  )
602
264
  }