@dsh-plus/image-studio 0.1.21 → 0.1.23

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.
@@ -5,11 +5,15 @@
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,
16
+ cardVariantFor,
13
17
  IDLE_STATUS,
14
18
  type NamespaceSettingsApi,
15
19
  type PluginConfigViewProps,
@@ -19,10 +23,21 @@ import {
19
23
  import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
20
24
 
21
25
  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'
26
+ import { fetchProviders } from './api.ts'
27
+ import {
28
+ type Draft,
29
+ draftOf,
30
+ type ParamDraft,
31
+ type PromptDraft,
32
+ type ProviderDraft,
33
+ payloadOf,
34
+ } from './draft.ts'
25
35
  import type { Translate } from './i18n.ts'
36
+ import {
37
+ ParamPresetSection,
38
+ PromptPresetSection,
39
+ ProviderPresetSection,
40
+ } from './preset-sections.tsx'
26
41
 
27
42
  export interface CardProps extends PluginConfigViewProps {
28
43
  t: Translate
@@ -30,172 +45,104 @@ export interface CardProps extends PluginConfigViewProps {
30
45
  api: NamespaceSettingsApi
31
46
  }
32
47
 
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
-
48
+ /** 协议目录(服务端 /providers):驱动提供商预设的协议下拉。 */
49
+ function useProtocols(): Array<{ id: string; label: string }> {
50
+ const [protocols, setProtocols] = useState<Array<{ id: string; label: string }>>([])
140
51
  useEffect(() => {
141
- if (!isValidPresetId(presetId)) return
142
- let alive = true
143
- fetchCredentialStatus(presetId)
144
- .then((res) => {
145
- if (alive) setConfigured(res.configured)
146
- })
52
+ fetchProviders()
53
+ .then((res) => setProtocols(res.protocols.map((p) => ({ id: p.id, label: p.label }))))
147
54
  .catch(() => {})
148
- return () => {
149
- alive = false
150
- }
151
- }, [presetId])
55
+ }, [])
56
+ return protocols
57
+ }
152
58
 
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
- }
59
+ /** 卡片动作:放弃(回到 Host 值并重置基线)与保存。 */
60
+ function cardActions(options: {
61
+ t: Translate
62
+ dirty: boolean
63
+ saving: boolean
64
+ disabled: boolean
65
+ onDiscard(): void
66
+ onSave(): void
67
+ }): CardAction[] {
68
+ const { t } = options
69
+ return [
70
+ {
71
+ key: 'discard',
72
+ label: t('card.discard'),
73
+ disabled: !options.dirty || options.saving,
74
+ onClick: options.onDiscard,
75
+ },
76
+ {
77
+ key: 'save',
78
+ label: t(options.saving ? 'common.saving' : 'card.save'),
79
+ variant: 'primary',
80
+ disabled: !options.dirty || options.saving || options.disabled,
81
+ onClick: options.onSave,
82
+ },
83
+ ]
84
+ }
164
85
 
86
+ /** 高级项:并发 / 超时 / 代理 / 画廊上限 / 上传保留时长。 */
87
+ function AdvancedFields(props: {
88
+ t: Translate
89
+ draft: Draft
90
+ disabled: boolean
91
+ onPatch(patch: Partial<Draft>): void
92
+ }): ReactElement {
93
+ const { t, draft, disabled, onPatch } = props
165
94
  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)}
95
+ <>
96
+ <h4 className="imsc-groupTitle">{t('card.advanced')}</h4>
97
+ <div className="imsc-grid2">
98
+ <TextField
99
+ prefix="imsc"
100
+ id="imsc-conc"
101
+ label={t('card.maxConcurrent')}
102
+ numeric
103
+ value={draft.maxConcurrent}
104
+ disabled={disabled}
105
+ onEdit={(v) => onPatch({ maxConcurrent: v })}
106
+ />
107
+ <TextField
108
+ prefix="imsc"
109
+ id="imsc-timeout"
110
+ label={t('card.requestTimeoutMs')}
111
+ numeric
112
+ value={draft.requestTimeoutMs}
113
+ disabled={disabled}
114
+ onEdit={(v) => onPatch({ requestTimeoutMs: v })}
115
+ />
116
+ </div>
117
+ <div className="imsc-grid2">
118
+ <TextField
119
+ prefix="imsc"
120
+ id="imsc-proxy"
121
+ label={t('card.proxy')}
122
+ value={draft.proxy}
123
+ disabled={disabled}
124
+ onEdit={(v) => onPatch({ proxy: v })}
125
+ />
126
+ <TextField
127
+ prefix="imsc"
128
+ id="imsc-gmax"
129
+ label={t('card.galleryMaxItems')}
130
+ numeric
131
+ value={draft.galleryMaxItems}
132
+ disabled={disabled}
133
+ onEdit={(v) => onPatch({ galleryMaxItems: v })}
134
+ />
135
+ </div>
136
+ <TextField
137
+ prefix="imsc"
138
+ id="imsc-upload-ttl"
139
+ label={t('card.uploadTtlHours')}
140
+ numeric
141
+ value={draft.uploadTtlHours}
142
+ disabled={disabled}
143
+ onEdit={(v) => onPatch({ uploadTtlHours: v })}
176
144
  />
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>
145
+ </>
199
146
  )
