@dickpy/dsh-imagegen 1.5.2 → 1.5.4

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.
@@ -21,6 +21,8 @@ import type { ImageGenScope } from './settings-scope.ts'
21
21
  import { describeModel } from '../model-catalog.ts'
22
22
  import { IMAGE_MODEL_API, PRESETS_API, PROMPT_ENHANCE_API, USAGE_API, type ModelMapping, type PresetProviderView } from '../protocol.ts'
23
23
  import type { ImageGenKey } from './locales.ts'
24
+ import { tt, type TranslateValues } from './helpers.ts'
25
+ import { useImageGenLanguageTick } from './use-language.ts'
24
26
  import css from './settings-card.module.css'
25
27
 
26
28
  /** The global (non-channel) fields this card's staged form edits. */
@@ -31,6 +33,14 @@ export interface ImageGenSettings {
31
33
  promptApiUrl?: string
32
34
  promptApiKey?: string
33
35
  promptModel?: string
36
+ storageEnabled?: boolean
37
+ storageEndpoint?: string
38
+ storageRegion?: string
39
+ storagePrefix?: string
40
+ storageAccessKey?: string
41
+ storageSecretKey?: string
42
+ storageSyncGallery?: boolean
43
+ storageSyncHistory?: boolean
34
44
  }
35
45
 
36
46
  /** What the card renders. */
@@ -45,12 +55,29 @@ export interface ImageGenSettingsCardState extends CardShell {
45
55
  promptApiUrl: CardFieldState
46
56
  promptApiKey: CardFieldState
47
57
  promptModel: CardFieldState
58
+ storageEnabled: CardFieldState
59
+ storageEndpoint: CardFieldState
60
+ storageRegion: CardFieldState
61
+ storagePrefix: CardFieldState
62
+ storageAccessKey: CardFieldState
63
+ storageSecretKey: CardFieldState
64
+ storageSyncGallery: CardFieldState
65
+ storageSyncHistory: CardFieldState
66
+ }
67
+
68
+ /** Result of probing the configured object storage from the card. */
69
+ export interface StorageTestOutcome {
70
+ ok: boolean
71
+ ms?: number
72
+ message?: string
48
73
  }
49
74
 
50
75
  /** The registration-side face the card's slot entry injects. */
51
76
  export interface ImageGenSettingsCardFace extends CardActions {
52
77
  /** Channel staging actions (committed together with the card's save). */
53
78
  channels: ChannelsFormActions
79
+ /** Save staged edits, then upload a probe object to the configured store. */
80
+ storageTest: () => Promise<StorageTestOutcome>
54
81
  hooks: {
55
82
  /** Card snapshot bound by the renderer as useImageGenSettingsCard. */
56
83
  imageGenSettingsCard: SnapshotStore<ImageGenSettingsCardState>
@@ -71,8 +98,16 @@ export class ImageGenSettingsCardController {
71
98
  textField('promptApiUrl'),
72
99
  secretField('promptApiKey'),
73
100
  textField('promptModel'),
101
+ booleanField('storageEnabled'),
102
+ textField('storageEndpoint'),
103
+ textField('storageRegion'),
104
+ textField('storagePrefix'),
105
+ textField('storageAccessKey'),
106
+ secretField('storageSecretKey'),
107
+ booleanField('storageSyncGallery'),
108
+ booleanField('storageSyncHistory'),
74
109
  ], {
75
- secretSettled: () => this.scope.getSecretSetSnapshot('promptApiKey'),
110
+ secretSettled: (field) => this.scope.getSecretSetSnapshot(field),
76
111
  })
77
112
  this.channelsForm = new ChannelsForm(scope)
78
113
  }
@@ -89,6 +124,14 @@ export class ImageGenSettingsCardController {
89
124
  promptApiUrl: this.form.field('promptApiUrl'),
90
125
  promptApiKey: this.form.field('promptApiKey'),
91
126
  promptModel: this.form.field('promptModel'),
127
+ storageEnabled: this.form.field('storageEnabled'),
128
+ storageEndpoint: this.form.field('storageEndpoint'),
129
+ storageRegion: this.form.field('storageRegion'),
130
+ storagePrefix: this.form.field('storagePrefix'),
131
+ storageAccessKey: this.form.field('storageAccessKey'),
132
+ storageSecretKey: this.form.field('storageSecretKey'),
133
+ storageSyncGallery: this.form.field('storageSyncGallery'),
134
+ storageSyncHistory: this.form.field('storageSyncHistory'),
92
135
  }
93
136
  }
94
137
 
