@dickpy/dsh-imagegen 1.0.20 → 1.2.0
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.
- package/README.md +46 -13
- package/docs/images/agent-chat-edit.png +0 -0
- package/docs/images/agent-chat-generate.png +0 -0
- package/docs/images/multi-model-comparison.png +0 -0
- package/lib/client.js +1677 -538
- package/lib/client.js.map +1 -1
- package/lib/index.js +877 -57
- package/package.json +4 -1
- package/src/agent-image-tools.ts +289 -0
- package/src/client/ImageGenPanel.tsx +333 -46
- package/src/client/SettingsCard.tsx +293 -23
- package/src/client/api.ts +37 -1
- package/src/client/locales.ts +150 -2
- package/src/client/panel.module.css +129 -1
- package/src/client/settings-card.module.css +222 -0
- package/src/client/settings-form.ts +12 -0
- package/src/client/settings-scope.ts +24 -1
- package/src/engine.ts +32 -4
- package/src/gallery-store.ts +15 -1
- package/src/generation-runtime.ts +48 -0
- package/src/image-models.ts +19 -0
- package/src/index.ts +72 -3
- package/src/prompt-enhancer.ts +79 -0
- package/src/protocol.ts +37 -2
- package/src/routes.ts +142 -40
- package/src/task-queue.ts +103 -0
|
@@ -9,17 +9,22 @@
|
|
|
9
9
|
import { useState } from 'react'
|
|
10
10
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
11
11
|
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
12
|
-
import { CardForm, booleanField, secretField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
|
|
12
|
+
import { CardForm, booleanField, secretField, stringListField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
|
|
13
13
|
import type { ImageGenScope } from './settings-scope.ts'
|
|
14
|
-
import {
|
|
14
|
+
import { IMAGE_MODEL_API, PROMPT_ENHANCE_API } from '../protocol.ts'
|
|
15
15
|
import css from './settings-card.module.css'
|
|
16
16
|
|
|
17
17
|
/** The fields this card edits (the namespace's full schema). */
|
|
18
18
|
export interface ImageGenSettings {
|
|
19
19
|
enabled?: boolean
|
|
20
20
|
announceToAgent?: boolean
|
|
21
|
+
allowAgentImageGeneration?: boolean
|
|
21
22
|
apiUrl?: string
|
|
22
23
|
apiKey?: string
|
|
24
|
+
imageModels?: string[]
|
|
25
|
+
promptApiUrl?: string
|
|
26
|
+
promptApiKey?: string
|
|
27
|
+
promptModel?: string
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/** What the card renders. */
|
|
@@ -28,10 +33,15 @@ export interface ImageGenSettingsCardState extends CardShell {
|
|
|
28
33
|
enabled: CardFieldState
|
|
29
34
|
/** System-prompt announcement flag. */
|
|
30
35
|
announceToAgent: CardFieldState
|
|
36
|
+
allowAgentImageGeneration: CardFieldState
|
|
31
37
|
/** API base URL. */
|
|
32
38
|
apiUrl: CardFieldState
|
|
33
39
|
/** API key draft (the stored value is never rendered). */
|
|
34
40
|
apiKey: CardFieldState
|
|
41
|
+
imageModels: CardFieldState
|
|
42
|
+
promptApiUrl: CardFieldState
|
|
43
|
+
promptApiKey: CardFieldState
|
|
44
|
+
promptModel: CardFieldState
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
/** The registration-side face the card's slot entry injects. */
|
|
@@ -53,8 +63,13 @@ export class ImageGenSettingsCardController {
|
|
|
53
63
|
this.form = new CardForm(scope, [
|
|
54
64
|
booleanField('enabled'),
|
|
55
65
|
booleanField('announceToAgent'),
|
|
66
|
+
booleanField('allowAgentImageGeneration'),
|
|
56
67
|
textField('apiUrl'),
|
|
57
68
|
secretField('apiKey'),
|
|
69
|
+
stringListField('imageModels'),
|
|
70
|
+
textField('promptApiUrl'),
|
|
71
|
+
secretField('promptApiKey'),
|
|
72
|
+
textField('promptModel'),
|
|
58
73
|
], {
|
|
59
74
|
// The redacted wire view never returns the key; a save's outcome is
|
|
60
75
|
// judged by the namespace's secrets sidecar instead.
|
|
@@ -67,8 +82,13 @@ export class ImageGenSettingsCardController {
|
|
|
67
82
|
...this.form.shell(),
|
|
68
83
|
enabled: this.form.field('enabled'),
|
|
69
84
|
announceToAgent: this.form.field('announceToAgent'),
|
|
85
|
+
allowAgentImageGeneration: this.form.field('allowAgentImageGeneration'),
|
|
70
86
|
apiUrl: this.form.field('apiUrl'),
|
|
71
87
|
apiKey: this.form.field('apiKey'),
|
|
88
|
+
imageModels: this.form.field('imageModels'),
|
|
89
|
+
promptApiUrl: this.form.field('promptApiUrl'),
|
|
90
|
+
promptApiKey: this.form.field('promptApiKey'),
|
|
91
|
+
promptModel: this.form.field('promptModel'),
|
|
72
92
|
}
|
|
73
93
|
}
|
|
74
94
|
|
|
@@ -106,6 +126,19 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
106
126
|
const state = props.useImageGenSettingsCard(snapshot => snapshot)
|
|
107
127
|
const keySet = props.useImageGenKeySet(snapshot => snapshot)
|
|
108
128
|
const [open, setOpen] = useState(false)
|
|
129
|
+
const [promptModels, setPromptModels] = useState<string[]>([])
|
|
130
|
+
const [loadingPromptModels, setLoadingPromptModels] = useState(false)
|
|
131
|
+
const [promptModelsError, setPromptModelsError] = useState<string | null>(null)
|
|
132
|
+
const [imageModelCandidates, setImageModelCandidates] = useState<string[]>([])
|
|
133
|
+
const [loadingImageModels, setLoadingImageModels] = useState(false)
|
|
134
|
+
const [imageModelsError, setImageModelsError] = useState<string | null>(null)
|
|
135
|
+
const [manualModelsOpen, setManualModelsOpen] = useState(false)
|
|
136
|
+
const [manualModel, setManualModel] = useState('')
|
|
137
|
+
const [enhancementOpen, setEnhancementOpen] = useState(false)
|
|
138
|
+
const [manualPromptModelOpen, setManualPromptModelOpen] = useState(false)
|
|
139
|
+
const [manualPromptModel, setManualPromptModel] = useState('')
|
|
140
|
+
const [promptApiOpen, setPromptApiOpen] = useState(false)
|
|
141
|
+
const [moreOpen, setMoreOpen] = useState(false)
|
|
109
142
|
if (!state.available) return null
|
|
110
143
|
const title = t('settings.title')
|
|
111
144
|
const blocked = !state.dirty || state.invalid || state.saving
|
|
@@ -162,10 +195,16 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
162
195
|
? (
|
|
163
196
|
<div className={css.body}>
|
|
164
197
|
{!state.writable ? <p className={css.readOnly} role="status">{t('settings.readOnly')}</p> : null}
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
198
|
+
<ValueField
|
|
199
|
+
id="dsh-imagegen-settings-apiurl"
|
|
200
|
+
label={t('settings.apiUrl')}
|
|
201
|
+
hint={t('settings.apiUrlHint')}
|
|
202
|
+
placeholder="https://api.openai.com/v1"
|
|
203
|
+
{...fieldProps}
|
|
204
|
+
{...state.apiUrl}
|
|
205
|
+
onEdit={(text) => { props.edit('apiUrl', text) }}
|
|
206
|
+
onReset={() => { props.resetField('apiUrl') }}
|
|
207
|
+
/>
|
|
169
208
|
<ValueField
|
|
170
209
|
id="dsh-imagegen-settings-apikey"
|
|
171
210
|
label={t('settings.apiKey')}
|
|
@@ -181,16 +220,216 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
181
220
|
onClear={() => { props.resetField('apiKey') }}
|
|
182
221
|
canClear={keySet}
|
|
183
222
|
/>
|
|
223
|
+
<div className={css.sectionDivider} />
|
|
224
|
+
<section className={css.modelSection} aria-label={t('settings.imageModelsTitle')}>
|
|
225
|
+
<div className={css.sectionHeader}>
|
|
226
|
+
<div>
|
|
227
|
+
<h3 className={css.sectionTitle}>{t('settings.imageModelsTitle')}</h3>
|
|
228
|
+
<p className={css.sectionHint}>{t('settings.imageModelsHint')}</p>
|
|
229
|
+
</div>
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
className={css.modelFetch}
|
|
233
|
+
disabled={disabled || loadingImageModels}
|
|
234
|
+
onClick={() => {
|
|
235
|
+
setLoadingImageModels(true)
|
|
236
|
+
setImageModelsError(null)
|
|
237
|
+
void fetch(IMAGE_MODEL_API.models, { method: 'POST' })
|
|
238
|
+
.then(async response => {
|
|
239
|
+
const body = await response.json() as { ok?: boolean; models?: string[]; message?: string }
|
|
240
|
+
if (!response.ok || body.ok !== true) throw new Error(body.message ?? `HTTP ${response.status}`)
|
|
241
|
+
setImageModelCandidates(body.models ?? [])
|
|
242
|
+
})
|
|
243
|
+
.catch(error => { setImageModelsError(error instanceof Error ? error.message : String(error)) })
|
|
244
|
+
.finally(() => { setLoadingImageModels(false) })
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
{loadingImageModels ? t('settings.imageModelsLoading') : t('settings.imageModelsFetch')}
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
<div className={css.modelSummary}>
|
|
251
|
+
{splitModels(state.imageModels.text).map(model => (
|
|
252
|
+
<span key={model} className={css.modelChip}>
|
|
253
|
+
<span>{model}</span>
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
disabled={disabled}
|
|
257
|
+
aria-label={`${t('settings.removeModel')}: ${model}`}
|
|
258
|
+
onClick={() => { props.edit('imageModels', splitModels(state.imageModels.text).filter(value => value !== model).join('\n')) }}
|
|
259
|
+
>
|
|
260
|
+
×
|
|
261
|
+
</button>
|
|
262
|
+
</span>
|
|
263
|
+
))}
|
|
264
|
+
<button type="button" className={css.addModel} disabled={disabled} onClick={() => { setManualModelsOpen(open => !open) }}>
|
|
265
|
+
{manualModelsOpen ? t('settings.cancelAddModel') : t('settings.addModel')}
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
{manualModelsOpen ? (
|
|
269
|
+
<div className={css.manualModelRow}>
|
|
270
|
+
<input
|
|
271
|
+
className={css.input}
|
|
272
|
+
value={manualModel}
|
|
273
|
+
placeholder={t('settings.addModelPlaceholder')}
|
|
274
|
+
disabled={disabled}
|
|
275
|
+
onChange={event => { setManualModel(event.target.value) }}
|
|
276
|
+
onKeyDown={event => {
|
|
277
|
+
if (event.key !== 'Enter') return
|
|
278
|
+
event.preventDefault()
|
|
279
|
+
const next = manualModel.trim()
|
|
280
|
+
if (next === '') return
|
|
281
|
+
props.edit('imageModels', [...splitModels(state.imageModels.text), next].join('\n'))
|
|
282
|
+
setManualModel('')
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
className={css.addModel}
|
|
288
|
+
disabled={disabled || manualModel.trim() === ''}
|
|
289
|
+
onClick={() => {
|
|
290
|
+
props.edit('imageModels', [...splitModels(state.imageModels.text), manualModel.trim()].join('\n'))
|
|
291
|
+
setManualModel('')
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
{t('settings.addModelConfirm')}
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
) : null}
|
|
298
|
+
{imageModelCandidates.length > 0 ? (
|
|
299
|
+
<div className={css.modelCandidateList} role="group" aria-label={t('settings.imageModelsCandidates')}>
|
|
300
|
+
<span className={css.modelCandidateLabel}>{t('settings.imageModelsCandidates')}</span>
|
|
301
|
+
{imageModelCandidates.map(candidate => {
|
|
302
|
+
const selected = splitModels(state.imageModels.text).includes(candidate)
|
|
303
|
+
return (
|
|
304
|
+
<label key={candidate} className={css.modelCandidate} data-selected={selected ? '' : undefined}>
|
|
305
|
+
<input
|
|
306
|
+
type="checkbox"
|
|
307
|
+
checked={selected}
|
|
308
|
+
disabled={disabled}
|
|
309
|
+
onChange={() => {
|
|
310
|
+
const selectedModels = splitModels(state.imageModels.text)
|
|
311
|
+
const next = selected ? selectedModels.filter(model => model !== candidate) : [...selectedModels, candidate]
|
|
312
|
+
props.edit('imageModels', next.join('\n'))
|
|
313
|
+
}}
|
|
314
|
+
/>
|
|
315
|
+
<span>{candidate}</span>
|
|
316
|
+
</label>
|
|
317
|
+
)
|
|
318
|
+
})}
|
|
319
|
+
</div>
|
|
320
|
+
) : null}
|
|
321
|
+
{imageModelsError !== null ? <p className={css.failed} role="status">{imageModelsError}</p> : null}
|
|
322
|
+
</section>
|
|
323
|
+
<button
|
|
324
|
+
type="button"
|
|
325
|
+
className={css.disclosure}
|
|
326
|
+
aria-expanded={enhancementOpen}
|
|
327
|
+
onClick={() => { setEnhancementOpen(open => !open) }}
|
|
328
|
+
>
|
|
329
|
+
<span>{t('settings.promptEnhanceTitle')}</span>
|
|
330
|
+
<span>{t('settings.optional')}</span>
|
|
331
|
+
<span aria-hidden="true">{enhancementOpen ? '⌃' : '⌄'}</span>
|
|
332
|
+
</button>
|
|
333
|
+
{enhancementOpen ? <section className={css.optionalContent} aria-label={t('settings.promptEnhanceTitle')}>
|
|
334
|
+
<p className={css.sectionHint}>{t('settings.promptEnhanceHint')}</p>
|
|
335
|
+
<div className={css.sectionHeader}>
|
|
336
|
+
<div>
|
|
337
|
+
<h3 className={css.sectionTitle}>{t('settings.promptModel')}</h3>
|
|
338
|
+
<p className={css.sectionHint}>{t('settings.promptModelDetectionHint')}</p>
|
|
339
|
+
</div>
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
className={css.modelFetch}
|
|
343
|
+
disabled={disabled || loadingPromptModels}
|
|
344
|
+
onClick={() => {
|
|
345
|
+
setLoadingPromptModels(true)
|
|
346
|
+
setPromptModelsError(null)
|
|
347
|
+
void fetch(PROMPT_ENHANCE_API.models, { method: 'POST' })
|
|
348
|
+
.then(async response => {
|
|
349
|
+
const body = await response.json() as { ok?: boolean; models?: string[]; message?: string }
|
|
350
|
+
if (!response.ok || body.ok !== true) throw new Error(body.message ?? `HTTP ${response.status}`)
|
|
351
|
+
setPromptModels(body.models ?? [])
|
|
352
|
+
})
|
|
353
|
+
.catch(error => { setPromptModelsError(error instanceof Error ? error.message : String(error)) })
|
|
354
|
+
.finally(() => { setLoadingPromptModels(false) })
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
{loadingPromptModels ? t('settings.promptModelsLoading') : t('settings.promptModelsFetch')}
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
<div className={css.modelSummary}>
|
|
361
|
+
{state.promptModel.text.trim() !== '' ? (
|
|
362
|
+
<span className={css.modelChip}>
|
|
363
|
+
<span>{state.promptModel.text}</span>
|
|
364
|
+
<button type="button" disabled={disabled} aria-label={`${t('settings.removeModel')}: ${state.promptModel.text}`} onClick={() => { props.edit('promptModel', '') }}>×</button>
|
|
365
|
+
</span>
|
|
366
|
+
) : null}
|
|
367
|
+
<button type="button" className={css.addModel} disabled={disabled} onClick={() => { setManualPromptModelOpen(open => !open) }}>
|
|
368
|
+
{manualPromptModelOpen ? t('settings.cancelAddModel') : t('settings.addModel')}
|
|
369
|
+
</button>
|
|
370
|
+
</div>
|
|
371
|
+
{manualPromptModelOpen ? (
|
|
372
|
+
<div className={css.manualModelRow}>
|
|
373
|
+
<input className={css.input} value={manualPromptModel} placeholder={t('settings.addPromptModelPlaceholder')} disabled={disabled} onChange={event => { setManualPromptModel(event.target.value) }} />
|
|
374
|
+
<button type="button" className={css.addModel} disabled={disabled || manualPromptModel.trim() === ''} onClick={() => { props.edit('promptModel', manualPromptModel); setManualPromptModel('') }}>
|
|
375
|
+
{t('settings.addModelConfirm')}
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
) : null}
|
|
379
|
+
{promptModels.length > 0 ? (
|
|
380
|
+
<div className={css.modelCandidateList} role="radiogroup" aria-label={t('settings.promptModelsCandidates')}>
|
|
381
|
+
<span className={css.modelCandidateLabel}>{t('settings.promptModelsCandidates')}</span>
|
|
382
|
+
{promptModels.map(candidate => (
|
|
383
|
+
<button
|
|
384
|
+
key={candidate}
|
|
385
|
+
type="button"
|
|
386
|
+
role="radio"
|
|
387
|
+
className={css.modelCandidate}
|
|
388
|
+
aria-checked={state.promptModel.text === candidate}
|
|
389
|
+
data-selected={state.promptModel.text === candidate ? '' : undefined}
|
|
390
|
+
disabled={disabled}
|
|
391
|
+
onClick={() => { props.edit('promptModel', candidate) }}
|
|
392
|
+
>
|
|
393
|
+
{candidate}
|
|
394
|
+
</button>
|
|
395
|
+
))}
|
|
396
|
+
</div>
|
|
397
|
+
) : null}
|
|
398
|
+
{promptModelsError !== null ? <p className={css.failed} role="status">{promptModelsError}</p> : null}
|
|
399
|
+
<button type="button" className={css.inlineDisclosure} aria-expanded={promptApiOpen} onClick={() => { setPromptApiOpen(open => !open) }}>
|
|
400
|
+
<span>{t('settings.promptApiAdvanced')}</span>
|
|
401
|
+
<span aria-hidden="true">{promptApiOpen ? '⌃' : '⌄'}</span>
|
|
402
|
+
</button>
|
|
403
|
+
{promptApiOpen ? <div className={css.optionalContent}>
|
|
184
404
|
<ValueField
|
|
185
|
-
id="dsh-imagegen-settings-apiurl"
|
|
186
|
-
label={t('settings.
|
|
187
|
-
hint={t('settings.
|
|
405
|
+
id="dsh-imagegen-settings-prompt-apiurl"
|
|
406
|
+
label={t('settings.promptApiUrl')}
|
|
407
|
+
hint={t('settings.promptApiUrlHint')}
|
|
188
408
|
placeholder="https://api.openai.com/v1"
|
|
189
409
|
{...fieldProps}
|
|
190
|
-
{...state.
|
|
191
|
-
onEdit={(text) => { props.edit('
|
|
192
|
-
onReset={() => { props.resetField('
|
|
410
|
+
{...state.promptApiUrl}
|
|
411
|
+
onEdit={(text) => { props.edit('promptApiUrl', text) }}
|
|
412
|
+
onReset={() => { props.resetField('promptApiUrl') }}
|
|
413
|
+
/>
|
|
414
|
+
<ValueField
|
|
415
|
+
id="dsh-imagegen-settings-prompt-apikey"
|
|
416
|
+
label={t('settings.promptApiKey')}
|
|
417
|
+
hint={t('settings.promptApiKeyHint')}
|
|
418
|
+
placeholder="sk-…"
|
|
419
|
+
secret
|
|
420
|
+
{...fieldProps}
|
|
421
|
+
{...state.promptApiKey}
|
|
422
|
+
overridden={false}
|
|
423
|
+
onEdit={(text) => { props.edit('promptApiKey', text) }}
|
|
424
|
+
onReset={() => { props.resetField('promptApiKey') }}
|
|
193
425
|
/>
|
|
426
|
+
</div> : null}
|
|
427
|
+
</section> : null}
|
|
428
|
+
<button type="button" className={css.disclosure} aria-expanded={moreOpen} onClick={() => { setMoreOpen(open => !open) }}>
|
|
429
|
+
<span>{t('settings.moreOptions')}</span>
|
|
430
|
+
<span aria-hidden="true">{moreOpen ? '⌃' : '⌄'}</span>
|
|
431
|
+
</button>
|
|
432
|
+
{moreOpen ? <div className={css.optionalContent}>
|
|
194
433
|
<BooleanField
|
|
195
434
|
id="dsh-imagegen-settings-enabled"
|
|
196
435
|
label={t('settings.enabled')}
|
|
@@ -215,6 +454,19 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
215
454
|
onEdit={(text) => { props.edit('announceToAgent', text) }}
|
|
216
455
|
onReset={() => { props.resetField('announceToAgent') }}
|
|
217
456
|
/>
|
|
457
|
+
<BooleanField
|
|
458
|
+
id="dsh-imagegen-settings-agent-generation"
|
|
459
|
+
label={t('settings.allowAgentImageGeneration')}
|
|
460
|
+
hint={t('settings.allowAgentImageGenerationHint')}
|
|
461
|
+
inheritLabel={t('settings.inherit')}
|
|
462
|
+
onLabel={t('settings.on')}
|
|
463
|
+
offLabel={t('settings.off')}
|
|
464
|
+
{...fieldProps}
|
|
465
|
+
{...state.allowAgentImageGeneration}
|
|
466
|
+
onEdit={(text) => { props.edit('allowAgentImageGeneration', text) }}
|
|
467
|
+
onReset={() => { props.resetField('allowAgentImageGeneration') }}
|
|
468
|
+
/>
|
|
469
|
+
</div> : null}
|
|
218
470
|
<div className={css.footer}>
|
|
219
471
|
{state.failed ? <p className={css.failed} role="status">{t('settings.saveFailed')}</p> : null}
|
|
220
472
|
<button
|
|
@@ -281,6 +533,8 @@ function ValueField(props: FieldProps & {
|
|
|
281
533
|
onClear?: () => void
|
|
282
534
|
/** Whether a stored secret exists (enables the clear control). */
|
|
283
535
|
canClear?: boolean
|
|
536
|
+
/** Render a multiline input for newline-separated configuration values. */
|
|
537
|
+
multiline?: boolean
|
|
284
538
|
}) {
|
|
285
539
|
return (
|
|
286
540
|
<div className={css.field}>
|
|
@@ -314,17 +568,29 @@ function ValueField(props: FieldProps & {
|
|
|
314
568
|
)
|
|
315
569
|
: null}
|
|
316
570
|
</div>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
571
|
+
{props.multiline === true ? (
|
|
572
|
+
<textarea
|
|
573
|
+
id={props.id}
|
|
574
|
+
className={props.invalid ? css.textareaInvalid : css.textarea}
|
|
575
|
+
{...props.invalid ? { 'aria-invalid': true } : {}}
|
|
576
|
+
value={props.text}
|
|
577
|
+
placeholder={props.placeholder ?? ''}
|
|
578
|
+
disabled={props.disabled}
|
|
579
|
+
onChange={(event) => { props.onEdit(event.target.value) }}
|
|
580
|
+
/>
|
|
581
|
+
) : (
|
|
582
|
+
<input
|
|
583
|
+
id={props.id}
|
|
584
|
+
className={props.invalid ? css.inputInvalid : css.input}
|
|
585
|
+
type={props.secret === true ? 'password' : 'text'}
|
|
586
|
+
autoComplete={props.secret === true ? 'off' : undefined}
|
|
587
|
+
{...props.invalid ? { 'aria-invalid': true } : {}}
|
|
588
|
+
value={props.text}
|
|
589
|
+
placeholder={props.placeholder ?? ''}
|
|
590
|
+
disabled={props.disabled}
|
|
591
|
+
onChange={(event) => { props.onEdit(event.target.value) }}
|
|
592
|
+
/>
|
|
593
|
+
)}
|
|
328
594
|
<p className={props.invalid ? css.invalid : css.hint}>
|
|
329
595
|
{props.invalid ? props.invalidLabel : props.hint}
|
|
330
596
|
</p>
|
|
@@ -332,6 +598,10 @@ function ValueField(props: FieldProps & {
|
|
|
332
598
|
)
|
|
333
599
|
}
|
|
334
600
|
|
|
601
|
+
function splitModels(value: string): string[] {
|
|
602
|
+
return [...new Set(value.split(/[\n,]/).map(model => model.trim()).filter(Boolean))]
|
|
603
|
+
}
|
|
604
|
+
|
|
335
605
|
/** A staged boolean field: 继承 / 开 / 关. */
|
|
336
606
|
function BooleanField(props: FieldProps & {
|
|
337
607
|
/** Copy for the inherit option. */
|
package/src/client/api.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* data access path the panel uses — plain fetch, same origin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { GALLERY_API, GENERATE_API, HISTORY_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult, type UpdateInfo } from '../protocol.ts'
|
|
6
|
+
import { GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult, type UpdateInfo } from '../protocol.ts'
|
|
7
7
|
|
|
8
8
|
/** Error carrying the route's JSON error message. */
|
|
9
9
|
export class ImageGenApiError extends Error {
|
|
@@ -73,6 +73,37 @@ export class ImageGenApi {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Ask the configured chat model to expand a concise image prompt. */
|
|
77
|
+
async enhancePrompt(prompt: string): Promise<string> {
|
|
78
|
+
const response = await fetch(PROMPT_ENHANCE_API.enhance, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'content-type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({ prompt }),
|
|
82
|
+
})
|
|
83
|
+
const body = await readEnvelope<{ ok: true; prompt: string }>(response)
|
|
84
|
+
return body.prompt
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async taskSubmit(request: GenerateRequest): Promise<GenerationTask> {
|
|
88
|
+
const response = await fetch(TASK_API.submit, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(request) })
|
|
89
|
+
return (await readEnvelope<{ ok: true; task: GenerationTask }>(response)).task
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async taskList(): Promise<GenerationTask[]> {
|
|
93
|
+
const response = await fetch(TASK_API.list, { method: 'POST' })
|
|
94
|
+
return (await readEnvelope<{ ok: true; tasks: GenerationTask[] }>(response)).tasks
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async taskCancel(id: string): Promise<GenerationTask> {
|
|
98
|
+
const response = await fetch(TASK_API.cancel, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) })
|
|
99
|
+
return (await readEnvelope<{ ok: true; task: GenerationTask }>(response)).task
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async taskRetry(id: string): Promise<GenerationTask> {
|
|
103
|
+
const response = await fetch(TASK_API.retry, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) })
|
|
104
|
+
return (await readEnvelope<{ ok: true; task: GenerationTask }>(response)).task
|
|
105
|
+
}
|
|
106
|
+
|
|
76
107
|
/** List the host-persisted history (newest first). */
|
|
77
108
|
async historyList(): Promise<HistoryEntry[]> {
|
|
78
109
|
const response = await fetch(HISTORY_API.list, { method: 'POST' })
|
|
@@ -135,6 +166,11 @@ export class ImageGenApi {
|
|
|
135
166
|
return body.entries
|
|
136
167
|
}
|
|
137
168
|
|
|
169
|
+
async gallerySetTags(id: string, tags: string[]): Promise<HistoryEntry[]> {
|
|
170
|
+
const response = await fetch(GALLERY_API.tags, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, tags }) })
|
|
171
|
+
return (await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)).entries
|
|
172
|
+
}
|
|
173
|
+
|
|
138
174
|
/** Fetch the prompt-template library (bundled snapshot or refreshed copy). */
|
|
139
175
|
async templatesList(): Promise<TemplateListResult> {
|
|
140
176
|
const response = await fetch(TEMPLATES_API.list, { method: 'POST' })
|