200
147
  }
201
148
 
@@ -212,13 +159,7 @@ export function StudioConfigCard(props: CardProps): ReactElement | string | null
212
159
  const [baseline, setBaseline] = useState('')
213
160
  const [saving, setSaving] = useState(false)
214
161
  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
- }, [])
162
+ const protocols = useProtocols()
222
163
 
223
164
  useEffect(() => {
224
165
  if (value === undefined || draft !== null) return
@@ -235,12 +176,11 @@ export function StudioConfigCard(props: CardProps): ReactElement | string | null
235
176
  // 插件页 summary 视图只出一行简介(hooks 已全部落定,可安全提前返回)。
236
177
  if (props.view === 'summary') return t('card.summary')
237
178
 
179
+ // 插件页 page 视图内嵌宿主页面容器(已带页面级内边距与标题),用无边框分节。
180
+ const variant = cardVariantFor(props.view)
181
+
238
182
  if (value === undefined || draft === null) {
239
- return (
240
- <li className="imsc-card">
241
- <p className="imsc-loading">{t('common.loading')}</p>
242
- </li>
243
- )
183
+ return <CardLoading prefix="imsc" variant={variant} text={t('common.loading')} />
244
184
  }
245
185
 
246
186
  const disabled = !snapshot.writable
@@ -277,12 +217,19 @@ export function StudioConfigCard(props: CardProps): ReactElement | string | null
277
217
  setDraft({ ...draft, ...next })
278
218
  setStatus(IDLE_STATUS)
279
219
  }
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)) })
220
+ const sectionProps = {
221
+ t,
222
+ draft,
223
+ disabled,
224
+ protocols,
225
+ onPatch: patch,
226
+ onPatchProvider: (index: number, next: Partial<ProviderDraft>): void =>
227
+ patch({ providers: draft.providers.map((p, i) => (i === index ? { ...p, ...next } : p)) }),
228
+ onPatchParam: (index: number, next: Partial<ParamDraft>): void =>
229
+ patch({ params: draft.params.map((p, i) => (i === index ? { ...p, ...next } : p)) }),
230
+ onPatchPrompt: (index: number, next: Partial<PromptDraft>): void =>
231
+ patch({ prompts: draft.prompts.map((p, i) => (i === index ? { ...p, ...next } : p)) }),
232
+ }
286
233
 
287
234
  return (
288
235
  <CardChrome
@@ -291,6 +238,7 @@ export function StudioConfigCard(props: CardProps): ReactElement | string | null
291
238
  description={t('card.description')}
292
239
  open={open}
293
240
  onToggle={setOpen}
241
+ variant={variant}
294
242
  statusBadge={{
295
243
  text:
296
244
  draft.providers.length > 0
@@ -302,301 +250,24 @@ export function StudioConfigCard(props: CardProps): ReactElement | string | null
302
250
  dirtyLabel={t('common.unsaved')}
303
251
  readOnlyNotice={disabled ? t('card.readOnly') : undefined}
304
252
  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
- },
253
+ actions={cardActions({
254
+ t,
255
+ dirty,
256
+ saving,
257
+ disabled,
258
+ onDiscard: () => {
259
+ const restored = draftOf(value)
260
+ setDraft(restored)
261
+ setBaseline(JSON.stringify(restored))
262
+ setStatus(IDLE_STATUS)
316
263
  },
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
- ]}
264
+ onSave,
265
+ })}
325
266
  >
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
- />
267
+ <ProviderPresetSection {...sectionProps} />
268
+ <PromptPresetSection {...sectionProps} />
269
+ <ParamPresetSection {...sectionProps} />
270
+ <AdvancedFields t={t} draft={draft} disabled={disabled} onPatch={patch} />
600
271
  </CardChrome>
601
272
  )
602
273
  }