@@ -104,6 +147,19 @@ export class ImageGenSettingsCardController {
104
147
  imageGenSettingsCard: cardStore,
105
148
  },
106
149
  channels: this.channelsForm.actions(),
150
+ // The probe needs the values the user is looking at, so staged edits are
151
+ // committed first; the host route then resolves the saved config itself.
152
+ storageTest: async (): Promise<StorageTestOutcome> => {
153
+ await this.form.save()
154
+ try {
155
+ const response = await fetch('/api/dsh-imagegen/storage/test', { method: 'POST' })
156
+ const body = await response.json() as { ok?: unknown; ms?: unknown; message?: unknown }
157
+ if (body.ok === true) return { ok: true, ms: typeof body.ms === 'number' ? body.ms : undefined }
158
+ return { ok: false, message: typeof body.message === 'string' ? body.message : `HTTP ${response.status}` }
159
+ } catch (error) {
160
+ return { ok: false, message: error instanceof Error ? error.message : String(error) }
161
+ }
162
+ },
107
163
  ...this.form.actions(),
108
164
  }
109
165
  }
@@ -127,7 +183,11 @@ interface UsageCounters {
127
183
  * @returns the card, or nothing while the namespace is still loading.
128
184
  */
129
185
  export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
130
- const { t } = props
186
+ // The card renders through the plugin's own dictionary so the uiLanguage
187
+ // override applies here too — the host-locale props.t would only follow the
188
+ // DSH interface language.
189
+ const t = tt
190
+ useImageGenLanguageTick()
131
191
  const state = props.useImageGenSettingsCard(snapshot => snapshot)
132
192
  const [open, setOpen] = useState(false)
133
193
  // Global-section local states (prompt enhancement etc.).
@@ -138,6 +198,9 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
138
198
  const [manualPromptModel, setManualPromptModel] = useState('')
139
199
  const [enhancementOpen, setEnhancementOpen] = useState(false)
140
200
  const [promptApiOpen, setPromptApiOpen] = useState(false)
201
+ const [storageOpen, setStorageOpen] = useState(false)
202
+ const [storageTesting, setStorageTesting] = useState(false)
203
+ const [storageTestResult, setStorageTestResult] = useState<string | null>(null)
141
204
  const [moreOpen, setMoreOpen] = useState(false)
142
205
  // Channel list local states.
143
206
  const [editingId, setEditingId] = useState<string | null>(null)
@@ -407,6 +470,121 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
407
470
  </div> : null}
408
471
  </section> : null}
409
472
 
