@goodandready/dsh-image-gen 0.10.35 → 0.11.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/lib/client.js CHANGED
@@ -100,7 +100,7 @@ window.__ModuleLoader__.load({
100
100
  .ig-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
101
101
  .ig-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
102
102
  .ig-btn-primary:hover:not(:disabled){opacity:0.9}
103
- .ig-btn-active{background:var(--dsw-alias-brand-primary, #3b82f6);color:#ffffff;border-color:transparent}
103
+ .ig-btn-active{background:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-bg-layer-1);border-color:transparent}
104
104
  .ig-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
105
105
  .ig-btn-danger:hover:not(:disabled){background:var(--dsw-alias-state-error-bg, color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent));border-color:var(--dsw-alias-state-error-primary)}
106
106
  .ig-btn-disabled{opacity:0.5;cursor:not-allowed}
@@ -119,6 +119,39 @@ window.__ModuleLoader__.load({
119
119
  .ig-matrix-img{width:100%;aspect-ratio:1/1;object-fit:cover;cursor:pointer}
120
120
  .ig-matrix-bar{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;font-size:11px}
121
121
 
122
+ /* Revision History (#148) */
123
+ .ig-rev-bar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:var(--dsw-alias-bg-layer-2);border-radius:8px;border:1px solid var(--dsw-alias-border-l2);margin-bottom:8px;font-size:12px}
124
+ .ig-rev-controls{display:flex;align-items:center;gap:6px}
125
+ .ig-rev-btn{appearance:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px;font-weight:600;transition:all .15s ease}
126
+ .ig-rev-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-secondary)}
127
+ .ig-rev-btn:disabled{opacity:0.4;cursor:not-allowed}
128
+ .ig-rev-badge{font-size:11px;color:var(--dsw-alias-label-secondary)}
129
+
130
+ /* Progressive Draft Preview (#147) */
131
+ .ig-progressive-wrap{position:relative;width:100%;overflow:hidden;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-height:160px;display:flex;align-items:center;justify-content:center}
132
+ .ig-draft-img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;filter:blur(6px);opacity:0.75;transform:scale(1.02);transition:opacity 0.4s ease}
133
+ .ig-final-img{position:relative;width:100%;height:auto;display:block;opacity:1;transition:opacity 0.35s ease}
134
+ .ig-draft-overlay{position:absolute;bottom:8px;left:8px;padding:3px 8px;border-radius:4px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 80%, transparent);font-size:11px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px;backdrop-filter:blur(4px)}
135
+
136
+ @media (prefers-reduced-motion: reduce){
137
+ .ig-draft-img,.ig-final-img{transition:none !important}
138
+ }
139
+
140
+ /* Asset Vault & Studio (#158, #159) */
141
+ .ig-vault-header{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;margin-bottom:12px}
142
+ .ig-vault-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:12px}
143
+ .ig-vault-card{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;transition:transform .15s ease, border-color .15s ease}
144
+ .ig-vault-card:hover{border-color:var(--dsw-alias-state-brand-primary);transform:translateY(-2px)}
145
+ .ig-vault-thumb{width:100%;aspect-ratio:1/1;object-fit:cover;cursor:pointer;background:var(--dsw-alias-bg-layer-3)}
146
+ .ig-vault-body{padding:8px 10px;display:flex;flex-direction:column;gap:4px;font-size:12px}
147
+ .ig-vault-prompt{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500;color:var(--dsw-alias-label-primary)}
148
+ .ig-vault-meta{font-size:11px;color:var(--dsw-alias-label-secondary);display:flex;justify-content:space-between}
149
+
150
+ .ig-studio-layout{display:grid;grid-template-columns:320px 1fr;gap:18px;min-height:550px}
151
+ @media (max-width: 800px){.ig-studio-layout{grid-template-columns:1fr}}
152
+ .ig-studio-sidebar{display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px}
153
+ .ig-studio-canvas{display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:12px;align-items:center;justify-content:center;position:relative}
154
+
122
155
  /* Backward-compatibility alias styles for toolview */
123
156
  .fal_head{font-size:13px;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-label-primary)}
124
157
  .ig-prompt{font-size:13px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2);border-radius:6px;border:1px solid var(--dsw-alias-border-l2);margin-bottom:8px;word-break:break-word}
