@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dickpy/dsh-imagegen",
3
3
  "description": "AI 鐢熷浘 (image generation) plugin for the dsh web GUI: text-to-image and image-to-image through a configurable OpenAI-compatible endpoint (gpt-image-2 / grok-imagine-image / dall-e-3, with native xAI Grok Imagine request shaping), with a settings card for api_url / api_key and a sidebar entry opening a split-pane generation studio.",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@deepseek-ai/cordis": "^4.0.1",
30
+ "@deepseek-ai/dsh-attachment": "0.1.0-rc.7",
30
31
  "@deepseek-ai/dsh-client-connection": "0.1.0-rc.7",
31
32
  "@deepseek-ai/dsh-client-locale": "0.1.0-rc.7",
32
33
  "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.7",
@@ -34,8 +35,10 @@
34
35
  "@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.7",
35
36
  "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.7",
36
37
  "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.7",
38
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.7",
37
39
  "@deepseek-ai/dsh-settings": "0.1.0-rc.7",
38
40
  "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.7",
41
+ "@deepseek-ai/dsh-tools": "0.1.0-rc.7",
39
42
  "@types/node": "^22.20.0",
40
43
  "@types/react": "~18.3.1",
41
44
  "@types/react-dom": "^18.3.5",
@@ -0,0 +1,289 @@
1
+ /** Agent-facing image-generation tools backed by the shared host queue. */
2
+
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import { createUserMessage } from '@deepseek-ai/dsh-llm/message'
5
+ import { defineTool } from '@deepseek-ai/dsh-tools'
6
+ import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
7
+ import type {} from '@deepseek-ai/dsh-attachment'
8
+ import type {} from '@deepseek-ai/dsh-tools'
9
+ import { ImageGenError } from './engine.ts'
10
+ import { ImageGenerationRuntime } from './generation-runtime.ts'
11
+ import { normalizeImageModels } from './image-models.ts'
12
+ import type { GenerationTask, GeneratedImage } from './protocol.ts'
13
+
14
+ export interface AgentImageToolConfig {
15
+ enabled: boolean
16
+ allowAgentImageGeneration: boolean
17
+ apiUrl: string
18
+ apiKey: string
19
+ imageModels: string[]
20
+ }
21
+
22
+ interface AgentImageRef {
23
+ attachment_id: string
24
+ media_type: string
25
+ bytes: number
26
+ width: number
27
+ height: number
28
+ name?: string
29
+ }
30
+
31
+ interface AgentTaskResult {
32
+ task_id: string
33
+ status: string
34
+ message: string
35
+ error?: string
36
+ images: AgentImageRef[]
37
+ }
38
+
39
+ const imageRefSchema = {
40
+ type: 'object',
41
+ additionalProperties: false,
42
+ properties: {
43
+ attachment_id: { type: 'string', required: true },
44
+ media_type: { type: 'string', required: true },
45
+ bytes: { type: 'integer', required: true },
46
+ width: { type: 'integer', required: true },
47
+ height: { type: 'integer', required: true },
48
+ name: { type: 'string' },
49
+ },
50
+ } as const
51
+
52
+ const taskResultSchema = {
53
+ type: 'object',
54
+ additionalProperties: false,
55
+ properties: {
56
+ task_id: { type: 'string', required: true },
57
+ status: { type: 'string', required: true },
58
+ message: { type: 'string', required: true },
59
+ error: { type: 'string' },
60
+ images: { type: 'array', required: true, items: imageRefSchema },
61
+ },
62
+ } as const
63
+
64
+ function acceptedMediaType(value: string): value is ImageMediaType {
65
+ return value === 'image/png' || value === 'image/jpeg' || value === 'image/webp' || value === 'image/gif'
66
+ }
67
+
68
+ function projectRef(ref: ImageAttachmentRef): AgentImageRef {
69
+ return {
70
+ attachment_id: String(ref.attachmentId),
71
+ media_type: ref.mediaType,
72
+ bytes: ref.bytes,
73
+ width: ref.width,
74
+ height: ref.height,
75
+ ...ref.name === undefined ? {} : { name: ref.name },
76
+ }
77
+ }
78
+
79
+ function restoreRef(value: AgentImageRef): ImageAttachmentRef {
80
+ if (!acceptedMediaType(value.media_type)) throw new ImageGenError('source_image.media_type is not a supported image type', 'bad-reference-image')
81
+ if (!Number.isInteger(value.bytes) || value.bytes < 1 || !Number.isInteger(value.width) || value.width < 1 || !Number.isInteger(value.height) || value.height < 1) {
82
+ throw new ImageGenError('source_image metadata is invalid', 'bad-reference-image')
83
+ }
84
+ return {
85
+ attachmentId: value.attachment_id as ImageAttachmentRef['attachmentId'],
86
+ mediaType: value.media_type,
87
+ bytes: value.bytes,
88
+ width: value.width,
89
+ height: value.height,
90
+ ...value.name === undefined ? {} : { name: value.name },
91
+ }
92
+ }
93
+
94
+ function imageDataUrl(image: { data: Uint8Array; ref: ImageAttachmentRef }): string {
95
+ return `data:${image.ref.mediaType};base64,${Buffer.from(image.data).toString('base64')}`
96
+ }
97
+
98
+ function renderTaskResult(value: AgentTaskResult): Array<{ type: 'text'; text: string } | { type: 'image'; attachment: ImageAttachmentRef }> {
99
+ const text = JSON.stringify(value)
100
+ return [
101
+ { type: 'text', text },
102
+ ...value.images.map(image => ({ type: 'image' as const, attachment: restoreRef(image) })),
103
+ ]
104
+ }
105
+
106
+ /** Register the global Agent tools and unregister them with the plugin lifecycle. */
107
+ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRuntime, resolve: () => AgentImageToolConfig): () => void {
108
+ const attachmentRefs = new Map<string, Promise<AgentImageRef[]>>()
109
+ const taskSubscriptions = new Set<() => void>()
110
+ const ensureConfigured = (): void => {
111
+ const config = resolve()
112
+ if (!config.enabled) throw new ImageGenError('AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.', 'plugin-disabled')
113
+ if (!config.allowAgentImageGeneration) throw new ImageGenError('Agent image generation is disabled in Settings > Plugins > AI Image.', 'agent-generation-disabled')
114
+ if (config.apiUrl.trim() === '' || config.apiKey.trim() === '') throw new ImageGenError('Image API credentials are not configured. Open Settings > Plugins > AI Image and fill in API URL and API key.', 'image-api-not-configured')
115
+ }
116
+ const selectedModel = (requested: unknown): string => {
117
+ const models = normalizeImageModels(resolve().imageModels)
118
+ const model = typeof requested === 'string' && requested.trim() !== '' ? requested.trim() : models[0]
119
+ if (!models.includes(model)) {
120
+ throw new ImageGenError(`Image model "${model}" is not configured. Choose one of: ${models.join(', ')}.`, 'image-model-not-configured')
121
+ }
122
+ return model
123
+ }
124
+ const materializeTaskImages = (task: GenerationTask): Promise<AgentImageRef[]> => {
125
+ if (task.status !== 'completed') return Promise.resolve([])
126
+ const existing = attachmentRefs.get(task.id)
127
+ if (existing !== undefined) return existing
128
+ const pending = ctx.attachments.saveImages((task.result?.images ?? []).map((image, index) => toSaveImage(image, task.id, index)))
129
+ .then(refs => refs.map(projectRef))
130
+ attachmentRefs.set(task.id, pending)
131
+ void pending.catch(() => {
132
+ if (attachmentRefs.get(task.id) === pending) attachmentRefs.delete(task.id)
133
+ })
134
+ return pending
135
+ }
136
+ const taskResult = async (task: GenerationTask): Promise<AgentTaskResult> => {
137
+ const images = await materializeTaskImages(task)
138
+ return {
139
+ task_id: task.id,
140
+ status: task.status,
141
+ message: task.status === 'completed'
142
+ ? 'Generation completed. The images are attached below and can be reused as source_image in edit_image.'
143
+ : task.status === 'failed'
144
+ ? 'Generation failed.'
145
+ : task.status === 'cancelled'
146
+ ? 'Generation was cancelled.'
147
+ : 'Generation is still running. Completion will be delivered to the conversation automatically with image attachments.',
148
+ ...task.error === undefined ? {} : { error: task.error },
149
+ images,
150
+ }
151
+ }
152
+ const findTask = (id: string): GenerationTask => {
153
+ const task = runtime.queue.list().find(candidate => candidate.id === id)
154
+ if (task === undefined) throw new ImageGenError(`Image generation task ${id} was not found.`, 'task-not-found')
155
+ return task
156
+ }
157
+ const notifyCompletion = async (agent: { send: (message: ReturnType<typeof createUserMessage>, target: 'next-turn', wakeup: true) => void }, task: GenerationTask): Promise<void> => {
158
+ const result = await taskResult(task)
159
+ const completed = task.status === 'completed'
160
+ const text = completed
161
+ ? `图像生成任务已完成(${task.id})。图片已附在这条消息中,可以直接查看、下载或作为后续图生图的参考。`
162
+ : task.status === 'cancelled'
163
+ ? `图像生成任务已取消(${task.id})。`
164
+ : `图像生成任务失败(${task.id}):${task.error ?? '未知错误'}`
165
+ agent.send(createUserMessage({
166
+ content: [
167
+ { type: 'text', text },
168
+ ...completed ? result.images.map(image => ({ type: 'image' as const, attachment: restoreRef(image) })) : [],
169
+ ],
170
+ // The stock conversation's plugin-context row renders text only; a
171
+ // user-role message is the supported path that renders image attachments.
172
+ source: { kind: 'user' },
173
+ }), 'next-turn', true)
174
+ }
175
+ const watchTask = (task: GenerationTask, agent: { send: (message: ReturnType<typeof createUserMessage>, target: 'next-turn', wakeup: true) => void } | undefined): void => {
176
+ if (agent === undefined) return
177
+ let dispose: (() => void) | undefined
178
+ const onChange = (updated: GenerationTask): void => {
179
+ if (updated.id !== task.id || !isFinalTask(updated)) return
180
+ dispose?.()
181
+ if (dispose !== undefined) taskSubscriptions.delete(dispose)
182
+ void notifyCompletion(agent, updated).catch(() => {})
183
+ }
184
+ dispose = runtime.queue.subscribe(onChange)
185
+ taskSubscriptions.add(dispose)
186
+ const current = findTask(task.id)
187
+ if (isFinalTask(current)) onChange(current)
188
+ }
189
+ const disposers = [
190
+ ctx.tools.register(defineTool({
191
+ name: 'generate_image',
192
+ description: 'Queue a text-to-image generation request. This returns immediately with a task id. When it finishes, the conversation automatically receives a visible image-attachment notification; do not repeatedly poll. Only use models configured for this plugin; omit model to use the first configured image model. Use get_image_generation_task only for an explicit status check or recovery.',
193
+ parameters: {
194
+ prompt: { type: 'string', required: true, description: 'Detailed image-generation prompt.' },
195
+ model: { type: 'string', description: 'One of the configured image models. Defaults to the first configured model.' },
196
+ size: { type: 'string', description: 'Aspect ratio such as 1:1, 16:9, 9:16, or auto.' },
197
+ quality: { type: 'string', description: 'auto, 1k, 2k, or 4k.' },
198
+ count: { type: 'integer', description: 'Number of images, 1 to 4. Defaults to 1.' },
199
+ detail: { type: 'string', description: 'Optional provider detail value, for example standard or high.' },
200
+ },
201
+ output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
202
+ async execute(args, exec) {
203
+ ensureConfigured()
204
+ const task = runtime.queue.submit({
205
+ mode: 'text',
206
+ model: selectedModel(args.model),
207
+ prompt: args.prompt.trim(),
208
+ size: args.size ?? 'auto',
209
+ quality: args.quality ?? 'auto',
210
+ n: Math.min(4, Math.max(1, args.count ?? 1)),
211
+ detail: args.detail ?? '',
212
+ })
213
+ watchTask(task, exec.agent)
214
+ return taskResult(task)
215
+ },
216
+ })),
217
+ ctx.tools.register(defineTool({
218
+ name: 'edit_image',
219
+ description: 'Queue an image-to-image edit. source_image must be an image reference returned by a completed generation notification or get_image_generation_task; pass that entire object unchanged. Only configured image models are allowed; omit model to use the first configured model. Completion is automatically delivered to the conversation with visible image attachments; do not repeatedly poll.',
220
+ parameters: {
221
+ prompt: { type: 'string', required: true, description: 'How to transform the source image.' },
222
+ source_image: { ...imageRefSchema, required: true, description: 'Image reference returned by get_image_generation_task.' },
223
+ model: { type: 'string', description: 'One of the configured image models. Defaults to the first configured model.' },
224
+ size: { type: 'string', description: 'Aspect ratio such as 1:1, 16:9, 9:16, or auto.' },
225
+ quality: { type: 'string', description: 'auto, 1k, 2k, or 4k.' },
226
+ count: { type: 'integer', description: 'Number of images, 1 to 4. Defaults to 1.' },
227
+ detail: { type: 'string', description: 'Optional provider detail value.' },
228
+ },
229
+ output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
230
+ async execute(args, exec) {
231
+ ensureConfigured()
232
+ const reference = await ctx.attachments.readImage(restoreRef(args.source_image), exec.signal)
233
+ const task = runtime.queue.submit({
234
+ mode: 'edit',
235
+ model: selectedModel(args.model),
236
+ prompt: args.prompt.trim(),
237
+ size: args.size ?? 'auto',
238
+ quality: args.quality ?? 'auto',
239
+ n: Math.min(4, Math.max(1, args.count ?? 1)),
240
+ detail: args.detail ?? '',
241
+ image: imageDataUrl(reference),
242
+ ...reference.ref.name === undefined ? {} : { refName: reference.ref.name },
243
+ })
244
+ watchTask(task, exec.agent)
245
+ return taskResult(task)
246
+ },
247
+ })),
248
+ ctx.tools.register(defineTool({
249
+ name: 'get_image_generation_task',
250
+ description: 'Optionally check an image-generation task status. Completed tasks return image references for edit_image, but the conversation already receives a visible completion notification automatically; do not poll repeatedly.',
251
+ parameters: { task_id: { type: 'string', required: true, description: 'Task id returned by generate_image or edit_image.' } },
252
+ output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
253
+ async execute(args) {
254
+ ensureConfigured()
255
+ return taskResult(findTask(args.task_id))
256
+ },
257
+ })),
258
+ ctx.tools.register(defineTool({
259
+ name: 'cancel_image_generation_task',
260
+ description: 'Cancel a queued or running image generation task.',
261
+ parameters: { task_id: { type: 'string', required: true, description: 'Task id returned by generate_image or edit_image.' } },
262
+ output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
263
+ async execute(args) {
264
+ ensureConfigured()
265
+ const task = runtime.queue.cancel(args.task_id)
266
+ if (task === undefined) throw new ImageGenError(`Image generation task ${args.task_id} was not found.`, 'task-not-found')
267
+ return taskResult(task)
268
+ },
269
+ })),
270
+ ]
271
+ return () => {
272
+ for (const dispose of taskSubscriptions) dispose()
273
+ taskSubscriptions.clear()
274
+ for (const dispose of disposers) dispose()
275
+ }
276
+ }
277
+
278
+ function isFinalTask(task: GenerationTask): boolean {
279
+ return task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'
280
+ }
281
+
282
+ function toSaveImage(image: GeneratedImage, taskId: string, index: number): { data: Uint8Array; mediaType: ImageMediaType; name: string } {
283
+ const mediaType = acceptedMediaType(image.mime) ? image.mime : 'image/png'
284
+ return {
285
+ data: Buffer.from(image.b64, 'base64'),
286
+ mediaType,
287
+ name: `imagegen-${taskId}-${index + 1}.${mediaType === 'image/jpeg' ? 'jpg' : mediaType.slice('image/'.length)}`,
288
+ }
289
+ }
@@ -16,13 +16,9 @@ import { errorMessage, tt } from './helpers.ts'
16
16
  import { TemplateLibrary } from './TemplateLibrary.tsx'