473
+ <button type="button" className={css.disclosure} aria-expanded={storageOpen} onClick={() => { setStorageOpen(open => !open) }}>
474
+ <span>{t('settings.storageTitle')}</span>
475
+ <span aria-hidden="true">{storageOpen ? '⌃' : '⌄'}</span>
476
+ </button>
477
+ {storageOpen ? <div className={css.optionalContent}>
478
+ <BooleanField
479
+ id="dsh-imagegen-settings-storage-enabled"
480
+ label={t('settings.storageEnabled')}
481
+ hint={t('settings.storageHint')}
482
+ inheritLabel={t('settings.inherit')}
483
+ onLabel={t('settings.on')}
484
+ offLabel={t('settings.off')}
485
+ {...fieldProps}
486
+ {...state.storageEnabled}
487
+ onEdit={(text) => { props.edit('storageEnabled', text) }}
488
+ onReset={() => { props.resetField('storageEnabled') }}
489
+ />
490
+ <ValueField
491
+ id="dsh-imagegen-settings-storage-endpoint"
492
+ label={t('settings.storageEndpoint')}
493
+ hint={t('settings.storageEndpointHint')}
494
+ placeholder="https://bucket-appid.cos.ap-guangzhou.myqcloud.com"
495
+ {...fieldProps}
496
+ {...state.storageEndpoint}
497
+ onEdit={(text) => { props.edit('storageEndpoint', text) }}
498
+ onReset={() => { props.resetField('storageEndpoint') }}
499
+ />
500
+ <ValueField
501
+ id="dsh-imagegen-settings-storage-region"
502
+ label={t('settings.storageRegion')}
503
+ hint={t('settings.storageRegionHint')}
504
+ placeholder="ap-guangzhou"
505
+ {...fieldProps}
506
+ {...state.storageRegion}
507
+ onEdit={(text) => { props.edit('storageRegion', text) }}
508
+ onReset={() => { props.resetField('storageRegion') }}
509
+ />
510
+ <ValueField
511
+ id="dsh-imagegen-settings-storage-prefix"
512
+ label={t('settings.storagePrefix')}
513
+ hint={t('settings.storagePrefixHint')}
514
+ placeholder="dsh-imagegen"
515
+ {...fieldProps}
516
+ {...state.storagePrefix}
517
+ onEdit={(text) => { props.edit('storagePrefix', text) }}
518
+ onReset={() => { props.resetField('storagePrefix') }}
519
+ />
520
+ <ValueField
521
+ id="dsh-imagegen-settings-storage-accesskey"
522
+ label={t('settings.storageAccessKey')}
523
+ hint={t('settings.storageAccessKeyHint')}
524
+ placeholder="AKID…"
525
+ {...fieldProps}
526
+ {...state.storageAccessKey}
527
+ onEdit={(text) => { props.edit('storageAccessKey', text) }}
528
+ onReset={() => { props.resetField('storageAccessKey') }}
529
+ />
530
+ <ValueField
531
+ id="dsh-imagegen-settings-storage-secretkey"
532
+ label={t('settings.storageSecretKey')}
533
+ hint={t('settings.storageSecretKeyHint')}
534
+ placeholder="…"
535
+ secret
536
+ {...fieldProps}
537
+ {...state.storageSecretKey}
538
+ overridden={false}
539
+ onEdit={(text) => { props.edit('storageSecretKey', text) }}
540
+ onReset={() => { props.resetField('storageSecretKey') }}
541
+ />
542
+ <BooleanField
543
+ id="dsh-imagegen-settings-storage-gallery"
544
+ label={t('settings.storageSyncGallery')}
545
+ hint={t('settings.storageSyncGalleryHint')}
546
+ inheritLabel={t('settings.inherit')}
547
+ onLabel={t('settings.on')}
548
+ offLabel={t('settings.off')}
549
+ {...fieldProps}
550
+ {...state.storageSyncGallery}
551
+ onEdit={(text) => { props.edit('storageSyncGallery', text) }}
552
+ onReset={() => { props.resetField('storageSyncGallery') }}
553
+ />
554
+ <BooleanField
555
+ id="dsh-imagegen-settings-storage-history"
556
+ label={t('settings.storageSyncHistory')}
557
+ hint={t('settings.storageSyncHistoryHint')}
558
+ inheritLabel={t('settings.inherit')}
559
+ onLabel={t('settings.on')}
560
+ offLabel={t('settings.off')}
561
+ {...fieldProps}
562
+ {...state.storageSyncHistory}
563
+ onEdit={(text) => { props.edit('storageSyncHistory', text) }}
564
+ onReset={() => { props.resetField('storageSyncHistory') }}
565
+ />
566
+ <div className={css.modelSummary}>
567
+ <button
568
+ type="button"
569
+ className={css.addModel}
570
+ disabled={disabled || storageTesting}
571
+ onClick={() => {
572
+ setStorageTesting(true)
573
+ setStorageTestResult(null)
574
+ void props.storageTest().then(outcome => {
575
+ setStorageTestResult(outcome.ok
576
+ ? t('settings.storageTestOk', { ms: outcome.ms ?? 0 })
577
+ : t('settings.storageTestFailed', { error: outcome.message ?? 'error' }))
578
+ }).finally(() => { setStorageTesting(false) })
579
+ }}
580
+ >
581
+ {storageTesting ? t('settings.storageTesting') : t('settings.storageTest')}
582
+ </button>
583
+ {storageTestResult !== null ? <p className={css.failed} role="status">{storageTestResult}</p> : null}
584
+ </div>
585
+ <p className={css.hint}>{t('settings.storageKeyHint')}</p>
586
+ </div> : null}
587
+
410
588
  <button type="button" className={css.disclosure} aria-expanded={moreOpen} onClick={() => { setMoreOpen(open => !open) }}>
411
589
  <span>{t('settings.moreOptions')}</span>
412
590
  <span aria-hidden="true">{moreOpen ? '⌃' : '⌄'}</span>
@@ -445,8 +623,8 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
445
623
  offLabel={t('settings.off')}
446
624
  {...fieldProps}
447
625
  {...state.allowAgentImageGeneration}
448
- onEdit={(text) => { props.edit('allowAgentImageGeneration', text) }}
449
- onReset={() => { props.resetField('allowAgentImageGeneration') }}
626
+ onEdit={(text) => { props.edit('enabled', text) }}
627
+ onReset={() => { props.resetField('enabled') }}
450
628
  />
451
629
  </div> : null}
