@dickpy/dsh-imagegen 1.1.0 → 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 +32 -10
- package/docs/images/agent-chat-edit.png +0 -0
- package/docs/images/agent-chat-generate.png +0 -0
- package/lib/client.js +687 -355
- package/lib/client.js.map +1 -1
- package/lib/index.js +664 -164
- package/package.json +4 -1
- package/src/agent-image-tools.ts +289 -0
- package/src/client/ImageGenPanel.tsx +30 -22
- package/src/client/SettingsCard.tsx +261 -70
- package/src/client/locales.ts +40 -0
- package/src/client/settings-card.module.css +180 -0
- package/src/client/settings-form.ts +12 -0
- package/src/client/settings-scope.ts +2 -0
- package/src/engine.ts +29 -2
- package/src/generation-runtime.ts +48 -0
- package/src/image-models.ts +19 -0
- package/src/index.ts +48 -2
- package/src/prompt-enhancer.ts +17 -5
- package/src/protocol.ts +6 -1
- package/src/routes.ts +41 -33
- package/src/task-queue.ts +36 -3
|
@@ -9,18 +9,19 @@
|
|
|
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 {
|
|
15
|
-
import { PROMPT_ENHANCE_API } from '../protocol.ts'
|
|
14
|
+
import { IMAGE_MODEL_API, PROMPT_ENHANCE_API } from '../protocol.ts'
|
|
16
15
|
import css from './settings-card.module.css'
|
|
17
16
|
|
|
18
17
|
/** The fields this card edits (the namespace's full schema). */
|
|
19
18
|
export interface ImageGenSettings {
|
|
20
19
|
enabled?: boolean
|
|
21
20
|
announceToAgent?: boolean
|
|
21
|
+
allowAgentImageGeneration?: boolean
|
|
22
22
|
apiUrl?: string
|
|
23
23
|
apiKey?: string
|
|
24
|
+
imageModels?: string[]
|
|
24
25
|
promptApiUrl?: string
|
|
25
26
|
promptApiKey?: string
|
|
26
27
|
promptModel?: string
|
|
@@ -32,10 +33,12 @@ export interface ImageGenSettingsCardState extends CardShell {
|
|
|
32
33
|
enabled: CardFieldState
|
|
33
34
|
/** System-prompt announcement flag. */
|
|
34
35
|
announceToAgent: CardFieldState
|
|
36
|
+
allowAgentImageGeneration: CardFieldState
|
|
35
37
|
/** API base URL. */
|
|
36
38
|
apiUrl: CardFieldState
|
|
37
39
|
/** API key draft (the stored value is never rendered). */
|
|
38
40
|
apiKey: CardFieldState
|
|
41
|
+
imageModels: CardFieldState
|
|
39
42
|
promptApiUrl: CardFieldState
|
|
40
43
|
promptApiKey: CardFieldState
|
|
41
44
|
promptModel: CardFieldState
|
|
@@ -60,8 +63,10 @@ export class ImageGenSettingsCardController {
|
|
|
60
63
|
this.form = new CardForm(scope, [
|
|
61
64
|
booleanField('enabled'),
|
|
62
65
|
booleanField('announceToAgent'),
|
|
66
|
+
booleanField('allowAgentImageGeneration'),
|
|
63
67
|
textField('apiUrl'),
|
|
64
68
|
secretField('apiKey'),
|
|
69
|
+
stringListField('imageModels'),
|
|
65
70
|
textField('promptApiUrl'),
|
|
66
71
|
secretField('promptApiKey'),
|
|
67
72
|
textField('promptModel'),
|
|
@@ -77,8 +82,10 @@ export class ImageGenSettingsCardController {
|
|
|
77
82
|
...this.form.shell(),
|
|
78
83
|
enabled: this.form.field('enabled'),
|
|
79
84
|
announceToAgent: this.form.field('announceToAgent'),
|
|
85
|
+
allowAgentImageGeneration: this.form.field('allowAgentImageGeneration'),
|
|
80
86
|
apiUrl: this.form.field('apiUrl'),
|
|
81
87
|
apiKey: this.form.field('apiKey'),
|
|
88
|
+
imageModels: this.form.field('imageModels'),
|
|
82
89
|
promptApiUrl: this.form.field('promptApiUrl'),
|
|
83
90
|
promptApiKey: this.form.field('promptApiKey'),
|
|
84
91
|
promptModel: this.form.field('promptModel'),
|
|
@@ -119,9 +126,19 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
119
126
|
const state = props.useImageGenSettingsCard(snapshot => snapshot)
|
|
120
127
|
const keySet = props.useImageGenKeySet(snapshot => snapshot)
|
|
121
128
|
const [open, setOpen] = useState(false)
|
|
122
|
-
const [
|
|
123
|
-
const [
|
|
124
|
-
const [
|
|
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)
|
|
125
142
|
if (!state.available) return null
|
|
126
143
|
const title = t('settings.title')
|
|
127
144
|
const blocked = !state.dirty || state.invalid || state.saving
|
|
@@ -178,10 +195,16 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
178
195
|
? (
|
|
179
196
|
<div className={css.body}>
|
|
180
197
|
{!state.writable ? <p className={css.readOnly} role="status">{t('settings.readOnly')}</p> : null}
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
/>
|
|
185
208
|
<ValueField
|
|
186
209
|
id="dsh-imagegen-settings-apikey"
|
|
187
210
|
label={t('settings.apiKey')}
|
|
@@ -197,19 +220,187 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
197
220
|
onClear={() => { props.resetField('apiKey') }}
|
|
198
221
|
canClear={keySet}
|
|
199
222
|
/>
|
|
200
|
-
<ValueField
|
|
201
|
-
id="dsh-imagegen-settings-apiurl"
|
|
202
|
-
label={t('settings.apiUrl')}
|
|
203
|
-
hint={t('settings.apiUrlHint')}
|
|
204
|
-
placeholder="https://api.openai.com/v1"
|
|
205
|
-
{...fieldProps}
|
|
206
|
-
{...state.apiUrl}
|
|
207
|
-
onEdit={(text) => { props.edit('apiUrl', text) }}
|
|
208
|
-
onReset={() => { props.resetField('apiUrl') }}
|
|
209
|
-
/>
|
|
210
223
|
<div className={css.sectionDivider} />
|
|
211
|
-
<
|
|
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')}>
|
|
212
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}>
|
|
213
404
|
<ValueField
|
|
214
405
|
id="dsh-imagegen-settings-prompt-apiurl"
|
|
215
406
|
label={t('settings.promptApiUrl')}
|
|
@@ -232,44 +423,13 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
232
423
|
onEdit={(text) => { props.edit('promptApiKey', text) }}
|
|
233
424
|
onReset={() => { props.resetField('promptApiKey') }}
|
|
234
425
|
/>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
onEdit={(text) => { props.edit('promptModel', text) }}
|
|
243
|
-
onReset={() => { props.resetField('promptModel') }}
|
|
244
|
-
/>
|
|
245
|
-
<div className={css.modelFetchRow}>
|
|
246
|
-
<button
|
|
247
|
-
type="button"
|
|
248
|
-
className={css.modelFetch}
|
|
249
|
-
disabled={disabled || loadingModels}
|
|
250
|
-
onClick={() => {
|
|
251
|
-
setLoadingModels(true)
|
|
252
|
-
setModelsError(null)
|
|
253
|
-
void fetch(PROMPT_ENHANCE_API.models, { method: 'POST' })
|
|
254
|
-
.then(async response => {
|
|
255
|
-
const body = await response.json() as { ok?: boolean; models?: string[]; message?: string }
|
|
256
|
-
if (!response.ok || body.ok !== true) throw new Error(body.message ?? `HTTP ${response.status}`)
|
|
257
|
-
setModels(body.models ?? [])
|
|
258
|
-
})
|
|
259
|
-
.catch(error => { setModelsError(error instanceof Error ? error.message : String(error)) })
|
|
260
|
-
.finally(() => { setLoadingModels(false) })
|
|
261
|
-
}}
|
|
262
|
-
>
|
|
263
|
-
{loadingModels ? t('settings.promptModelsLoading') : t('settings.promptModelsFetch')}
|
|
264
|
-
</button>
|
|
265
|
-
{models.length > 0 ? (
|
|
266
|
-
<select className={css.modelChoices} value="" onChange={event => { if (event.target.value !== '') props.edit('promptModel', event.target.value) }}>
|
|
267
|
-
<option value="">{t('settings.promptModelsSelect')}</option>
|
|
268
|
-
{models.map(model => <option key={model} value={model}>{model}</option>)}
|
|
269
|
-
</select>
|
|
270
|
-
) : null}
|
|
271
|
-
</div>
|
|
272
|
-
{modelsError !== null ? <p className={css.failed} role="status">{modelsError}</p> : null}
|
|
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}>
|
|
273
433
|
<BooleanField
|
|
274
434
|
id="dsh-imagegen-settings-enabled"
|
|
275
435
|
label={t('settings.enabled')}
|
|
@@ -294,6 +454,19 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
294
454
|
onEdit={(text) => { props.edit('announceToAgent', text) }}
|
|
295
455
|
onReset={() => { props.resetField('announceToAgent') }}
|
|
296
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}
|
|
297
470
|
<div className={css.footer}>
|
|
298
471
|
{state.failed ? <p className={css.failed} role="status">{t('settings.saveFailed')}</p> : null}
|
|
299
472
|
<button
|
|
@@ -360,6 +533,8 @@ function ValueField(props: FieldProps & {
|
|
|
360
533
|
onClear?: () => void
|
|
361
534
|
/** Whether a stored secret exists (enables the clear control). */
|
|
362
535
|
canClear?: boolean
|
|
536
|
+
/** Render a multiline input for newline-separated configuration values. */
|
|
537
|
+
multiline?: boolean
|
|
363
538
|
}) {
|
|
364
539
|
return (
|
|
365
540
|
<div className={css.field}>
|
|
@@ -393,17 +568,29 @@ function ValueField(props: FieldProps & {
|
|
|
393
568
|
)
|
|
394
569
|
: null}
|
|
395
570
|
</div>
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
+
)}
|
|
407
594
|
<p className={props.invalid ? css.invalid : css.hint}>
|
|
408
595
|
{props.invalid ? props.invalidLabel : props.hint}
|
|
409
596
|
</p>
|
|
@@ -411,6 +598,10 @@ function ValueField(props: FieldProps & {
|
|
|
411
598
|
)
|
|
412
599
|
}
|
|
413
600
|
|
|
601
|
+
function splitModels(value: string): string[] {
|
|
602
|
+
return [...new Set(value.split(/[\n,]/).map(model => model.trim()).filter(Boolean))]
|
|
603
|
+
}
|
|
604
|
+
|
|
414
605
|
/** A staged boolean field: 继承 / 开 / 关. */
|
|
415
606
|
function BooleanField(props: FieldProps & {
|
|
416
607
|
/** Copy for the inherit option. */
|
package/src/client/locales.ts
CHANGED
|
@@ -171,6 +171,20 @@ export const zh = {
|
|
|
171
171
|
'settings.apiKeyHint': 'Bearer 密钥,明文存于本机设置文档;界面只显示是否已设置',
|
|
172
172
|
'settings.apiKeySet': '已保存密钥;输入新值可更换,点击「清除」可删除',
|
|
173
173
|
'settings.apiKeyClear': '清除',
|
|
174
|
+
'settings.imageModelsTitle': '生图模型',
|
|
175
|
+
'settings.imageModelsHint': '保存 API 地址和密钥后检测候选模型;请只选择实际支持生图的项。',
|
|
176
|
+
'settings.imageModels': '允许使用的生图模型',
|
|
177
|
+
'settings.imageModelsManualHint': '一行一个模型;API 未提供 /models 时可手动填写。面板和 Agent 只能使用此列表。',
|
|
178
|
+
'settings.imageModelsFetch': '检测可用模型',
|
|
179
|
+
'settings.imageModelsLoading': '检测中…',
|
|
180
|
+
'settings.imageModelsCandidates': '检测到的候选模型(勾选后保存)',
|
|
181
|
+
'settings.addModel': '+ 手动添加',
|
|
182
|
+
'settings.cancelAddModel': '收起添加',
|
|
183
|
+
'settings.addModelPlaceholder': '输入模型名称,例如 qwen-image',
|
|
184
|
+
'settings.addModelConfirm': '添加',
|
|
185
|
+
'settings.removeModel': '移除模型',
|
|
186
|
+
'settings.optional': '可选',
|
|
187
|
+
'settings.moreOptions': '更多设置',
|
|
174
188
|
'settings.promptEnhanceTitle': '提示词增强模型',
|
|
175
189
|
'settings.promptEnhanceHint': '用于将简短描述扩写为更完整的生图提示词;留空的地址和密钥会复用生图配置。',
|
|
176
190
|
'settings.promptApiUrl': '对话 API 地址(可选)',
|
|
@@ -179,11 +193,17 @@ export const zh = {
|
|
|
179
193
|
'settings.promptApiKeyHint': '留空则复用图像 API 密钥。',
|
|
180
194
|
'settings.promptModel': '对话模型',
|
|
181
195
|
'settings.promptModelHint': '选择或填写支持 /chat/completions 的模型。',
|
|
196
|
+
'settings.promptModelDetectionHint': '默认复用生图 API;检测后点选一个模型即可。',
|
|
182
197
|
'settings.promptModelsFetch': '获取可用模型',
|
|
183
198
|
'settings.promptModelsLoading': '正在获取…',
|
|
184
199
|
'settings.promptModelsSelect': '选择一个模型',
|
|
200
|
+
'settings.promptModelsCandidates': '检测到的候选对话模型',
|
|
201
|
+
'settings.addPromptModelPlaceholder': '输入对话模型名称,例如 gpt-4.1-mini',
|
|
202
|
+
'settings.promptApiAdvanced': '使用独立对话 API(可选)',
|
|
185
203
|
'settings.announceToAgent': '向 Agent 播报本插件',
|
|
186
204
|
'settings.announceToAgentHint': '开启后,本插件的存在与能力会写入每个 Agent 的系统提示词',
|
|
205
|
+
'settings.allowAgentImageGeneration': '允许 Agent 调用生图',
|
|
206
|
+
'settings.allowAgentImageGenerationHint': '默认开启。关闭后,Agent 无法提交、查询或取消生图任务;侧边栏工作台不受影响。',
|
|
187
207
|
'settings.enabled': '启用插件',
|
|
188
208
|
'settings.enabledHint': '关闭后生图面板不可用(设置卡片始终可用)',
|
|
189
209
|
'settings.save': '保存',
|
|
@@ -388,6 +408,20 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
388
408
|
'settings.apiKeyHint': 'Bearer key, stored in plaintext in the local settings document; the UI only shows whether it is set',
|
|
389
409
|
'settings.apiKeySet': 'A key is stored; type a new value to replace it, or click "Clear" to remove it',
|
|
390
410
|
'settings.apiKeyClear': 'Clear',
|
|
411
|
+
'settings.imageModelsTitle': 'Image generation models',
|
|
412
|
+
'settings.imageModelsHint': 'Save the API URL and key, then detect candidate models. Select only models that actually support image generation.',
|
|
413
|
+
'settings.imageModels': 'Allowed image models',
|
|
414
|
+
'settings.imageModelsManualHint': 'One model per line. Add models manually when the API does not provide /models. The panel and Agent can use only this list.',
|
|
415
|
+
'settings.imageModelsFetch': 'Detect available models',
|
|
416
|
+
'settings.imageModelsLoading': 'Detecting…',
|
|
417
|
+
'settings.imageModelsCandidates': 'Detected candidate models (select, then save)',
|
|
418
|
+
'settings.addModel': '+ Add manually',
|
|
419
|
+
'settings.cancelAddModel': 'Hide add',
|
|
420
|
+
'settings.addModelPlaceholder': 'Enter a model name, e.g. qwen-image',
|
|
421
|
+
'settings.addModelConfirm': 'Add',
|
|
422
|
+
'settings.removeModel': 'Remove model',
|
|
423
|
+
'settings.optional': 'Optional',
|
|
424
|
+
'settings.moreOptions': 'More settings',
|
|
391
425
|
'settings.promptEnhanceTitle': 'Prompt enhancement model',
|
|
392
426
|
'settings.promptEnhanceHint': 'Expands short requests into complete image prompts. Blank URL and key reuse the image API configuration.',
|
|
393
427
|
'settings.promptApiUrl': 'Chat API URL (optional)',
|
|
@@ -396,11 +430,17 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
396
430
|
'settings.promptApiKeyHint': 'Leave blank to reuse the image API key.',
|
|
397
431
|
'settings.promptModel': 'Chat model',
|
|
398
432
|
'settings.promptModelHint': 'Select or enter a model supporting /chat/completions.',
|
|
433
|
+
'settings.promptModelDetectionHint': 'Uses the image API by default. Detect and select one model.',
|
|
399
434
|
'settings.promptModelsFetch': 'Fetch available models',
|
|
400
435
|
'settings.promptModelsLoading': 'Fetching…',
|
|
401
436
|
'settings.promptModelsSelect': 'Select a model',
|
|
437
|
+
'settings.promptModelsCandidates': 'Detected chat model candidates',
|
|
438
|
+
'settings.addPromptModelPlaceholder': 'Enter a chat model name, e.g. gpt-4.1-mini',
|
|
439
|
+
'settings.promptApiAdvanced': 'Use a separate chat API (optional)',
|
|
402
440
|
'settings.announceToAgent': 'Announce this plugin to agents',
|
|
403
441
|
'settings.announceToAgentHint': 'When on, the plugin presence and capabilities are written into every agent system prompt',
|
|
442
|
+
'settings.allowAgentImageGeneration': 'Allow agents to generate images',
|
|
443
|
+
'settings.allowAgentImageGenerationHint': 'On by default. When off, agents cannot submit, query, or cancel image tasks; the sidebar studio remains available.',
|
|
404
444
|
'settings.enabled': 'Enable plugin',
|
|
405
445
|
'settings.enabledHint': 'When off, the generation studio is unavailable (this card stays available)',
|
|
406
446
|
'settings.save': 'Save',
|