@@ -126,8 +159,7 @@ window.__ModuleLoader__.load({
126
159
  .ig-err{padding:10px;border-radius:6px;background:var(--dsw-alias-state-error-bg, color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent));color:var(--dsw-alias-state-error-primary);font-size:12px}
127
160
  `
128
161
  document.head.appendChild(style)
129
- }
130
- function createErrorBoundary() {
162
+ } function createErrorBoundary() {
131
163
  if (!React || typeof React.Component !== 'function') {
132
164
  return function NoopBoundary(props) { return props?.children || null }
133
165
  }
@@ -1374,10 +1406,6 @@ window.__ModuleLoader__.load({
1374
1406
  const [responsiveTab, setResponsiveTab] = react.useState(0)
1375
1407
  const [copied, setCopied] = react.useState(false)
1376
1408
 
1377
- react.useEffect(() => {
1378
- ensureCss()
1379
- }, [])
1380
-
1381
1409
  let args = {}
1382
1410
  let prompt = ''
1383
1411
  try {
@@ -1388,6 +1416,24 @@ window.__ModuleLoader__.load({
1388
1416
  }
1389
1417
  } catch (_) { /* malformed tool args */ }
1390
1418
 
1419
+ const initialUrl = parsed.attachment ? attachmentImageUrl(parsed.attachment) : parsed.url
1420
+ const [revisions, setRevisions] = react.useState(() => initialUrl ? [{ url: initialUrl, attachment: parsed.attachment, seed: args.seed, timestamp: Date.now() }] : [])
1421
+ const [activeRev, setActiveRev] = react.useState(0)
1422
+
1423
+ react.useEffect(() => {
1424
+ ensureCss()
1425
+ }, [])
1426
+
1427
+ react.useEffect(() => {
1428
+ if (!initialUrl) return
1429
+ setRevisions((prev) => {
1430
+ if (prev.some((r) => r.url === initialUrl)) return prev
1431
+ const next = [...prev, { url: initialUrl, attachment: parsed.attachment, seed: args.seed, timestamp: Date.now() }]
1432
+ setActiveRev(next.length - 1)
1433
+ return next
1434
+ })
1435
+ }, [initialUrl, args.seed])
1436
+
1391
1437
  const sendActionPrompt = async (text) => {
1392
1438
  try {
1393
1439
  const sessions = props.sessions
@@ -1468,21 +1514,28 @@ window.__ModuleLoader__.load({
1468
1514
  [active.width && active.height ? `${active.width}×${active.height}` : null, active.path ? String(active.path) : null].filter(Boolean).join(' · ')
1469
1515
  ) : null
1470
1516
  )
1471
- } else if (!parsed.attachment && parsed.url) {
1472
- body.push(react.createElement('img', {
1473
- key: 'i',
1474
- src: parsed.url,
1517
+ } else {
1518
+ // Revision bar (#148)
1519
+ if (revisions.length > 1) {
1520
+ body.push(react.createElement(ImageRevisionBar, {
1521
+ key: 'revs',
1522
+ revisions,
1523
+ activeIndex: activeRev,
1524
+ onSelect: (idx) => setActiveRev(idx),
1525
+ t,
1526
+ }))
1527
+ }
1528
+ // Progressive preview (#147)
1529
+ const currentRev = revisions[activeRev] || {}
1530
+ const displaySrc = currentRev.url || initialUrl
1531
+ const draftSrc = block && (block.draftUrl || block.previewUrl || '')
1532
+ body.push(react.createElement(ProgressiveImagePreview, {
1533
+ key: 'prog-img',
1534
+ src: displaySrc,
1535
+ draftSrc,
1536
+ isRunning: running,
1475
1537
  alt: prompt || 'generated image',
1476
- loading: 'lazy',
1477
- style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
1478
- }))
1479
- } else if (parsed.attachment) {
1480
- body.push(react.createElement('img', {
1481
- key: 'i',
1482
- src: attachmentImageUrl(parsed.attachment),
1483
- alt: prompt || 'generated image',
1484
- loading: 'lazy',
1485
- style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
1538
+ t,
1486
1539
  }))
1487
1540
  }
1488
1541
 
@@ -1595,10 +1648,110 @@ window.__ModuleLoader__.load({
1595
1648
  remixDrawer,
1596
1649
  inpaintDrawer
1597
1650
  )
1598
- }
1651
+ } // -------------------------------------------------------------- Image Revision History (#148)
1652
+ function ImageRevisionBar(props) {
1653
+ const revisions = props.revisions
1654
+ const activeIndex = props.activeIndex
1655
+ const onSelect = props.onSelect
1656
+ const t = props.t || ((k) => k)
1599
1657
 
1600
- // -------------------------------------------------------------- Dictionaries
1601
- // 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285).
1658
+ if (!Array.isArray(revisions) || revisions.length <= 1) return null
1659
+
1660
+ const current = revisions[activeIndex] || {}
1661
+ const hasPrev = activeIndex > 0
1662
+ const hasNext = activeIndex < revisions.length - 1
1663
+
1664
+ return react.createElement(
1665
+ 'div',
1666
+ { className: 'ig-rev-bar', role: 'navigation', 'aria-label': t('card.revisions') || 'Image revisions' },
1667
+ react.createElement(
1668
+ 'div',
1669
+ { className: 'ig-rev-controls' },
1670
+ react.createElement(
1671
+ 'button',
1672
+ {
1673
+ type: 'button',
1674
+ className: 'ig-rev-btn',
1675
+ disabled: !hasPrev,
1676
+ onClick: () => onSelect(activeIndex - 1),
1677
+ title: t('card.prevRevision') || 'Previous revision',
1678
+ },
1679
+ '◀'
1680
+ ),
1681
+ react.createElement(
1682
+ 'span',
1683
+ { style: { fontWeight: '600', color: 'var(--dsw-alias-label-primary)' } },
1684
+ `v${activeIndex + 1} / ${revisions.length}`
1685
+ ),
1686
+ react.createElement(
1687
+ 'button',
1688
+ {
1689
+ type: 'button',
1690
+ className: 'ig-rev-btn',
1691
+ disabled: !hasNext,
1692
+ onClick: () => onSelect(activeIndex + 1),
1693
+ title: t('card.nextRevision') || 'Next revision',
1694
+ },
1695
+ '▶'
1696
+ )
1697
+ ),
1698
+ react.createElement(
1699
+ 'div',
1700
+ { className: 'ig-rev-badge' },
1701
+ current.seed !== undefined ? `seed: ${current.seed}` : (current.timestamp ? new Date(current.timestamp).toLocaleTimeString() : '')
1702
+ )
1703
+ )
1704
+ } // -------------------------------------------------------------- Progressive Draft Preview (#147)
1705
+ function ProgressiveImagePreview(props) {
1706
+ const src = props.src
1707
+ const draftSrc = props.draftSrc
1708
+ const isRunning = props.isRunning
1709
+ const alt = props.alt || 'Generated image'
1710
+ const t = props.t || ((k) => k)
1711
+
1712
+ const [loaded, setLoaded] = react.useState(false)
1713
+
1714
+ react.useEffect(() => {
1715
+ setLoaded(false)
1716
+ }, [src])
1717
+
1718
+ if (isRunning && !src && draftSrc) {
1719
+ return react.createElement(
1720
+ 'div',
1721
+ { className: 'ig-progressive-wrap' },
1722
+ react.createElement('img', {
1723
+ src: draftSrc,
1724
+ alt: alt + ' (preview)',
1725
+ className: 'ig-draft-img',
1726
+ }),
1727
+ react.createElement(
1728
+ 'div',
1729
+ { className: 'ig-draft-overlay' },
1730
+ '⚡ ' + (t('card.draftPreview') || 'Rendering draft...')
1731
+ )
1732
+ )
1733
+ }
1734
+
1735
+ if (!src && !draftSrc) return null
1736
+
1737
+ return react.createElement(
1738
+ 'div',
1739
+ { className: 'ig-progressive-wrap', style: { minHeight: 'auto' } },
1740
+ draftSrc && !loaded ? react.createElement('img', {
1741
+ src: draftSrc,
1742
+ alt: alt + ' (draft)',
1743
+ className: 'ig-draft-img',
1744
+ }) : null,
1745
+ src ? react.createElement('img', {
1746
+ src,
1747
+ alt,
1748
+ loading: 'lazy',
1749
+ className: 'ig-final-img',
1750
+ style: { opacity: loaded || !draftSrc ? 1 : 0.8 },
1751
+ onLoad: () => setLoaded(true),
1752
+ }) : null
1753
+ )
1754
+ }// 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285).
1602
1755
 
1603
1756
  function InpaintCanvasOverlay({ react, t, parsed, onClose }) {
1604
1757
  const canvasRef = react.useRef(null)
@@ -2030,7 +2183,109 @@ window.__ModuleLoader__.load({
2030
2183
  ) : null
2031
2184
  )
2032
2185
  }
2033
- const en = {
2186
+ // -------------------------------------------------------------- Theme Pair Toolview (#189)
2187
+ function ThemePairToolView(props) {
2188
+ const block = props.block
2189
+ const t = props.t || ((k) => k)
2190
+ const [copiedHtml, setCopiedHtml] = react.useState(false)
2191
+ const [copiedCss, setCopiedCss] = react.useState(false)
2192
+ const [themeTab, setThemeTab] = react.useState('light')
2193
+
2194
+ const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
2195
+ let args = {}
2196
+ try { if (raw) args = JSON.parse(raw) } catch (_) { /* invalid json */ }
2197
+
2198
+ let data = {}
2199
+ try {
2200
+ const out = block && (block.output || block.result || block.text || '')
2201
+ if (typeof out === 'object') data = out
2202
+ else if (typeof out === 'string') data = JSON.parse(out)
2203
+ } catch (_) { /* invalid json */ }
2204
+
2205
+ const light = data.light || {}
2206
+ const dark = data.dark || {}
2207
+ const htmlSnippet = data.html_snippet || ''
2208
+ const cssSnippet = data.css_snippet || ''
2209
+
2210
+ const copyCode = (text, isHtml) => {
2211
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
2212
+ navigator.clipboard.writeText(text)
2213
+ if (isHtml) {
2214
+ setCopiedHtml(true)
2215
+ setTimeout(() => setCopiedHtml(false), 2000)
2216
+ } else {
2217
+ setCopiedCss(true)
2218
+ setTimeout(() => setCopiedCss(false), 2000)
2219
+ }
2220
+ }
2221
+ }
2222
+
2223
+ const activeImg = themeTab === 'light' ? light : dark
2224
+
2225
+ return react.createElement(
2226
+ 'div',
2227
+ { className: 'ig-page', style: { padding: '4px 0', gap: '10px' } },
2228
+ react.createElement('div', { className: 'fal_head' }, '🌓 ' + (t('themePair.title') || 'Theme Pair Graphic')),
2229
+ args.prompt ? react.createElement('div', { className: 'ig-prompt' }, args.prompt) : null,
2230
+ react.createElement(
2231
+ 'div',
2232
+ { className: 'ig-tabs' },
2233
+ react.createElement(
2234
+ 'button',
2235
+ {
2236
+ type: 'button',
2237
+ className: themeTab === 'light' ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
2238
+ onClick: () => setThemeTab('light'),
2239
+ },
2240
+ '☀️ ' + (t('themePair.light') || 'Light Theme')
2241
+ ),
2242
+ react.createElement(
2243
+ 'button',
2244
+ {
2245
+ type: 'button',
2246
+ className: themeTab === 'dark' ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
2247
+ onClick: () => setThemeTab('dark'),
2248
+ },
2249
+ '🌙 ' + (t('themePair.dark') || 'Dark Theme')
2250
+ )
2251
+ ),
2252
+ activeImg.url || activeImg.path ? react.createElement('img', {
2253
+ src: activeImg.url || activeImg.path,
2254
+ alt: args.prompt || 'theme image',
2255
+ loading: 'lazy',
2256
+ style: {
2257
+ width: '100%',
2258
+ maxHeight: '400px',
2259
+ objectFit: 'contain',
2260
+ borderRadius: '8px',
2261
+ border: '1px solid var(--dsw-alias-border-l2)',
2262
+ background: themeTab === 'light' ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-3)',
2263
+ },
2264
+ }) : null,
2265
+ htmlSnippet ? react.createElement(
2266
+ 'div',
2267
+ { style: { display: 'flex', gap: '8px', marginTop: '4px' } },
2268
+ react.createElement(
2269
+ 'button',
2270
+ {
2271
+ type: 'button',
2272
+ className: 'ig-btn',
2273
+ onClick: () => copyCode(htmlSnippet, true),
2274
+ },
2275
+ copiedHtml ? '✓ ' + (t('themePair.copiedHtml') || 'Copied HTML') : '📋 ' + (t('themePair.copyHtml') || 'Copy <picture> HTML')
2276
+ ),
2277
+ cssSnippet ? react.createElement(
2278
+ 'button',
2279
+ {
2280
+ type: 'button',
2281
+ className: 'ig-btn',
2282
+ onClick: () => copyCode(cssSnippet, false),
2283
+ },
2284
+ copiedCss ? '✓ ' + (t('themePair.copiedCss') || 'Copied CSS') : '🎨 ' + (t('themePair.copyCss') || 'Copy Theme CSS')
2285
+ ) : null
2286
+ ) : null
2287
+ )
2288
+ } const en = {
2034
2289
  'settings.navLabel': 'Image Studio',
2035
2290
  'settings.title': 'Image Studio',
2036
2291
  'settings.description': 'AI Image generation, inpainting, variations, upscale & vectorization.',
@@ -2258,6 +2513,22 @@ window.__ModuleLoader__.load({
2258
2513
  'f.cacheTtlDays': 'Cache Retention (Legacy)',
2259
2514
  'f.cacheTtlDaysHint': 'Legacy name for retention period in days.',
2260
2515
  'p.cacheTtlDays': '0',
2516
+
2517
+ 'studio.title': 'Image Studio',
2518
+ 'studio.canvas': 'Studio Canvas',
2519
+ 'studio.vault': 'Asset Vault',
2520
+ 'studio.parameters': 'Generation Parameters',
2521
+ 'studio.generate': 'Generate in Chat',
2522
+ 'studio.recent': 'Recent Generations',
2523
+ 'vault.title': 'Asset Browser & Vault',
2524
+ 'vault.search': 'Search prompt or tags...',
2525
+ 'vault.loadMore': 'Load more assets',
2526
+ 'vault.empty': 'No assets found in vault.',
2527
+ 'vault.inspector': 'Asset Inspector',
2528
+ 'vault.reroll': 'Re-roll with seed',
2529
+ 'vault.insert': 'Insert in Chat',
2530
+ 'vault.download': 'Download',
2531
+ 'vault.delete': 'Delete',
2261
2532
  }
2262
2533
 
2263
2534
  const zh = {
@@ -2476,6 +2747,22 @@ window.__ModuleLoader__.load({
2476
2747
  'f.cacheTtlDays': '缓存生命周期 (Legacy)',
2477
2748
  'f.cacheTtlDaysHint': '已废弃的缓存保留时长参数。',
2478
2749
  'p.cacheTtlDays': '0',
2750
+
2751
+ 'studio.title': '图像创作工作台',
2752
+ 'studio.canvas': '创作画布',
2753
+ 'studio.vault': '素材资产库',
2754
+ 'studio.parameters': '生成参数配置',
2755
+ 'studio.generate': '在对话中生成',
2756
+ 'studio.recent': '最近生成历史',
2757
+ 'vault.title': '图像素材库与浏览器',
2758
+ 'vault.search': '搜索提示词或标签...',
2759
+ 'vault.loadMore': '加载更多素材',
2760
+ 'vault.empty': '素材库中暂无图像。',
2761
+ 'vault.inspector': '素材检查器',
2762
+ 'vault.reroll': '使用此种子重新生成',
2763
+ 'vault.insert': '插入到当前对话',
2764
+ 'vault.download': '下载图像',
2765
+ 'vault.delete': '删除',
2479
2766
  }
2480
2767
 
2481
2768
  // -------------------------------------------------------------- Registration & Apply
@@ -2682,6 +2969,18 @@ window.__ModuleLoader__.load({
2682
2969
  )
2683
2970
  )
2684
2971
 
2972
+ registerSlotWhenReady('tool.call.toolview', () =>
2973
+ ctx.slots.register(
2974
+ {
2975
+ name: 'tool.call.toolview',
2976
+ key: 'generate_theme_pair',
2977
+ locale: NS,
2978
+ inject: () => ({ sessions: ctx.sessions }),
2979
+ },
2980
+ (props) => react.createElement(ErrorBoundary, null, react.createElement(ThemePairView, props))
2981
+ )
2982
+ )
2983
+
2685
2984
 
2686
2985
  // Native Sidebar Right Pane Tab & BetterSidebar
2687
2986
  if (typeof ctx.inject === 'function') {
@@ -2691,16 +2990,16 @@ window.__ModuleLoader__.load({
2691
2990
  if (!tabs || typeof tabs.register !== 'function') return
2692
2991
  try {
2693
2992
  const def = {
2694
- id: '@goodandready/dsh-image-gen:gallery',
2695
- kind: 'image-gallery',
2993
+ id: '@goodandready/dsh-image-gen:studio',
2994
+ kind: 'image-studio',
2696
2995
  priority: 'extension',
2697
- title: () => 'Gallery',
2996
+ title: () => 'Image Studio',
2698
2997
  guide: [
2699
2998
  {
2700
2999
  order: 45,
2701
- title: () => 'Image Studio Gallery',
2702
- description: () => 'Browse and inspect recent image generations',
2703
- icon: () => react.createElement('span', null, '🖼️'),
3000
+ title: () => 'Image Studio & Vault',
3001
+ description: () => 'Interactive visual studio & asset browser',
3002
+ icon: () => react.createElement('span', null, '🎨'),
2704
3003
  },
2705
3004
  ],
2706
3005
  }
@@ -2720,7 +3019,7 @@ window.__ModuleLoader__.load({
2720
3019
  locale: NS,
2721
3020
  inject: () => ({ ctx }),
2722
3021
  },
2723
- (props) => react.createElement(ErrorBoundary, null, react.createElement(GalleryView, { ...props, ctx }))
3022
+ (props) => react.createElement(ErrorBoundary, null, react.createElement(ImageStudioView, { ...props, ctx }))
2724
3023
  )
2725
3024
  } catch (e) {
2726
3025
  console.warn('[dsh-image-gen] native sidebar pane tab register failed', e)
@@ -2748,7 +3047,7 @@ window.__ModuleLoader__.load({
2748
3047
  title: () => 'Gallery',
2749
3048
  icon: () => react.createElement('span', null, '🖼️'),
2750
3049
  order: 45,
2751
- component: ({ scope }) => react.createElement(ErrorBoundary, null, react.createElement(GalleryView, { ctx, scope })),
3050
+ component: ({ scope }) => react.createElement(ErrorBoundary, null, react.createElement(ImageStudioView, { ctx, scope })),
2752
3051
  })
2753
3052
  } catch (e) {
2754
3053
  console.warn('[dsh-image-gen] betterSidebar registerTab failed', e)
@@ -2771,10 +3070,7 @@ window.__ModuleLoader__.load({
2771
3070
  )
2772
3071
  )
2773
3072
 
2774
- // List seat (plugins.item): the seat the Plugins page renders as the plugin's own
2775
- // page with its configuration. The label is a static string on purpose — it is
2776
- // resolved while the page renders, and a locale lookup there would take the whole
2777
- // client batch down with it.
3073
+ // List seat (plugins.item)
2778
3074
  registerSlotWhenReady('plugins.item', () =>
2779
3075
  ctx.slots.register(
2780
3076
  {
@@ -2803,6 +3099,574 @@ window.__ModuleLoader__.load({
2803
3099
  )
2804
3100
  }
2805
3101
 
3102
+ // -------------------------------------------------------------- Asset Vault View (#159)
3103
+ function AssetVaultView(props) {
3104
+ const { ctx, onSelectAsset } = props
3105
+ const [items, setItems] = react.useState([])
3106
+ const [loading, setLoading] = react.useState(false)
3107
+ const [q, setQ] = react.useState('')
3108
+ const [provider, setProvider] = react.useState('all')
3109
+ const [aspect, setAspect] = react.useState('all')
3110
+ const [sort, setSort] = react.useState('newest')
3111
+ const [offset, setOffset] = react.useState(0)
3112
+ const [hasMore, setHasMore] = react.useState(false)
3113
+ const [selected, setSelected] = react.useState(null)
3114
+ const [feedback, setFeedback] = react.useState('')
3115
+
3116
+ const showFeedback = (msg) => {
3117
+ setFeedback(msg)
3118
+ setTimeout(() => setFeedback(''), 2500)
3119
+ }
3120
+
3121
+ const loadItems = (newOffset = 0, append = false) => {
3122
+ setLoading(true)
3123
+ const params = new URLSearchParams({
3124
+ q,
3125
+ provider: provider === 'all' ? '' : provider,
3126
+ aspect: aspect === 'all' ? '' : aspect,
3127
+ sort,
3128
+ offset: String(newOffset),
3129
+ limit: '24',
3130
+ })
3131
+ fetch('/dsh-image-gen/vault?' + params.toString())
3132
+ .then((r) => r.json())
3133
+ .then((data) => {
3134
+ setLoading(false)
3135
+ if (data && data.ok) {
3136
+ setItems(append ? (prev) => [...prev, ...data.items] : data.items)
3137
+ setOffset(data.offset + data.items.length)
3138
+ setHasMore(data.hasMore)
3139
+ }
3140
+ })
3141
+ .catch(() => setLoading(false))
3142
+ }
3143
+
3144
+ react.useEffect(() => {
3145
+ loadItems(0, false)
3146
+ }, [q, provider, aspect, sort])
3147
+
3148
+ const handleDelete = (id) => {
3149
+ if (!confirm('Are you sure you want to delete this asset?')) return
3150
+ fetch('/dsh-image-gen/vault?id=' + encodeURIComponent(id), { method: 'DELETE' })
3151
+ .then((r) => r.json())
3152
+ .then((res) => {
3153
+ if (res.ok) {
3154
+ setItems((prev) => prev.filter((it) => it.id !== id && it.attachmentId !== id))
3155
+ if (selected && (selected.id === id || selected.attachmentId === id)) {
3156
+ setSelected(null)
3157
+ }
3158
+ showFeedback('Asset deleted')
3159
+ }
3160
+ })
3161
+ .catch(() => showFeedback('Failed to delete asset'))
3162
+ }
3163
+
3164
+ const handleInsert = (item) => {
3165
+ const link = '![' + (item.prompt || 'Generated Image') + '](' + (item.url || item.thumbnailUrl) + ')'
3166
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
3167
+ navigator.clipboard.writeText(link)
3168
+ showFeedback('Markdown snippet copied to clipboard!')
3169
+ }
3170
+ }
3171
+
3172
+ const handleReroll = (item) => {
3173
+ if (typeof onSelectAsset === 'function') {
3174
+ onSelectAsset(item)
3175
+ return
3176
+ }
3177
+ const cmd = '/image ' + (item.prompt || '') + (item.seed !== undefined ? ' --seed ' + item.seed : '')
3178
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
3179
+ navigator.clipboard.writeText(cmd)
3180
+ showFeedback('Prompt & seed copied to clipboard!')
3181
+ }
3182
+ }
3183
+
3184
+ return react.createElement(
3185
+ 'div',
3186
+ { className: 'ig-vault-container', style: { display: 'flex', flexDirection: 'column', gap: '14px', width: '100%' } },
3187
+ react.createElement(
3188
+ 'div',
3189
+ { className: 'ig-vault-header' },
3190
+ react.createElement('input', {
3191
+ type: 'text',
3192
+ className: 'ig-input',
3193
+ style: { flex: '1 1 200px', padding: '6px 12px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
3194
+ placeholder: 'Search prompt or tags...',
3195
+ value: q,
3196
+ onChange: (e) => setQ(e.target.value),
3197
+ }),
3198
+ react.createElement(
3199
+ 'select',
3200
+ {
3201
+ className: 'ig-select',
3202
+ value: provider,
3203
+ onChange: (e) => setProvider(e.target.value),
3204
+ style: { padding: '6px 10px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
3205
+ },
3206
+ react.createElement('option', { value: 'all' }, 'All Providers'),
3207
+ react.createElement('option', { value: 'fal' }, 'FAL.ai'),
3208
+ react.createElement('option', { value: 'openai' }, 'OpenAI'),
3209
+ react.createElement('option', { value: 'local' }, 'Local (ComfyUI)')
3210
+ ),
3211
+ react.createElement(
3212
+ 'select',
3213
+ {
3214
+ className: 'ig-select',
3215
+ value: aspect,
3216
+ onChange: (e) => setAspect(e.target.value),
3217
+ style: { padding: '6px 10px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
3218
+ },
3219
+ react.createElement('option', { value: 'all' }, 'All Ratios'),
3220
+ react.createElement('option', { value: '1:1' }, '1:1 Square'),
3221
+ react.createElement('option', { value: '16:9' }, '16:9 Landscape'),
3222
+ react.createElement('option', { value: '9:16' }, '9:16 Portrait'),
3223
+ react.createElement('option', { value: '4:3' }, '4:3 Standard'),
3224
+ react.createElement('option', { value: '21:9' }, '21:9 Ultrawide')
3225
+ ),
3226
+ react.createElement(
3227
+ 'button',
3228
+ {
3229
+ type: 'button',
3230
+ className: 'ig-tab-btn',
3231
+ onClick: () => setSort(sort === 'newest' ? 'oldest' : 'newest'),
3232
+ style: { border: '1px solid var(--dsw-alias-border-l2)', padding: '6px 10px', borderRadius: '6px', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer' },
3233
+ },
3234
+ sort === 'newest' ? '↓ Newest' : '↑ Oldest'
3235
+ )
3236
+ ),
3237
+ feedback && react.createElement('div', { style: { padding: '6px 12px', borderRadius: '6px', background: 'color-mix(in srgb, var(--dsw-alias-state-brand-primary) 15%, transparent)', color: 'var(--dsw-alias-state-brand-primary)', fontSize: '12px' } }, feedback),
3238
+ items.length === 0 && !loading
3239
+ ? react.createElement('div', { style: { padding: '32px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, 'No assets found in vault.')
3240
+ : react.createElement(
3241
+ 'div',
3242
+ { className: 'ig-vault-grid' },
3243
+ items.map((item) =>
3244
+ react.createElement(
3245
+ 'div',
3246
+ { key: item.id || item.attachmentId, className: 'ig-vault-card' },
3247
+ react.createElement('img', {
3248
+ src: item.thumbnailUrl || item.url,
3249
+ className: 'ig-vault-thumb',
3250
+ loading: 'lazy',
3251
+ onClick: () => setSelected(item),
3252
+ }),
3253
+ react.createElement(
3254
+ 'div',
3255
+ { className: 'ig-vault-body' },
3256
+ react.createElement('div', { className: 'ig-vault-prompt', title: item.prompt }, item.prompt || 'Untitled'),
3257
+ react.createElement(
3258
+ 'div',
3259
+ { className: 'ig-vault-meta' },
3260
+ react.createElement('span', null, item.provider || 'local'),
3261
+ item.seed !== undefined && react.createElement('span', null, 'Seed ' + item.seed)
3262
+ ),
3263
+ react.createElement(
3264
+ 'div',
3265
+ { style: { display: 'flex', gap: '4px', marginTop: '6px' } },
3266
+ react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleReroll(item), title: 'Re-roll with seed' }, '🎲 Re-roll'),
3267
+ react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleInsert(item), title: 'Insert into chat' }, '💬 Insert'),
3268
+ react.createElement('button', { type: 'button', className: 'ig-rev-btn', onClick: () => handleDelete(item.id || item.attachmentId), title: 'Delete' }, '🗑️')
3269
+ )
3270
+ )
3271
+ )
3272
+ )
3273
+ ),
3274
+ hasMore &&
3275
+ react.createElement(
3276
+ 'div',
3277
+ { style: { textAlign: 'center', margin: '14px 0' } },
3278
+ react.createElement(
3279
+ 'button',
3280
+ {
3281
+ type: 'button',
3282
+ className: 'ig-tab-btn',
3283
+ onClick: () => loadItems(offset, true),
3284
+ disabled: loading,
3285
+ style: { padding: '8px 20px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer' },
3286
+ },
3287
+ loading ? 'Loading...' : 'Load more assets'
3288
+ )
3289
+ ),
3290
+ selected &&
3291
+ react.createElement(
3292
+ 'div',
3293
+ {
3294
+ style: { position: 'fixed', inset: 0, background: 'var(--dsw-alias-overlay-backdrop, color-mix(in srgb, var(--dsw-alias-bg-layer-0, black) 70%, transparent))', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px' },
3295
+ onClick: () => setSelected(null),
3296
+ },
3297
+ react.createElement(
3298
+ 'div',
3299
+ {
3300
+ style: { background: 'var(--dsw-alias-bg-layer-3)', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '12px', maxWidth: '580px', width: '100%', maxHeight: '90vh', overflowY: 'auto', padding: '18px', display: 'flex', flexDirection: 'column', gap: '12px' },
3301
+ onClick: (e) => e.stopPropagation(),
3302
+ },
3303
+ react.createElement(
3304
+ 'div',
3305
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3306
+ react.createElement('span', { style: { fontWeight: '700', fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } }, 'Asset Inspector'),
3307
+ react.createElement('button', { type: 'button', onClick: () => setSelected(null), style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px', color: 'var(--dsw-alias-label-secondary)' } }, '✕')
3308
+ ),
3309
+ react.createElement('img', {
3310
+ src: selected.url || selected.thumbnailUrl,
3311
+ style: { width: '100%', borderRadius: '8px', maxHeight: '360px', objectFit: 'contain', background: 'var(--dsw-alias-bg-layer-2)' },
3312
+ }),
3313
+ react.createElement(
3314
+ 'div',
3315
+ { style: { display: 'flex', flexWrap: 'wrap', gap: '6px', fontSize: '11px' } },
3316
+ selected.seed !== undefined && react.createElement('span', { className: 'ig-badge' }, 'Seed: ' + selected.seed),
3317
+ selected.provider && react.createElement('span', { className: 'ig-badge' }, 'Provider: ' + selected.provider),
3318
+ selected.width && react.createElement('span', { className: 'ig-badge' }, selected.width + '×' + selected.height),
3319
+ selected.cost ? react.createElement('span', { className: 'ig-badge' }, '$' + Number(selected.cost).toFixed(4)) : null
3320
+ ),
3321
+ react.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-layer-2)', padding: '10px', borderRadius: '6px', lineHeight: '1.4' } }, selected.prompt || 'No prompt'),
3322
+ react.createElement(
3323
+ 'div',
3324
+ { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '6px' } },
3325
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleReroll(selected) }, '🎲 Re-roll'),
3326
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleInsert(selected) }, '💬 Insert in Chat'),
3327
+ react.createElement(
3328
+ 'a',
3329
+ { href: selected.url || selected.thumbnailUrl, download: 'vault-asset-' + (selected.id || 'image') + '.png', className: 'ig-tab-btn', style: { textDecoration: 'none', display: 'inline-flex', alignItems: 'center' } },
3330
+ '⬇ Download'
3331
+ ),
3332
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', style: { color: 'var(--dsw-alias-state-error-primary)' }, onClick: () => handleDelete(selected.id || selected.attachmentId) }, '🗑️ Delete')
3333
+ )
3334
+ )
3335
+ )
3336
+ )
3337
+ }
3338
+ // -------------------------------------------------------------- Fullscreen Image Studio (#158)
3339
+ function ImageStudioView(props) {
3340
+ const { ctx } = props
3341
+ const [viewMode, setViewMode] = react.useState('studio') // 'studio' | 'vault'
3342
+ const [isFullscreen, setIsFullscreen] = react.useState(false)
3343
+ const [layoutGrid, setLayoutGrid] = react.useState('1x1') // '1x1' | '2x2' | '1x4'
3344
+ const [prompt, setPrompt] = react.useState(() => {
3345
+ try { return localStorage.getItem('dsh_studio_prompt') || '' } catch (_) { return '' }
3346
+ })
3347
+ const [stylePreset, setStylePreset] = react.useState('none')
3348
+ const [aspectRatio, setAspectRatio] = react.useState('1:1')
3349
+ const [provider, setProvider] = react.useState('fal')
3350
+ const [seed, setSeed] = react.useState('')
3351
+ const [activeImages, setActiveImages] = react.useState([])
3352
+ const [recentStrip, setRecentStrip] = react.useState([])
3353
+ const [status, setStatus] = react.useState('')
3354
+
3355
+ const presets = [
3356
+ { id: 'none', label: 'Default' },
3357
+ { id: 'photorealistic', label: 'Photo' },
3358
+ { id: 'anime', label: 'Anime' },
3359
+ { id: 'cyberpunk', label: 'Cyberpunk' },
3360
+ { id: 'flat_vector', label: 'Vector' },
3361
+ { id: '3d_render', label: '3D Render' },
3362
+ ]
3363
+
3364
+ const refreshRecent = () => {
3365
+ fetch('/dsh-image-gen/vault?limit=12')
3366
+ .then((r) => r.json())
3367
+ .then((data) => {
3368
+ if (data && data.ok && Array.isArray(data.items)) {
3369
+ setRecentStrip(data.items)
3370
+ if (activeImages.length === 0 && data.items.length > 0) {
3371
+ setActiveImages([data.items[0]])
3372
+ }
3373
+ }
3374
+ })
3375
+ .catch(() => {})
3376
+ }
3377
+
3378
+ react.useEffect(() => {
3379
+ refreshRecent()
3380
+ }, [])
3381
+
3382
+ const updatePrompt = (val) => {
3383
+ setPrompt(val)
3384
+ try { localStorage.setItem('dsh_studio_prompt', val) } catch (_) { /* storage unavailable */ }
3385
+ }
3386
+
3387
+ const handleSelectFromVault = (asset) => {
3388
+ if (!asset) return
3389
+ setActiveImages([asset])
3390
+ if (asset.prompt) updatePrompt(asset.prompt)
3391
+ if (asset.seed !== undefined) setSeed(String(asset.seed))
3392
+ if (asset.provider) setProvider(asset.provider)
3393
+ setViewMode('studio')
3394
+ }
3395
+
3396
+ const handleCopySnippet = (format) => {
3397
+ const first = activeImages[0]
3398
+ if (!first) return
3399
+ const url = first.url || first.thumbnailUrl
3400
+ let snippet = ''
3401
+ if (format === 'markdown') snippet = '![' + (first.prompt || 'Artwork') + '](' + url + ')'
3402
+ if (format === 'html') snippet = '<img src="' + url + '" alt="' + (first.prompt || 'Artwork') + '" />'
3403
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
3404
+ navigator.clipboard.writeText(snippet)
3405
+ setStatus('Copied ' + format.toUpperCase() + ' snippet!')
3406
+ setTimeout(() => setStatus(''), 2000)
3407
+ }
3408
+ }
3409
+
3410
+ const containerStyle = isFullscreen
3411
+ ? { position: 'fixed', inset: 0, zIndex: 99999, background: 'var(--dsw-alias-bg-layer-1)', padding: '20px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '14px' }
3412
+ : { display: 'flex', flexDirection: 'column', gap: '14px', width: '100%' }
3413
+
3414
+ return react.createElement(
3415
+ 'div',
3416
+ { className: 'ig-studio-container', style: containerStyle },
3417
+ react.createElement(
3418
+ 'div',
3419
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '10px', borderBottom: '1px solid var(--dsw-alias-border-l2)', paddingBottom: '10px' } },
3420
+ react.createElement(
3421
+ 'div',
3422
+ { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
3423
+ react.createElement('span', { style: { fontWeight: '700', fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, '🎨 Image Studio'),
3424
+ react.createElement(
3425
+ 'button',
3426
+ {
3427
+ type: 'button',
3428
+ className: 'ig-tab-btn ' + (viewMode === 'studio' ? 'active' : ''),
3429
+ onClick: () => setViewMode('studio'),
3430
+ },
3431
+ 'Studio Canvas'
3432
+ ),
3433
+ react.createElement(
3434
+ 'button',
3435
+ {
3436
+ type: 'button',
3437
+ className: 'ig-tab-btn ' + (viewMode === 'vault' ? 'active' : ''),
3438
+ onClick: () => setViewMode('vault'),
3439
+ },
3440
+ '🗃️ Asset Vault'
3441
+ )
3442
+ ),
3443
+ react.createElement(
3444
+ 'div',
3445
+ { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
3446
+ viewMode === 'studio' &&
3447
+ react.createElement(
3448
+ 'div',
3449
+ { style: { display: 'flex', gap: '4px' } },
3450
+ ['1x1', '2x2', '1x4'].map((grid) =>
3451
+ react.createElement(
3452
+ 'button',
3453
+ {
3454
+ key: grid,
3455
+ type: 'button',
3456
+ className: 'ig-tab-btn ' + (layoutGrid === grid ? 'active' : ''),
3457
+ onClick: () => setLayoutGrid(grid),
3458
+ title: 'Grid layout: ' + grid,
3459
+ },
3460
+ grid
3461
+ )
3462
+ )
3463
+ ),
3464
+ react.createElement(
3465
+ 'button',
3466
+ {
3467
+ type: 'button',
3468
+ className: 'ig-tab-btn',
3469
+ onClick: () => setIsFullscreen(!isFullscreen),
3470
+ style: { border: '1px solid var(--dsw-alias-border-l2)' },
3471
+ },
3472
+ isFullscreen ? '✕ Exit Fullscreen' : '⛶ Fullscreen'
3473
+ )
3474
+ )
3475
+ ),
3476
+ viewMode === 'vault'
3477
+ ? react.createElement(AssetVaultView, { ctx, onSelectAsset: handleSelectFromVault })
3478
+ : react.createElement(
3479
+ react.Fragment,
3480
+ null,
3481
+ react.createElement(
3482
+ 'div',
3483
+ { className: 'ig-studio-layout' },
3484
+ react.createElement(
3485
+ 'div',
3486
+ { className: 'ig-studio-sidebar' },
3487
+ react.createElement('span', { style: { fontWeight: '600', fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, 'Generation Parameters'),
3488
+ react.createElement(
3489
+ 'div',
3490
+ null,
3491
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Prompt:'),
3492
+ react.createElement('textarea', {
3493
+ className: 'ig-input',
3494
+ rows: 4,
3495
+ style: { width: '100%', resize: 'vertical', borderRadius: '6px', padding: '8px', fontSize: '12px' },
3496
+ placeholder: 'Describe your vision...',
3497
+ value: prompt,
3498
+ onChange: (e) => updatePrompt(e.target.value),
3499
+ })
3500
+ ),
3501
+ react.createElement(
3502
+ 'div',
3503
+ null,
3504
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Style Preset:'),
3505
+ react.createElement(
3506
+ 'div',
3507
+ { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
3508
+ presets.map((p) =>
3509
+ react.createElement(
3510
+ 'button',
3511
+ {
3512
+ key: p.id,
3513
+ type: 'button',
3514
+ className: 'ig-tab-btn ' + (stylePreset === p.id ? 'active' : ''),
3515
+ style: { fontSize: '11px', padding: '3px 8px' },
3516
+ onClick: () => setStylePreset(p.id),
3517
+ },
3518
+ p.label
3519
+ )
3520
+ )
3521
+ )
3522
+ ),
3523
+ react.createElement(
3524
+ 'div',
3525
+ { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' } },
3526
+ react.createElement(
3527
+ 'div',
3528
+ null,
3529
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Aspect Ratio:'),
3530
+ react.createElement(
3531
+ 'select',
3532
+ {
3533
+ className: 'ig-select',
3534
+ style: { width: '100%', padding: '6px', fontSize: '12px' },
3535
+ value: aspectRatio,
3536
+ onChange: (e) => setAspectRatio(e.target.value),
3537
+ },
3538
+ react.createElement('option', { value: '1:1' }, '1:1 Square'),
3539
+ react.createElement('option', { value: '16:9' }, '16:9 Landscape'),
3540
+ react.createElement('option', { value: '9:16' }, '9:16 Portrait'),
3541
+ react.createElement('option', { value: '4:3' }, '4:3 Standard'),
3542
+ react.createElement('option', { value: '21:9' }, '21:9 Ultrawide')
3543
+ )
3544
+ ),
3545
+ react.createElement(
3546
+ 'div',
3547
+ null,
3548
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Provider:'),
3549
+ react.createElement(
3550
+ 'select',
3551
+ {
3552
+ className: 'ig-select',
3553
+ style: { width: '100%', padding: '6px', fontSize: '12px' },
3554
+ value: provider,
3555
+ onChange: (e) => setProvider(e.target.value),
3556
+ },
3557
+ react.createElement('option', { value: 'fal' }, 'FAL.ai'),
3558
+ react.createElement('option', { value: 'openai' }, 'OpenAI'),
3559
+ react.createElement('option', { value: 'local' }, 'Local (ComfyUI)')
3560
+ )
3561
+ )
3562
+ ),
3563
+ react.createElement(
3564
+ 'div',
3565
+ null,
3566
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Seed (optional):'),
3567
+ react.createElement(
3568
+ 'div',
3569
+ { style: { display: 'flex', gap: '6px' } },
3570
+ react.createElement('input', {
3571
+ type: 'number',
3572
+ className: 'ig-input',
3573
+ style: { flex: 1, padding: '6px', fontSize: '12px' },
3574
+ placeholder: 'Random',
3575
+ value: seed,
3576
+ onChange: (e) => setSeed(e.target.value),
3577
+ }),
3578
+ react.createElement(
3579
+ 'button',
3580
+ {
3581
+ type: 'button',
3582
+ className: 'ig-tab-btn',
3583
+ onClick: () => setSeed(String(Math.floor(Math.random() * 2147483647))),
3584
+ title: 'Randomize seed',
3585
+ },
3586
+ '🎲'
3587
+ )
3588
+ )
3589
+ ),
3590
+ react.createElement(
3591
+ 'button',
3592
+ {
3593
+ type: 'button',
3594
+ className: 'ig-save-btn',
3595
+ style: { marginTop: '8px', padding: '10px' },
3596
+ onClick: () => {
3597
+ const cmd = '/image ' + (prompt || 'Artwork') + (stylePreset !== 'none' ? ' --style ' + stylePreset : '') + ' --aspect ' + aspectRatio + ' --provider ' + provider + (seed ? ' --seed ' + seed : '')
3598
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
3599
+ navigator.clipboard.writeText(cmd)
3600
+ setStatus('Copied generation command to clipboard!')
3601
+ setTimeout(() => setStatus(''), 2500)
3602
+ }
3603
+ },
3604
+ },
3605
+ '🎨 Generate in Chat'
3606
+ ),
3607
+ status && react.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-state-brand-primary)', textAlign: 'center' } }, status)
3608
+ ),
3609
+ react.createElement(
3610
+ 'div',
3611
+ { className: 'ig-studio-canvas' },
3612
+ activeImages.length === 0
3613
+ ? react.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, 'Select an image from the recent strip below or click Generate.')
3614
+ : react.createElement(
3615
+ 'div',
3616
+ {
3617
+ style: {
3618
+ display: 'grid',
3619
+ gridTemplateColumns: layoutGrid === '2x2' ? 'repeat(2, 1fr)' : layoutGrid === '1x4' ? 'repeat(4, 1fr)' : '1fr',
3620
+ gap: '12px',
3621
+ width: '100%',
3622
+ maxHeight: '440px',
3623
+ overflowY: 'auto',
3624
+ },
3625
+ },
3626
+ (layoutGrid === '1x1' ? activeImages.slice(0, 1) : activeImages.slice(0, layoutGrid === '2x2' ? 4 : 4)).map((img, i) =>
3627
+ react.createElement('img', {
3628
+ key: img.id || i,
3629
+ src: img.url || img.thumbnailUrl,
3630
+ style: { width: '100%', height: 'auto', maxHeight: '400px', objectFit: 'contain', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-3)' },
3631
+ })
3632
+ )
3633
+ ),
3634
+ activeImages.length > 0 &&
3635
+ react.createElement(
3636
+ 'div',
3637
+ { style: { display: 'flex', gap: '8px', marginTop: '10px' } },
3638
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('markdown') }, 'Copy Markdown'),
3639
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('html') }, 'Copy HTML'),
3640
+ react.createElement(
3641
+ 'a',
3642
+ { href: activeImages[0].url || activeImages[0].thumbnailUrl, download: 'studio-image.png', className: 'ig-tab-btn', style: { textDecoration: 'none' } },
3643
+ '⬇ Download'
3644
+ )
3645
+ )
3646
+ )
3647
+ ),
3648
+ react.createElement(
3649
+ 'div',
3650
+ { style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '10px' } },
3651
+ react.createElement('span', { style: { fontSize: '12px', fontWeight: '600', color: 'var(--dsw-alias-label-secondary)' } }, 'Recent Generations:'),
3652
+ react.createElement(
3653
+ 'div',
3654
+ { className: 'ig-studio-strip', style: { display: 'flex', gap: '8px', overflowX: 'auto', padding: '4px 0' } },
3655
+ recentStrip.map((item) =>
3656
+ react.createElement('img', {
3657
+ key: item.id || item.attachmentId,
3658
+ src: item.thumbnailUrl || item.url,
3659
+ className: 'ig-studio-strip-thumb',
3660
+ style: { width: '64px', height: '64px', borderRadius: '6px', objectFit: 'cover', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2)', flexShrink: 0 },
3661
+ title: item.prompt || 'Artwork',
3662
+ onClick: () => handleSelectFromVault(item),
3663
+ })
3664
+ )
3665
+ )
3666
+ )
3667
+ )
3668
+ )
3669
+ }
2806
3670
  exports.apply = apply
2807
3671
  exports.inject = inject
2808
3672
  return module.exports