@dickpy/dsh-imagegen 1.5.5 → 1.5.7

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.
@@ -17,15 +17,15 @@ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
17
17
  import type { ImageGenApi } from './api.ts'
18
18
  import { errorMessage, tt } from './helpers.ts'
19
19
  import { TemplateLibrary } from './TemplateLibrary.tsx'
20
- import { InspirationGallery } from './InspirationGallery.tsx'
21
- import { CanvasWorkspace } from './CanvasWorkspace.tsx'
20
+ import { InspirationGallery } from './InspirationGallery.tsx'
21
+ import { CanvasWorkspace } from './CanvasWorkspace.tsx'
22
22
  import { useImageGenLanguageTick } from './use-language.ts'
23
23
  import type { EcommerceRefRole, GeneratedImage, GenerateMode, GenerateRequest, GenerationTask, GenerationTaskStatus, HistoryEntry, HistoryImageRef, ProductSetDraft, ProductSetSlot, UpdateInfo } from '../protocol.ts'
24
24
  import { AGENT_IMAGE_API } from '../protocol.ts'
25
25
  import type { ImageGenConfig, ImageGenScope } from './settings-scope.ts'
26
26
  import { imageModelOptions } from './settings-scope.ts'
27
27
  import { normalizeImageModels } from '../image-models.ts'
28
- import { describeModel } from '../model-catalog.ts'
28
+ import { describeModel, promptCharLimit } from '../model-catalog.ts'
29
29
  import { CHAT_IMAGE_EVENT, type ChatImageEventDetail, type ConversationService } from './conversation-sync.ts'
30
30
  import css from './panel.module.css'
31
31
 
@@ -396,7 +396,7 @@ type PanelTab = GenerateMode | 'gallery'
396
396
 
397
397
  /** Top-level workspaces inside the panel. 'normal' is the classic studio;
398
398
  * more task-oriented modes (prototype, …) can join alongside 'ecommerce'. */
399
- type PanelWorkspace = 'normal' | 'ecommerce' | 'canvas'
399
+ type PanelWorkspace = 'normal' | 'ecommerce' | 'canvas'
400
400
 
401
401
  type GalleryFilter = string
402
402
  type ComparisonSession = { taskIds: string[]; prompt: string; comparisonId: string }