452
630
  <div className={css.footer}>
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 { CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATE_FAVORITES_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateCase, type TemplateFavorite, type TemplateListResult, type TemplateRefreshResult, type TemplateSample, type UpdateInfo } from '../protocol.ts'
6
+ import { CANVAS_API, CONVERSATION_IMAGE_API, DATA_FOLDER_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, STORAGE_API, TASK_API, TEMPLATE_FAVORITES_API, TEMPLATES_API, UPDATE_API, type CanvasAssetRef, type CanvasDocument, type CanvasSummary, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateCase, type TemplateFavorite, type TemplateListResult, type TemplateRefreshResult, type TemplateSample, type UpdateInfo } from '../protocol.ts'
7
7
 
8
8
  /** Error carrying the route's JSON error message. */
9
9
  export class ImageGenApiError extends Error {
@@ -181,6 +181,41 @@ export class ImageGenApi {
181
181
  return (await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)).entries
182
182
  }
183
183
 
184
+ async canvasList(): Promise<CanvasSummary[]> {
185
+ const response = await fetch(CANVAS_API.list, { method: 'POST' })
186
+ return (await readEnvelope<{ ok: true; projects: CanvasSummary[] }>(response)).projects
187
+ }
188
+
189
+ async canvasCreate(title?: string): Promise<CanvasDocument> {
190
+ const response = await fetch(CANVAS_API.create, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title }) })
191
+ return (await readEnvelope<{ ok: true; document: CanvasDocument }>(response)).document
192
+ }
193
+
194
+ async canvasRead(id: string): Promise<CanvasDocument> {
195
+ const response = await fetch(CANVAS_API.read, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) })
196
+ return (await readEnvelope<{ ok: true; document: CanvasDocument }>(response)).document
197
+ }
198
+
199
+ async canvasSave(document: CanvasDocument, expectedRevision: number): Promise<CanvasDocument> {
200
+ const response = await fetch(CANVAS_API.save, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ document, expectedRevision }) })
201
+ return (await readEnvelope<{ ok: true; document: CanvasDocument }>(response)).document
202
+ }
203
+
204
+ async canvasRemove(id: string): Promise<CanvasSummary[]> {
205
+ const response = await fetch(CANVAS_API.remove, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) })
206
+ return (await readEnvelope<{ ok: true; projects: CanvasSummary[] }>(response)).projects
207
+ }
208
+
209
+ async canvasUpload(dataUrl: string, width: number, height: number, meta: { origin?: string; originId?: string; entryId?: string; imageIndex?: number } = {}): Promise<CanvasAssetRef> {
210
+ const response = await fetch(CANVAS_API.assetUpload, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ dataUrl, width, height, ...meta }) })
211
+ return (await readEnvelope<{ ok: true; asset: CanvasAssetRef }>(response)).asset
212
+ }
213
+
214
+ async canvasImport(source: 'history' | 'gallery', entryId: string, imageIndex: number, width: number, height: number): Promise<CanvasAssetRef> {
215
+ const response = await fetch(CANVAS_API.assetImport, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source, entryId, imageIndex, width, height }) })
216
+ return (await readEnvelope<{ ok: true; asset: CanvasAssetRef }>(response)).asset
217
+ }
218
+
184
219
  /** Fetch one template source's list (bundled snapshot or refreshed copy). */
185
220
  async templatesList(sourceId: string): Promise<TemplateListResult> {
186
221
  const response = await fetch(TEMPLATES_API.list, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source: sourceId }) })
@@ -214,6 +249,18 @@ export class ImageGenApi {
214
249
  return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
215
250
  }
216
251
 
252
+ /** Reveal the host data directory (saved images) in the OS file manager. */
253
+ async openDataFolder(): Promise<{ ok: boolean; path?: string; message?: string }> {
254
+ const response = await fetch(DATA_FOLDER_API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({}) })
255
+ return await readEnvelope<{ ok: true; path: string }>(response)
256
+ }
257
+
258
+ /** Probe the configured S3-compatible object storage with a small upload. */
259
+ async storageTest(): Promise<{ ok: boolean; ms?: number; key?: string; message?: string }> {
260
+ const response = await fetch(STORAGE_API.test, { method: 'POST' })
261
+ return await readEnvelope<{ ok: true; ms: number; key: string }>(response)
262
+ }
263
+
217
264
  /** Star one template (the host keeps a full case snapshot). */
218
265
  async favoritesAdd(sourceId: string, item: TemplateCase): Promise<TemplateFavorite[]> {
219
266
  const response = await fetch(TEMPLATE_FAVORITES_API.add, {