17
17
  import type { GeneratedImage, GenerateMode, GenerateRequest, GenerationTask, HistoryEntry, HistoryImageRef, UpdateInfo } from '../protocol.ts'
18
18
  import type { ImageGenConfig, ImageGenScope } from './settings-scope.ts'
19
+ import { DEFAULT_IMAGE_MODELS, normalizeImageModels } from '../image-models.ts'
19
20
  import css from './panel.module.css'
20
21
 
21
- /** Models offered by the dropdown. Anything OpenAI-compatible that answers
22
- * /images/generations (+ /images/edits) works; grok-imagine-image is handled
23
- * specially host-side (JSON /images/edits, aspect_ratio, b64_json). */
24
- const MODELS = ['gpt-image-2', 'grok-imagine-image'] as const
25
-
26
22
  /** Size options, presented as aspect ratios (auto = let the model decide).
27
23
  * The host maps each ratio onto the model's own vocabulary: aspect_ratio for
28
24
  * Grok Imagine, the closest pixel size for OpenAI-compatible endpoints. */
@@ -153,7 +149,7 @@ function formatTime(timestamp: number): string {
153
149
  /** Studio tabs: the two generation modes plus the gallery view. */
154
150
  type PanelTab = GenerateMode | 'gallery'
155
151
 
156
- type GalleryFilter = 'all' | 'text' | 'edit' | 'gpt-image-2' | 'grok-imagine-image'
152
+ type GalleryFilter = string
157
153
  type ComparisonSession = { taskIds: string[]; prompt: string }
158
154
 
159
155
  /** Render the studio. */
@@ -169,6 +165,7 @@ export function ImageGenPanel(props: {
169
165
  const apiKeySet = useSecretSet(scope, 'apiKey')
170
166
  const promptKeySet = useSecretSet(scope, 'promptApiKey')
171
167
  const connected = enabled && configured && apiKeySet
168
+ const imageModels = normalizeImageModels(config?.imageModels)
172
169
 
173
170
  const [tab, setTab] = useState<PanelTab>('text')
174
171
  const [prompt, setPrompt] = useState('')
@@ -176,9 +173,9 @@ export function ImageGenPanel(props: {
176
173
  const [quality, setQuality] = useState<string>('auto')
177
174
  const [count, setCount] = useState(1)
178
175
  const [detail, setDetail] = useState('')
179
- const [model, setModel] = useState<string>(MODELS[0])
176
+ const [model, setModel] = useState<string>(DEFAULT_IMAGE_MODELS[0])
180
177
  const [compareEnabled, setCompareEnabled] = useState(false)
181
- const [compareModels, setCompareModels] = useState<string[]>([...MODELS])
178
+ const [compareModels, setCompareModels] = useState<string[]>([...DEFAULT_IMAGE_MODELS])
182
179
  const [modelOpen, setModelOpen] = useState(false)
183
180
  const [refImage, setRefImage] = useState<{ dataUrl: string; name: string } | null>(null)
184
181
  const [images, setImages] = useState<GeneratedImage[]>([])
@@ -222,6 +219,17 @@ export function ImageGenPanel(props: {
222
219
  const previewStage = useRef<HTMLDivElement>(null)
223
220
  const elapsed = useElapsed(generating, startedAt)
224
221
 
222
+ // A saved settings change is authoritative. Keep the active selection and
223
+ // comparison choices in that allow-list without disturbing valid choices.
224
+ const imageModelKey = imageModels.join('\u0000')
225
+ useEffect(() => {
226
+ setModel(previous => imageModels.includes(previous) ? previous : imageModels[0])
227
+ setCompareModels(previous => {
228
+ const retained = previous.filter(candidate => imageModels.includes(candidate))
229
+ return retained.length > 0 ? retained : [imageModels[0]]
230
+ })
231
+ }, [imageModelKey])
232
+
225
233
  const filteredGallery = gallery
226
234
  .filter(entry => {
227
235
  if (galleryFilter === 'all') return true
@@ -235,6 +243,7 @@ export function ImageGenPanel(props: {
235
243
  .sort((a, b) => gallerySort === 'newest' ? b.createdAt - a.createdAt : a.createdAt - b.createdAt)
236
244
 
237
245
  const galleryTagOptions = [...new Set(gallery.flatMap(entry => entry.tags ?? []))].sort((a, b) => a.localeCompare(b))
246
+ const galleryModels = [...new Set([...imageModels, ...gallery.map(entry => entry.model)])]
238
247
 
239
248
  const filteredHistory = history.filter(entry => {
240
249
  const query = historyQuery.trim().toLocaleLowerCase()
@@ -399,7 +408,7 @@ export function ImageGenPanel(props: {
399
408
  }
400
409
  const request: GenerateRequest = {
401
410
  mode: tab === 'gallery' ? 'text' : tab,
402
- model,
411
+ model: imageModels.includes(model) ? model : imageModels[0],
403
412
  prompt: promptText,
404
413
  size,
405
414
  quality,
@@ -410,7 +419,7 @@ export function ImageGenPanel(props: {
410
419
  }
411
420
  setError(null)
412
421
  try {
413
- const targetModels = compareEnabled ? compareModels : [model]
422
+ const targetModels = (compareEnabled ? compareModels : [request.model]).filter(candidate => imageModels.includes(candidate))
414
423
  if (targetModels.length === 0) {
415
424
  setError(tt('compare.selectRequired'))
416
425
  return
@@ -497,7 +506,7 @@ export function ImageGenPanel(props: {
497
506
  setQuality(normalizeQuality(entry.quality))
498
507
  setDetail((DETAILS as readonly string[]).includes(entry.detail) ? entry.detail : '')
499
508
  setCount(entry.n >= 1 && entry.n <= 4 ? entry.n : 1)
500
- setModel((MODELS as readonly string[]).includes(entry.model) ? entry.model : MODELS[0])
509
+ setModel(imageModels.includes(entry.model) ? entry.model : imageModels[0])
501
510
  setRefImage(null)
502
511
  setImages(restored)
503
512
  setError(null)
@@ -607,7 +616,7 @@ export function ImageGenPanel(props: {
607
616
  setQuality(normalizeQuality(entry.quality))
608
617
  setDetail((DETAILS as readonly string[]).includes(entry.detail) ? entry.detail : '')
609
618
  setCount(entry.n >= 1 && entry.n <= 4 ? entry.n : 1)
610
- setModel((MODELS as readonly string[]).includes(entry.model) ? entry.model : MODELS[0])
619
+ setModel(imageModels.includes(entry.model) ? entry.model : imageModels[0])
611
620
  setRefImage(null)
612
621
  setImages(restored)
613
622
  setError(null)
@@ -794,13 +803,12 @@ export function ImageGenPanel(props: {
794
803
  {tab === 'gallery' ? (
795
804
  <div className={css.galleryFilters}>
796
805
  <div className={css.galleryFilterHeading}>{tt('gallery.categories')}</div>
797
- {([
798
- ['all', 'gallery.all'],
799
- ['text', 'mode.text'],
800
- ['edit', 'mode.edit'],
801
- ['gpt-image-2', 'gallery.gpt'],
802
- ['grok-imagine-image', 'gallery.grok'],
803
- ] as const).map(([value, label]) => (
806
+ {[
807
+ ['all', tt('gallery.all')],
808
+ ['text', tt('mode.text')],
809
+ ['edit', tt('mode.edit')],
810
+ ...galleryModels.map(value => [value, value]),
811
+ ].map(([value, label]) => (
804
812
  <button
805
813
  key={value}
806
814
  type="button"
@@ -808,7 +816,7 @@ export function ImageGenPanel(props: {
808
816
  data-active={galleryFilter === value ? '' : undefined}
809
817
  onClick={() => { setGalleryFilter(value) }}
810
818
  >
811
- <span>{tt(label as never)}</span>
819
+ <span>{label}</span>
812
820
  <span className={css.galleryFilterCount}>{gallery.filter(entry => value === 'all' || value === 'text' || value === 'edit' ? (value === 'all' ? true : entry.mode === value) : entry.model === value).length}</span>
813
821
  </button>
814
822
  ))}
@@ -1012,7 +1020,7 @@ export function ImageGenPanel(props: {
1012
1020
  </button>
1013
1021
  {modelOpen ? (
1014
1022
  <div className={css.modelMenuList} role="listbox" aria-label={tt('model.label')}>
1015
- {MODELS.map(option => (
1023
+ {imageModels.map(option => (
1016
1024
  <button
1017
1025
  key={option}
1018
1026
  type="button"
@@ -1036,7 +1044,7 @@ export function ImageGenPanel(props: {
1036
1044
  </label>
1037
1045
  {compareEnabled ? (
1038
1046
  <div className={css.compareModelChoices} role="group" aria-label={tt('compare.models')}>
1039
- {MODELS.map(option => (
1047
+ {imageModels.map(option => (
1040
1048
  <label key={option}>
1041
1049
  <input type="checkbox" checked={compareModels.includes(option)} onChange={() => { setCompareModels(previous => previous.includes(option) ? previous.filter(value => value !== option) : [...previous, option]) }} />
1042
1050
  <span>{option}</span>