@@ -484,18 +484,18 @@ export function ImageGenPanel(props: {
484
484
  const apiKeySet = (config?.channels ?? []).length > 0 ? channelKeySet : legacyKeySet
485
485
  const connected = enabled && configured && apiKeySet
486
486
 
487
- const [tab, setTab] = useState<PanelTab>('text')
488
- const [workspace, setWorkspace] = useState<PanelWorkspace>('normal')
489
- const [canvasImportRequest, setCanvasImportRequest] = useState<{ source: 'history' | 'gallery'; entryId: string; imageIndex: number } | undefined>()
487
+ const [tab, setTab] = useState<PanelTab>('text')
488
+ const [workspace, setWorkspace] = useState<PanelWorkspace>('normal')
489
+ const [canvasImportRequest, setCanvasImportRequest] = useState<{ source: 'history' | 'gallery'; entryId: string; imageIndex: number } | undefined>()
490
490
  /** Switch to a normal-generation tab, leaving any task workspace. */
491
- const openTab = (next: PanelTab): void => {
492
- setWorkspace('normal')
493
- setTab(next)
494
- }
495
- const addEntryToCanvas = (source: 'history' | 'gallery', entryId: string, imageIndex = 0): void => {
496
- setCanvasImportRequest({ source, entryId, imageIndex })
497
- setWorkspace('canvas')
498
- }
491
+ const openTab = (next: PanelTab): void => {
492
+ setWorkspace('normal')
493
+ setTab(next)
494
+ }
495
+ const addEntryToCanvas = (source: 'history' | 'gallery', entryId: string, imageIndex = 0): void => {
496
+ setCanvasImportRequest({ source, entryId, imageIndex })
497
+ setWorkspace('canvas')
498
+ }
499
499
  const [prompt, setPrompt] = useState('')
500
500
  const [size, setSize] = useState<string>('auto')
501
501
  const [quality, setQuality] = useState<string>('auto')
@@ -524,6 +524,7 @@ export function ImageGenPanel(props: {
524
524
  const [galleryAdding, setGalleryAdding] = useState(false)
525
525
  const [galleryMessage, setGalleryMessage] = useState<string | null>(null)
526
526
  const [galleryFilter, setGalleryFilter] = useState<GalleryFilter>('all')
527
+ const galleryUploadRef = useRef<HTMLInputElement>(null)
527
528
  const [galleryRatio, setGalleryRatio] = useState('all')
528
529
  const [galleryTagFilter, setGalleryTagFilter] = useState<string | null>(null)
529
530
  const [galleryView, setGalleryView] = useState<'masonry' | 'grid'>('masonry')
@@ -1253,6 +1254,47 @@ export function ImageGenPanel(props: {
1253
1254
  }
1254
1255
  }
1255
1256
 
1257
+ /** Save local image files into the asset library so the canvas and studio
1258
+ * can reuse them later; entries dedupe by content host-side. */
1259
+ const uploadGalleryFiles = async (files: File[]): Promise<void> => {
1260
+ if (files.length === 0 || galleryAdding) return
1261
+ setGalleryAdding(true)
1262
+ try {
1263
+ let entries: HistoryEntry[] | undefined
1264
+ for (const file of files) {
1265
+ const dataUrl = await new Promise<string>((resolve, reject) => {
1266
+ const reader = new FileReader()
1267
+ reader.onload = () => resolve(String(reader.result))
1268
+ reader.onerror = () => reject(new Error(tt('canvas.dropSub')))
1269
+ reader.readAsDataURL(file)
1270
+ })
1271
+ const separator = dataUrl.indexOf(',')
1272
+ const result = await api.galleryAppend({
1273
+ id: '',
1274
+ createdAt: Date.now(),
1275
+ mode: 'text',
1276
+ model: tt('gallery.uploadModel'),
1277
+ prompt: file.name.replace(/\.[a-z0-9]+$/i, ''),
1278
+ size: 'auto',
1279
+ quality: 'auto',
1280
+ detail: '',
1281
+ n: 1,
1282
+ images: [{ b64: separator >= 0 ? dataUrl.slice(separator + 1) : dataUrl, mime: file.type || 'image/png' }],
1283
+ })
1284
+ entries = result.entries
1285
+ }
1286
+ if (entries !== undefined) {
1287
+ setGallery(entries)
1288
+ setGalleryMessage(tt('gallery.uploaded'))
1289
+ window.setTimeout(() => { setGalleryMessage(null) }, 2200)
1290
+ }
1291
+ } catch (caught) {
1292
+ setError(errorMessage(caught))
1293
+ } finally {
1294
+ setGalleryAdding(false)
1295
+ }
1296
+ }
1297
+
1256
1298
  /** Put a generated image into the native conversation composer. */
1257
1299
  const addImageToConversation = async (image: GeneratedImage, index: number, actionKey: number | string = index): Promise<void> => {
1258
1300
  if (addingToConversation !== null) return
@@ -1439,7 +1481,15 @@ export function ImageGenPanel(props: {
1439
1481
  })
1440
1482
  }
1441
1483
 
1442
- const generateDisabled = submitting || modeModels.length === 0
1484
+ // Prompt hard limit of the model the generate button will actually use
1485
+ // (mirrors handleGenerate's pick). Over the limit the counter turns red and
1486
+ // the button locks — the engine would fast-fail anyway, but the user sees
1487
+ // why before spending a click.
1488
+ const activeModel = modeModels.includes(model) ? model : modeModels[0] ?? ''
1489
+ const promptLimit = promptCharLimit(activeModel)
1490
+ const promptOverLimit = promptLimit !== null && prompt.trim().length >= promptLimit
1491
+
1492
+ const generateDisabled = submitting || modeModels.length === 0 || promptOverLimit
1443
1493
  const ecommerceSlots = ecommerce.slots.filter(slot => slot.enabled && slot.count > 0)
1444
1494
  const ecommerceTotal = ecommerceSlots.reduce((total, slot) => total + slot.count, 0)
1445
1495
  const ecommerceGenerateDisabled = submitting || ecommerceGenerating || ecommerceSlots.length === 0 || ecommerce.productName.trim() === '' || (ecommerce.language === 'custom' && effectiveEcommerceLanguage(ecommerce) === '')
@@ -1626,8 +1676,8 @@ export function ImageGenPanel(props: {
1626
1676
  </span>
1627
1677
  </button>
1628
1678
  <span className={css.historyActions}>
1629
- {entry.images.length > 0 ? (
1630
- <button
1679
+ {entry.images.length > 0 ? (
1680
+ <button
1631
1681
  type="button"
1632
1682
  className={css.historyIconAction}
1633
1683
  disabled={conversationBusy}
@@ -1650,21 +1700,21 @@ export function ImageGenPanel(props: {
1650
1700
  onClick={() => { void addHistoryEntryToGallery(entry) }}
1651
1701
  >
1652
1702
  <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2.5" y="3" width="11" height="10" rx="1.5" /><circle cx="5.9" cy="6.1" r="1" /><path d="M13.5 10.2l-3.1-3.1L4.6 13" /></svg>
1653
- </button>
1654
- ) : null}
1655
- {entry.images.length > 0 ? (
1656
- <button
1657
- type="button"
1658
- className={css.historyIconAction}
1659
- title={tt('canvas.addToCanvas')}
1660
- aria-label={tt('canvas.addToCanvas')}
1661
- data-history-add-canvas=""
1662
- onClick={() => { addEntryToCanvas('history', entry.id, 0) }}
1663
- >
1664
- <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="11" height="11" rx="1.5" /><path d="M5 8h6M8 5v6" /></svg>
1665
- </button>
1666
- ) : null}
1667
- <button
1703
+ </button>
1704
+ ) : null}
1705
+ {entry.images.length > 0 ? (
1706
+ <button
1707
+ type="button"
1708
+ className={css.historyIconAction}
1709
+ title={tt('canvas.addToCanvas')}
1710
+ aria-label={tt('canvas.addToCanvas')}
1711
+ data-history-add-canvas=""
1712
+ onClick={() => { addEntryToCanvas('history', entry.id, 0) }}
1713
+ >
1714
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="11" height="11" rx="1.5" /><path d="M5 8h6M8 5v6" /></svg>
1715
+ </button>
1716
+ ) : null}
1717
+ <button
1668
1718
  type="button"
1669
1719
  className={css.historyIconAction}
1670
1720
  data-danger
@@ -1683,6 +1733,9 @@ export function ImageGenPanel(props: {
1683
1733
  </aside>
1684
1734
  )
1685
1735
 
1736
+ const isGallery = workspace === 'normal' && tab === 'gallery'
1737
+ const isGeneration = workspace === 'normal' && tab !== 'gallery'
1738
+
1686
1739
  return (
1687
1740
  <div className={css.panel}>
1688
1741
  <header className={css.panelHeader}>
@@ -1699,12 +1752,12 @@ export function ImageGenPanel(props: {
1699
1752
  <svg viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
1700
1753
  </a>
1701
1754
  </span>
1702
- <nav className={css.topNav} role="tablist" aria-label={tt('workspace.label')}>
1703
- <button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab !== 'gallery' ? '' : undefined} onClick={() => { if (workspace !== 'normal' || tab === 'gallery') openTab('text') }}>{tt('workspace.normal')}</button>
1704
- <button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab === 'gallery' ? '' : undefined} onClick={() => { openTab('gallery') }}>{tt('gallery.title')}</button>
1705
- <span className={css.topNavDivider} aria-hidden="true" />
1706
- <button type="button" className={css.topNavItem} data-active={workspace === 'canvas' ? '' : undefined} onClick={() => { setWorkspace('canvas') }}>{tt('workspace.canvas')}</button>
1707
- <button type="button" className={css.topNavItem} data-active={workspace === 'ecommerce' ? '' : undefined} onClick={() => { setWorkspace('ecommerce') }}>{tt('workspace.ecommerce')}<span className={css.previewBadge}>{tt('ecommerce.badge')}</span></button>
1755
+ <nav className={css.topNav} role="tablist" aria-label={tt('workspace.label')}>
1756
+ <button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab !== 'gallery' ? '' : undefined} onClick={() => { if (workspace !== 'normal' || tab === 'gallery') openTab('text') }}>{tt('workspace.normal')}</button>
1757
+ <button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab === 'gallery' ? '' : undefined} onClick={() => { openTab('gallery') }}>{tt('gallery.title')}</button>
1758
+ <span className={css.topNavDivider} aria-hidden="true" />
1759
+ <button type="button" className={css.topNavItem} data-active={workspace === 'canvas' ? '' : undefined} onClick={() => { setWorkspace('canvas') }}>{tt('workspace.canvas')}</button>
1760
+ <button type="button" className={css.topNavItem} data-active={workspace === 'ecommerce' ? '' : undefined} onClick={() => { setWorkspace('ecommerce') }}>{tt('workspace.ecommerce')}<span className={css.previewBadge}>{tt('ecommerce.badge')}</span></button>
1708
1761
  </nav>
1709
1762
  <span className={css.panelHeaderActions}>
1710
1763
  <button
@@ -1746,7 +1799,7 @@ export function ImageGenPanel(props: {
1746
1799
 
1747
1800
  <div className={css.studio}>
1748
1801
  {/* ------------------------------- left history + generation workspace */}
1749
- <div className={css.generation} data-workspace={workspace}>
1802
+ <div className={css.generation} data-workspace={workspace}>
1750
1803
  {/* ------------------------------------------------ config sidebar */}
1751
1804
  <aside
1752
1805
  ref={configAsideRef}
@@ -1770,7 +1823,7 @@ export function ImageGenPanel(props: {
1770
1823
  </svg>
1771
1824
  </button>
1772
1825
  </div>
1773
- {workspace === 'normal' && tab === 'gallery' ? (
1826
+ {isGallery ? (
1774
1827
  <div className={css.galleryFilters}>
1775
1828
  <div className={css.galleryFilterHeading}>{tt('gallery.categories')}</div>
1776
1829
  {[
@@ -2000,7 +2053,7 @@ export function ImageGenPanel(props: {
2000
2053
  </section>
2001
2054
  ) : null}
2002
2055
 
2003
- {tab === 'edit' ? (
2056
+ {isGeneration && tab === 'edit' ? (
2004
2057
  <section className={css.card}>
2005
2058
  {refImage === null
2006
2059
  ? (
@@ -2048,7 +2101,7 @@ export function ImageGenPanel(props: {
2048
2101
  ) : null}
2049
2102
 
2050
2103
  {/* prompt (normal workspace only — ecommerce has its own form) */}
2051
- {workspace === 'normal' ? (<>
2104
+ {isGeneration ? (<>
2052
2105
  <section className={css.card}>
2053
2106
  <textarea
2054
2107
  className={css.prompt}
@@ -2075,7 +2128,15 @@ export function ImageGenPanel(props: {
2075
2128
  >
2076
2129
  {enhancing ? tt('prompt.enhancing') : tt('prompt.enhance')}
2077
2130
  </button>
2078
- <span className={css.promptCount}>{tt('prompt.count', { count: prompt.length })}</span>
2131
+ <span
2132
+ className={css.promptCount}
2133
+ data-over={promptOverLimit ? '' : undefined}
2134
+ title={promptOverLimit ? tt('prompt.overLimit', { limit: promptLimit ?? 0 }) : undefined}
2135
+ >
2136
+ {promptLimit === null
2137
+ ? tt('prompt.count', { count: prompt.length })
2138
+ : tt('prompt.countLimit', { count: prompt.length, limit: promptLimit })}
2139
+ </span>
2079
2140
  </div>
2080
2141
  </section>
2081
2142
 
@@ -2172,7 +2233,7 @@ export function ImageGenPanel(props: {
2172
2233
  )}
2173
2234
  </div>
2174
2235
  ) : null}
2175
- {workspace === 'ecommerce' ? null : <label className={css.modelWrap}>
2236
+ {isGeneration ? <label className={css.modelWrap}>
2176
2237
  <span className={css.modelLabel}>{tt('model.label')}</span>
2177
2238
  <span ref={modelMenuRef} className={css.modelMenu} data-open={modelOpen ? 'true' : 'false'}>
2178
2239
  <button
@@ -2204,8 +2265,8 @@ export function ImageGenPanel(props: {
2204
2265
  </div>
2205
2266
  ) : null}
2206
2267
  </span>
2207
- </label>}
2208
- {workspace !== 'ecommerce' ? <div className={css.compareControl}>
2268
+ </label> : null}
2269
+ {isGeneration ? <div className={css.compareControl}>
2209
2270
  <label className={css.compareToggle}>
2210
2271
  <input type="checkbox" checked={compareEnabled} onChange={event => { setCompareEnabled(event.target.checked) }} />
2211
2272
  <span>{tt('compare.enable')}</span>
@@ -2221,7 +2282,7 @@ export function ImageGenPanel(props: {
2221
2282
  </div>
2222
2283
  ) : null}
2223
2284
  </div> : null}
2224
- {workspace !== 'ecommerce' ? <Button
2285
+ {isGeneration ? <Button
2225
2286
  variant="primary"
2226
2287
  size="md"
2227
2288
  className={css.generateButton}
@@ -2236,26 +2297,26 @@ export function ImageGenPanel(props: {
2236
2297
  ) : tt('generate')}
2237
2298
  </Button> : null}
2238
2299
  </section>
2239
- </aside>
2240
-
2241
- {workspace === 'canvas' ? (
2242
- <CanvasWorkspace
2243
- api={api}
2244
- imageModels={imageModels}
2245
- defaultChannelId={defaultChannelId}
2246
- connected={connected}
2247
- history={history}
2248
- gallery={gallery}
2249
- tasks={tasks}
2250
- importRequest={canvasImportRequest}
2251
- onImportRequestHandled={() => { setCanvasImportRequest(undefined) }}
2252
- onOpenSettings={() => { openSettingsGuide('generation') }}
2253
- />
2254
- ) : null}
2255
-
2256
- {/* ------------------------------------------------------- canvas */}
2257
- <section className={css.canvas} data-gallery={workspace === 'normal' && tab === 'gallery' ? 'true' : undefined}>
2258
- {workspace === 'normal' && tab === 'gallery' ? (
2300
+ </aside>
2301
+
2302
+ {workspace === 'canvas' ? (
2303
+ <CanvasWorkspace
2304
+ api={api}
2305
+ imageModels={imageModels}
2306
+ defaultChannelId={defaultChannelId}
2307
+ connected={connected}
2308
+ history={history}
2309
+ gallery={gallery}
2310
+ tasks={tasks}
2311
+ importRequest={canvasImportRequest}
2312
+ onImportRequestHandled={() => { setCanvasImportRequest(undefined) }}
2313
+ onOpenSettings={() => { openSettingsGuide('generation') }}
2314
+ />
2315
+ ) : null}
2316
+
2317
+ {/* ------------------------------------------------------- canvas */}
2318
+ <section className={css.canvas} data-gallery={isGallery ? 'true' : undefined}>
2319
+ {isGallery ? (
2259
2320
  <div className={css.galleryWorkspace}>
2260
2321
  <header className={css.galleryToolbar}>
2261
2322
  <div>
@@ -2264,6 +2325,19 @@ export function ImageGenPanel(props: {
2264
2325
  </div>
2265
2326
  <div className={css.galleryToolbarActions}>
2266
2327
  <input className={css.gallerySearch} value={galleryQuery} onChange={event => { setGalleryQuery(event.target.value) }} placeholder={tt('gallery.search')} aria-label={tt('gallery.search')} />
2328
+ <button type="button" className={css.gallerySelectMode} disabled={galleryAdding} title={tt('gallery.uploadHint')} onClick={() => galleryUploadRef.current?.click()}>{galleryAdding ? '…' : tt('gallery.upload')}</button>
2329
+ <input
2330
+ ref={galleryUploadRef}
2331
+ type="file"
2332
+ accept="image/png,image/jpeg,image/webp,image/gif"
2333
+ multiple
2334
+ hidden
2335
+ onChange={event => {
2336
+ const files = [...(event.target.files ?? [])]
2337
+ event.target.value = ''
2338
+ if (files.length > 0) void uploadGalleryFiles(files)
2339
+ }}
2340
+ />
2267
2341
  <button type="button" className={css.gallerySelectMode} data-active={gallerySelecting ? '' : undefined} aria-pressed={gallerySelecting} onClick={() => { setGallerySelecting(previous => !previous) }}>
2268
2342
  {gallerySelecting ? tt('gallery.selectionDone') : tt('gallery.select')}
2269
2343
  </button>
@@ -2310,7 +2384,7 @@ export function ImageGenPanel(props: {
2310
2384
  <span className={css.galleryBadge}>{entry.mode === 'edit' ? tt('mode.edit') : tt('mode.text')}</span>
2311
2385
  </button>
2312
2386
  <div className={css.galleryCardActions}>
2313
- <button
2387
+ <button
2314
2388
  type="button"
2315
2389
  className={css.galleryCardAction}
2316
2390
  data-gallery-add-conversation=""
@@ -2319,19 +2393,19 @@ export function ImageGenPanel(props: {
2319
2393
  onClick={(event) => { event.stopPropagation(); void addGalleryEntryToConversation(entry) }}
2320
2394
  >
2321
2395
  <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 4.5h10v7H3z"/><path d="M5.5 2.5h5M8 6v4M6 8h4"/></svg>
2322
- {galleryConversationAddingId === entry.id || addingToConversation === `gallery:${entry.id}` ? tt('conversation.adding') : tt('conversation.add')}
2323
- </button>
2324
- <button
2325
- type="button"
2326
- className={css.galleryCardAction}
2327
- data-gallery-add-canvas=""
2328
- title={tt('canvas.addToCanvas')}
2329
- onClick={(event) => { event.stopPropagation(); addEntryToCanvas('gallery', entry.id, 0) }}
2330
- >
2331
- <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="11" height="11" rx="1.5" /><path d="M5 8h6M8 5v6" /></svg>
2332
- {tt('canvas.addToCanvas')}
2333
- </button>
2334
- </div>
2396
+ {galleryConversationAddingId === entry.id || addingToConversation === `gallery:${entry.id}` ? tt('conversation.adding') : tt('conversation.add')}
2397
+ </button>
2398
+ <button
2399
+ type="button"
2400
+ className={css.galleryCardAction}
2401
+ data-gallery-add-canvas=""
2402
+ title={tt('canvas.addToCanvas')}
2403
+ onClick={(event) => { event.stopPropagation(); addEntryToCanvas('gallery', entry.id, 0) }}
2404
+ >
2405
+ <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="11" height="11" rx="1.5" /><path d="M5 8h6M8 5v6" /></svg>
2406
+ {tt('canvas.addToCanvas')}
2407
+ </button>
2408
+ </div>
2335
2409
  <div className={css.galleryCardFooter}>
2336
2410
  <span className={css.galleryAvatar}>{entry.model.toLowerCase().startsWith('nanobanana') ? 'N' : entry.model.toLowerCase().startsWith('seedream') ? 'S' : entry.model.startsWith('grok') ? 'G' : 'D'}</span>
2337
2411
  <span className={css.galleryCardInfo}>
@@ -2426,7 +2500,7 @@ export function ImageGenPanel(props: {
2426
2500
  )}
2427
2501
  </div>
2428
2502
  ) : null}
2429
- {(workspace === 'ecommerce' || tab !== 'gallery') && tasks.length > 0 ? (
2503
+ {!isGallery && tasks.length > 0 ? (
2430
2504
  <section className={css.taskTray} data-open={taskTrayOpen ? 'true' : 'false'} aria-label={tt('tasks.title')}>
2431
2505
  <header className={css.taskTrayHeader}>
2432
2506
  <button type="button" className={css.taskTrayToggle} aria-expanded={taskTrayOpen} onClick={() => { setTaskTrayOpen(open => !open) }}>
@@ -2474,7 +2548,7 @@ export function ImageGenPanel(props: {
2474
2548
  </div>
2475
2549
  </section>
2476
2550
  ) : null}
2477
- {generating && comparison === null && workspace !== 'ecommerce' ? (
2551
+ {generating && comparison === null && isGeneration ? (
2478
2552
  <div className={css.canvasState} data-generation-state={activeTask?.status ?? 'submitting'} role="status">
2479
2553
  <span className={css.bigSpinner} />
2480
2554
  <span className={css.canvasStateTitle}>
@@ -2496,7 +2570,7 @@ export function ImageGenPanel(props: {
2496
2570
  <div className={css.canvasError} role="alert">{tt('canvas.error', { error })}</div>
2497
2571
  ) : null}
2498
2572
 
2499
- {!generating && !error && images.length === 0 && workspace !== 'ecommerce' ? (
2573
+ {isGeneration && !generating && !error && images.length === 0 ? (
2500
2574
  <InspirationGallery
2501
2575
  api={api}
2502
2576
  onUse={(text) => {
@@ -2506,7 +2580,7 @@ export function ImageGenPanel(props: {
2506
2580
  />
2507
2581
  ) : null}
2508
2582
 
2509
- {!generating && images.length > 0 && workspace !== 'ecommerce' ? (
2583
+ {isGeneration && !generating && images.length > 0 ? (
2510
2584
  <div className={css.canvasBody}>
2511
2585
  <div className={css.canvasMeta}>
2512
2586
  <span>{tt('canvas.images', { count: images.length })}</span>