@goodandready/dsh-image-gen 0.10.30 → 0.10.32

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
@@ -112,6 +112,12 @@ window.__ModuleLoader__.load({
112
112
  .ig-alert-err{padding:10px 14px;border-radius:8px;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:13px}
113
113
  .ig-banner-warning{padding:12px 16px;border-radius:8px;background:var(--dsw-alias-state-warning-bg, color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent));border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
114
114
 
115
+ .ig-inpaint-box{position:relative;margin-top:10px;padding:12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}
116
+ .ig-matrix-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:10px;margin-top:8px}
117
+ .ig-matrix-cell{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column}
118
+ .ig-matrix-img{width:100%;aspect-ratio:1/1;object-fit:cover;cursor:pointer}
119
+ .ig-matrix-bar{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;font-size:11px}
120
+
115
121
  /* Backward-compatibility alias styles for toolview */
116
122
  .fal_head{font-size:13px;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-label-primary)}
117
123
  .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}
@@ -120,7 +126,6 @@ window.__ModuleLoader__.load({
120
126
  `
121
127
  document.head.appendChild(style)
122
128
  }
123
-
124
129
  function createErrorBoundary() {
125
130
  if (!React || typeof React.Component !== 'function') {
126
131
  return function NoopBoundary(props) { return props?.children || null }
@@ -273,6 +278,7 @@ window.__ModuleLoader__.load({
273
278
 
274
279
  // Provider: Codex / Grok
275
280
  { field: 'subscriptionQuality', spec: selectField('subscriptionQuality', ['', 'low', 'medium', 'high']), kind: 'select', options: ['', 'low', 'medium', 'high'], labelKey: 'f.subscriptionQuality', hintKey: 'f.subscriptionQualityHint', when: ['codex', 'grok'], tab: 'provider' },
281
+ { field: 'fallbackProviders', spec: textField('fallbackProviders'), kind: 'text', labelKey: 'f.fallbackProviders', hintKey: 'f.fallbackProvidersHint', placeholderKey: 'p.fallbackProviders', tab: 'provider' },
276
282
 
277
283
  // Enhancer & Style
278
284
  { field: 'autoEnhancePrompt', spec: booleanField('autoEnhancePrompt'), kind: 'select', options: ['true', 'false'], labelKey: 'f.autoEnhancePrompt', hintKey: 'f.autoEnhancePromptHint', tab: 'enhancer' },
@@ -1332,14 +1338,8 @@ window.__ModuleLoader__.load({
1332
1338
  const attachment = item.attachment || null
1333
1339
  const url = attachmentImageUrl(attachment) || item.url || ''
1334
1340
  const rawDevice = String(item.device || '').toLowerCase()
1335
- const device = ['mobile', 'tablet', 'desktop'].includes(rawDevice)
1336
- ? rawDevice
1337
- : `device-${index + 1}`
1338
- const label = item.label
1339
- || (device === 'mobile' ? 'Mobile'
1340
- : device === 'tablet' ? 'Tablet'
1341
- : device === 'desktop' ? 'Desktop'
1342
- : `#${index + 1}`)
1341
+ const device = ['mobile', 'tablet', 'desktop'].includes(rawDevice) ? rawDevice : `device-${index + 1}`
1342
+ const label = item.label || (device === 'mobile' ? 'Mobile' : device === 'tablet' ? 'Tablet' : device === 'desktop' ? 'Desktop' : `#${index + 1}`)
1343
1343
  return {
1344
1344
  device,
1345
1345
  label,
@@ -1366,10 +1366,12 @@ window.__ModuleLoader__.load({
1366
1366
  const responsiveImages = !failed && !running ? readResponsiveImages(block) : []
1367
1367
  const t = props.t || ((k) => k)
1368
1368
  const [remixOpen, setRemixOpen] = react.useState(false)
1369
+ const [inpaintOpen, setInpaintOpen] = react.useState(false)
1369
1370
  const [remixStrength, setRemixStrength] = react.useState(0.45)
1370
1371
  const [remixPrompt, setRemixPrompt] = react.useState('')
1371
1372
  const [copiedRemix, setCopiedRemix] = react.useState(false)
1372
1373
  const [responsiveTab, setResponsiveTab] = react.useState(0)
1374
+ const [copied, setCopied] = react.useState(false)
1373
1375
 
1374
1376
  react.useEffect(() => {
1375
1377
  ensureCss()
@@ -1383,9 +1385,7 @@ window.__ModuleLoader__.load({
1383
1385
  args = JSON.parse(raw)
1384
1386
  prompt = args.prompt || ''
1385
1387
  }
1386
- } catch (_) { /* malformed tool args; use empty prompt */ }
1387
-
1388
- const [copied, setCopied] = react.useState(false)
1388
+ } catch (_) { /* malformed tool args */ }
1389
1389
 
1390
1390
  const sendActionPrompt = async (text) => {
1391
1391
  try {
@@ -1394,7 +1394,7 @@ window.__ModuleLoader__.load({
1394
1394
  const binding = current && sessions.binding(current)
1395
1395
  const session = binding && binding.session
1396
1396
  if (session && session.prompt) await session.prompt([{ type: 'text', text }], 'queue')
1397
- } catch (_) { /* session prompt delivery failed; non-fatal */ }
1397
+ } catch (_) { /* session prompt delivery failed */ }
1398
1398
  }
1399
1399
 
1400
1400
  const onCopyPrompt = () => {
@@ -1438,12 +1438,7 @@ window.__ModuleLoader__.load({
1438
1438
  body.push(
1439
1439
  react.createElement(
1440
1440
  'div',
1441
- {
1442
- className: 'ig-tabs',
1443
- role: 'tablist',
1444
- 'aria-label': t('card.responsiveTabs') || 'Responsive viewports',
1445
- key: 'rtabs',
1446
- },
1441
+ { className: 'ig-tabs', role: 'tablist', 'aria-label': t('card.responsiveTabs') || 'Responsive viewports', key: 'rtabs' },
1447
1442
  responsiveImages.map((img, idx) =>
1448
1443
  react.createElement(
1449
1444
  'button',
@@ -1459,36 +1454,18 @@ window.__ModuleLoader__.load({
1459
1454
  )
1460
1455
  )
1461
1456
  ),
1462
- active && active.url
1463
- ? react.createElement('img', {
1464
- key: 'ri',
1465
- src: active.url,
1466
- alt: prompt || (active.label + ' responsive mockup'),
1467
- loading: 'lazy',
1468
- style: {
1469
- maxWidth: '100%',
1470
- borderRadius: '8px',
1471
- border: '1px solid var(--dsw-alias-border-l2)',
1472
- },
1473
- })
1474
- : null,
1475
- active
1476
- ? react.createElement(
1477
- 'div',
1478
- {
1479
- key: 'rmeta',
1480
- style: {
1481
- marginTop: '6px',
1482
- fontSize: '11px',
1483
- color: 'var(--dsw-alias-label-secondary)',
1484
- },
1485
- },
1486
- [
1487
- active.width && active.height ? `${active.width}×${active.height}` : null,
1488
- active.path ? String(active.path) : null,
1489
- ].filter(Boolean).join(' · ')
1490
- )
1491
- : null
1457
+ active && active.url ? react.createElement('img', {
1458
+ key: 'ri',
1459
+ src: active.url,
1460
+ alt: prompt || (active.label + ' responsive mockup'),
1461
+ loading: 'lazy',
1462
+ style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
1463
+ }) : null,
1464
+ active ? react.createElement(
1465
+ 'div',
1466
+ { key: 'rmeta', style: { marginTop: '6px', fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } },
1467
+ [active.width && active.height ? `${active.width}×${active.height}` : null, active.path ? String(active.path) : null].filter(Boolean).join(' · ')
1468
+ ) : null
1492
1469
  )
1493
1470
  } else if (!parsed.attachment && parsed.url) {
1494
1471
  body.push(react.createElement('img', {
@@ -1499,10 +1476,9 @@ window.__ModuleLoader__.load({
1499
1476
  style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
1500
1477
  }))
1501
1478
  } else if (parsed.attachment) {
1502
- const a = parsed.attachment
1503
1479
  body.push(react.createElement('img', {
1504
1480
  key: 'i',
1505
- src: attachmentImageUrl(a),
1481
+ src: attachmentImageUrl(parsed.attachment),
1506
1482
  alt: prompt || 'generated image',
1507
1483
  loading: 'lazy',
1508
1484
  style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
@@ -1512,6 +1488,7 @@ window.__ModuleLoader__.load({
1512
1488
  const actions = []
1513
1489
  if (!running && !failed && (parsed.url || parsed.attachment || responsiveImages.length)) {
1514
1490
  actions.push(
1491
+ react.createElement('button', { key: 'inpaint', type: 'button', className: 'ig-btn', onClick: () => setInpaintOpen(!inpaintOpen) }, '🖌️ ' + (t('card.inpaint') || 'Inpaint')),
1515
1492
  react.createElement('button', { key: 'remix', type: 'button', className: 'ig-btn', onClick: () => setRemixOpen(!remixOpen) }, '⚡ ' + (t('card.remix') || 'Remix')),
1516
1493
  react.createElement('button', { key: 'reroll', type: 'button', className: 'ig-btn', onClick: onReroll }, '🎲 ' + t('card.reroll')),
1517
1494
  react.createElement('button', { key: 'upscale', type: 'button', className: 'ig-btn', onClick: onUpscale }, '🔍 ' + t('card.upscale')),
@@ -1601,17 +1578,403 @@ window.__ModuleLoader__.load({
1601
1578
  )
1602
1579
  ) : null
1603
1580
 
1581
+ const inpaintDrawer = inpaintOpen ? react.createElement(InpaintCanvasOverlay, {
1582
+ react,
1583
+ t,
1584
+ parsed,
1585
+ onClose: () => setInpaintOpen(false),
1586
+ }) : null
1587
+
1604
1588
  return react.createElement(
1605
1589
  'div',
1606
1590
  { className: 'ig-section-card', style: { margin: '8px 0' } },
1607
1591
  head,
1608
1592
  body,
1609
1593
  actions.length > 0 ? react.createElement('div', { className: 'ig-row', style: { marginTop: '8px' } }, actions) : null,
1610
- remixDrawer
1594
+ remixDrawer,
1595
+ inpaintDrawer
1611
1596
  )
1612
1597
  }
1613
1598
 
1614
1599
  // -------------------------------------------------------------- Dictionaries
1600
+ // 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285).
1601
+
1602
+ function InpaintCanvasOverlay({ react, t, parsed, onClose }) {
1603
+ const canvasRef = react.useRef(null)
1604
+ const isDrawingRef = react.useRef(false)
1605
+ const [brushSize, setBrushSize] = react.useState(24)
1606
+ const [inpaintPrompt, setInpaintPrompt] = react.useState('')
1607
+ const [copied, setCopied] = react.useState(false)
1608
+ const [hasStrokes, setHasStrokes] = react.useState(false)
1609
+ const undoStackRef = react.useRef([])
1610
+
1611
+ const imgUrl = (parsed && (parsed.url || (parsed.attachment && attachmentImageUrl(parsed.attachment)))) || ''
1612
+ const targetRef = (parsed && (parsed.attachment?.attachmentId || parsed.path || parsed.url)) || 'current image'
1613
+
1614
+ const initCanvas = react.useCallback(() => {
1615
+ const canvas = canvasRef.current
1616
+ if (!canvas) return
1617
+ const ctx = canvas.getContext('2d')
1618
+ if (!ctx) return
1619
+ ctx.fillStyle = '#000000'
1620
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
1621
+ undoStackRef.current = [ctx.getImageData(0, 0, canvas.width, canvas.height)]
1622
+ }, [])
1623
+
1624
+ react.useEffect(() => {
1625
+ initCanvas()
1626
+ }, [initCanvas])
1627
+
1628
+ function getPos(e) {
1629
+ const canvas = canvasRef.current
1630
+ if (!canvas) return { x: 0, y: 0 }
1631
+ const rect = canvas.getBoundingClientRect()
1632
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX
1633
+ const clientY = e.touches ? e.touches[0].clientY : e.clientY
1634
+ const scaleX = canvas.width / rect.width
1635
+ const scaleY = canvas.height / rect.height
1636
+ return {
1637
+ x: (clientX - rect.left) * scaleX,
1638
+ y: (clientY - rect.top) * scaleY,
1639
+ }
1640
+ }
1641
+
1642
+ function startDraw(e) {
1643
+ if (e.touches && e.touches.length > 1) return
1644
+ e.preventDefault()
1645
+ const canvas = canvasRef.current
1646
+ if (!canvas) return
1647
+ const ctx = canvas.getContext('2d')
1648
+ if (!ctx) return
1649
+
1650
+ isDrawingRef.current = true
1651
+ const pos = getPos(e)
1652
+ ctx.lineWidth = brushSize
1653
+ ctx.lineCap = 'round'
1654
+ ctx.lineJoin = 'round'
1655
+ ctx.strokeStyle = '#ffffff'
1656
+ ctx.fillStyle = '#ffffff'
1657
+
1658
+ ctx.beginPath()
1659
+ ctx.arc(pos.x, pos.y, brushSize / 2, 0, Math.PI * 2)
1660
+ ctx.fill()
1661
+ ctx.beginPath()
1662
+ ctx.moveTo(pos.x, pos.y)
1663
+ }
1664
+
1665
+ function draw(e) {
1666
+ if (!isDrawingRef.current) return
1667
+ if (e.touches && e.touches.length > 1) return
1668
+ e.preventDefault()
1669
+ const canvas = canvasRef.current
1670
+ if (!canvas) return
1671
+ const ctx = canvas.getContext('2d')
1672
+ if (!ctx) return
1673
+
1674
+ const pos = getPos(e)
1675
+ ctx.lineWidth = brushSize
1676
+ ctx.lineCap = 'round'
1677
+ ctx.lineJoin = 'round'
1678
+ ctx.strokeStyle = '#ffffff'
1679
+ ctx.lineTo(pos.x, pos.y)
1680
+ ctx.stroke()
1681
+ setHasStrokes(true)
1682
+ }
1683
+
1684
+ function stopDraw(e) {
1685
+ if (!isDrawingRef.current) return
1686
+ isDrawingRef.current = false
1687
+ const canvas = canvasRef.current
1688
+ if (!canvas) return
1689
+ const ctx = canvas.getContext('2d')
1690
+ if (!ctx) return
1691
+ ctx.closePath()
1692
+
1693
+ if (undoStackRef.current.length >= 15) {
1694
+ undoStackRef.current.shift()
1695
+ }
1696
+ undoStackRef.current.push(ctx.getImageData(0, 0, canvas.width, canvas.height))
1697
+ }
1698
+
1699
+ function handleUndo() {
1700
+ const canvas = canvasRef.current
1701
+ if (!canvas) return
1702
+ const ctx = canvas.getContext('2d')
1703
+ if (!ctx || undoStackRef.current.length <= 1) return
1704
+ undoStackRef.current.pop()
1705
+ const prev = undoStackRef.current[undoStackRef.current.length - 1]
1706
+ if (prev) {
1707
+ ctx.putImageData(prev, 0, 0)
1708
+ }
1709
+ if (undoStackRef.current.length <= 1) {
1710
+ setHasStrokes(false)
1711
+ }
1712
+ }
1713
+
1714
+ function handleClear() {
1715
+ initCanvas()
1716
+ setHasStrokes(false)
1717
+ }
1718
+
1719
+ function handleApply() {
1720
+ const canvas = canvasRef.current
1721
+ if (!canvas) return
1722
+ const maskDataUrl = canvas.toDataURL('image/png')
1723
+ const promptText = inpaintPrompt.trim() || 'inpaint masked region'
1724
+ const instruction = 'Use edit_image with prompt: "' + promptText + '", source_image: "' + targetRef + '", mask: "' + maskDataUrl + '"'
1725
+
1726
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
1727
+ navigator.clipboard.writeText(instruction)
1728
+ setCopied(true)
1729
+ setTimeout(() => setCopied(false), 2500)
1730
+ }
1731
+ }
1732
+
1733
+ return react.createElement(
1734
+ 'div',
1735
+ { className: 'ig-inpaint-box' },
1736
+ react.createElement(
1737
+ 'div',
1738
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1739
+ react.createElement(
1740
+ 'span',
1741
+ { style: { fontSize: '13px', fontWeight: '700', color: 'var(--dsw-alias-label-primary)' } },
1742
+ '🖌️ ' + (t('inpaint.title') || 'Inpainting Canvas (Paint white mask over region to edit)')
1743
+ ),
1744
+ react.createElement(
1745
+ 'button',
1746
+ {
1747
+ type: 'button',
1748
+ className: 'ig-btn',
1749
+ style: { padding: '2px 8px', fontSize: '11px' },
1750
+ onClick: onClose,
1751
+ },
1752
+ '✕'
1753
+ )
1754
+ ),
1755
+ react.createElement(
1756
+ 'div',
1757
+ { style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' } },
1758
+ react.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, (t('inpaint.brush_size') || 'Brush') + ': ' + brushSize + 'px'),
1759
+ react.createElement('input', {
1760
+ type: 'range',
1761
+ min: '6',
1762
+ max: '64',
1763
+ value: brushSize,
1764
+ onChange: (e) => setBrushSize(parseInt(e.target.value, 10)),
1765
+ style: { width: '120px', cursor: 'pointer' },
1766
+ }),
1767
+ react.createElement(
1768
+ 'button',
1769
+ {
1770
+ type: 'button',
1771
+ className: 'ig-btn',
1772
+ style: { padding: '3px 8px', fontSize: '11px' },
1773
+ onClick: handleUndo,
1774
+ disabled: !hasStrokes,
1775
+ },
1776
+ '↩ ' + (t('inpaint.undo') || 'Undo')
1777
+ ),
1778
+ react.createElement(
1779
+ 'button',
1780
+ {
1781
+ type: 'button',
1782
+ className: 'ig-btn',
1783
+ style: { padding: '3px 8px', fontSize: '11px' },
1784
+ onClick: handleClear,
1785
+ },
1786
+ '🗑️ ' + (t('inpaint.clear') || 'Clear Mask')
1787
+ )
1788
+ ),
1789
+ react.createElement(
1790
+ 'div',
1791
+ {
1792
+ style: {
1793
+ position: 'relative',
1794
+ width: '100%',
1795
+ maxWidth: '512px',
1796
+ aspectRatio: '1/1',
1797
+ borderRadius: '8px',
1798
+ overflow: 'hidden',
1799
+ border: '1px solid var(--dsw-alias-border-l2)',
1800
+ background: '#000000',
1801
+ margin: '4px 0',
1802
+ userSelect: 'none',
1803
+ touchAction: 'none',
1804
+ },
1805
+ },
1806
+ imgUrl ? react.createElement('img', {
1807
+ src: imgUrl,
1808
+ alt: 'Source for Inpaint',
1809
+ style: {
1810
+ position: 'absolute',
1811
+ top: 0,
1812
+ left: 0,
1813
+ width: '100%',
1814
+ height: '100%',
1815
+ objectFit: 'contain',
1816
+ opacity: 0.45,
1817
+ pointerEvents: 'none',
1818
+ },
1819
+ }) : null,
1820
+ react.createElement('canvas', {
1821
+ ref: canvasRef,
1822
+ width: 512,
1823
+ height: 512,
1824
+ style: {
1825
+ position: 'absolute',
1826
+ top: 0,
1827
+ left: 0,
1828
+ width: '100%',
1829
+ height: '100%',
1830
+ cursor: 'crosshair',
1831
+ mixBlendMode: 'screen',
1832
+ },
1833
+ onMouseDown: startDraw,
1834
+ onMouseMove: draw,
1835
+ onMouseUp: stopDraw,
1836
+ onMouseLeave: stopDraw,
1837
+ onTouchStart: startDraw,
1838
+ onTouchMove: draw,
1839
+ onTouchEnd: stopDraw,
1840
+ })
1841
+ ),
1842
+ react.createElement('input', {
1843
+ type: 'text',
1844
+ placeholder: t('inpaint.prompt_placeholder') || 'Describe what to generate in the masked area...',
1845
+ value: inpaintPrompt,
1846
+ onChange: (e) => setInpaintPrompt(e.target.value),
1847
+ className: 'ig-field-input',
1848
+ style: { fontSize: '12px', padding: '6px 10px', width: '100%' },
1849
+ }),
1850
+ react.createElement(
1851
+ 'div',
1852
+ { style: { display: 'flex', justifyContent: 'flex-end', marginTop: '2px' } },
1853
+ react.createElement(
1854
+ 'button',
1855
+ {
1856
+ type: 'button',
1857
+ className: 'ig-btn ig-btn-primary',
1858
+ style: { padding: '5px 12px', fontSize: '12px' },
1859
+ onClick: handleApply,
1860
+ },
1861
+ copied ? (t('inpaint.copied') || '✓ Instruction Copied!') : ('🖌️ ' + (t('inpaint.apply') || 'Apply Inpainting'))
1862
+ )
1863
+ )
1864
+ )
1865
+ }
1866
+ // 106-style-matrix-view.js — 2×2 Style Matrix toolview with Blind A/B Compare (#286).
1867
+
1868
+ function StyleMatrixCard(props) {
1869
+ const block = props.block || {}
1870
+ const parsed = react.useMemo(() => {
1871
+ const raw = block.output || block.text || ''
1872
+ const obj = tryParseJsonObject(raw)
1873
+ return obj || { base_prompt: '', blind_mode: false, cells: [] }
1874
+ }, [block])
1875
+
1876
+ const [blind, setBlind] = react.useState(Boolean(parsed.blind_mode))
1877
+ const [copiedId, setCopiedId] = react.useState(null)
1878
+ const [previewImg, setPreviewImg] = react.useState(null)
1879
+
1880
+ const cells = parsed.cells || []
1881
+
1882
+ function handleSelectStyle(c) {
1883
+ const styleName = c.style || 'chosen_style'
1884
+ const instruction = 'Set style preset to "' + styleName + '" for subsequent image generations.'
1885
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
1886
+ navigator.clipboard.writeText(instruction)
1887
+ setCopiedId(c.id)
1888
+ setTimeout(() => setCopiedId(null), 2500)
1889
+ }
1890
+ }
1891
+
1892
+ const cellNodes = cells.map((c) => {
1893
+ const imgUrl = c.url || (c.attachment && attachmentImageUrl(c.attachment)) || ''
1894
+ const styleLabel = blind ? ('Option ' + c.id) : c.style
1895
+ const isCopied = copiedId === c.id
1896
+
1897
+ return react.createElement(
1898
+ 'div',
1899
+ { key: c.id, className: 'ig-matrix-cell' },
1900
+ imgUrl ? react.createElement('img', {
1901
+ src: imgUrl,
1902
+ alt: styleLabel,
1903
+ className: 'ig-matrix-img',
1904
+ onClick: () => setPreviewImg(imgUrl),
1905
+ }) : react.createElement('div', { style: { height: '140px', background: 'var(--dsw-alias-bg-layer-2)' } }),
1906
+ react.createElement(
1907
+ 'div',
1908
+ { className: 'ig-matrix-bar' },
1909
+ react.createElement(
1910
+ 'span',
1911
+ { style: { fontWeight: '600', color: 'var(--dsw-alias-label-primary)' } },
1912
+ '[' + c.id + '] ' + styleLabel
1913
+ ),
1914
+ react.createElement(
1915
+ 'button',
1916
+ {
1917
+ type: 'button',
1918
+ className: 'ig-btn',
1919
+ style: { padding: '2px 8px', fontSize: '11px' },
1920
+ onClick: () => handleSelectStyle(c),
1921
+ },
1922
+ isCopied ? (t('matrix.copied') || '✓ Selected!') : ('⭐ ' + (t('matrix.use_style') || 'Use Style'))
1923
+ )
1924
+ )
1925
+ )
1926
+ })
1927
+
1928
+ return react.createElement(
1929
+ 'div',
1930
+ { className: 'ig-section-card', style: { margin: '8px 0' } },
1931
+ react.createElement(
1932
+ 'div',
1933
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1934
+ react.createElement(
1935
+ 'span',
1936
+ { className: 'ig-section-title' },
1937
+ '⚡ ' + (t('matrix.title') || 'Style Matrix Benchmark (2×2)')
1938
+ ),
1939
+ react.createElement(
1940
+ 'button',
1941
+ {
1942
+ type: 'button',
1943
+ className: 'ig-tab-btn',
1944
+ style: { padding: '4px 10px', fontSize: '12px' },
1945
+ onClick: () => setBlind(!blind),
1946
+ },
1947
+ blind ? ('🙈 ' + (t('matrix.blind_toggle') || 'Reveal Styles')) : '👁️ Mask Styles (Blind)'
1948
+ )
1949
+ ),
1950
+ parsed.base_prompt ? react.createElement('div', { className: 'ig-prompt' }, parsed.base_prompt) : null,
1951
+ react.createElement('div', { className: 'ig-matrix-grid' }, cellNodes),
1952
+ previewImg ? react.createElement(
1953
+ 'div',
1954
+ {
1955
+ style: {
1956
+ position: 'fixed',
1957
+ top: 0,
1958
+ left: 0,
1959
+ right: 0,
1960
+ bottom: 0,
1961
+ background: 'rgba(0,0,0,0.85)',
1962
+ display: 'flex',
1963
+ alignItems: 'center',
1964
+ justifyContent: 'center',
1965
+ zIndex: 99999,
1966
+ cursor: 'zoom-out',
1967
+ },
1968
+ onClick: () => setPreviewImg(null),
1969
+ },
1970
+ react.createElement('img', {
1971
+ src: previewImg,
1972
+ alt: 'Enlarged Preview',
1973
+ style: { maxWidth: '90vw', maxHeight: '90vh', borderRadius: '8px' },
1974
+ })
1975
+ ) : null
1976
+ )
1977
+ }
1615
1978
  const en = {
1616
1979
  'settings.navLabel': 'Image Studio',
1617
1980
  'settings.title': 'Image Studio',
@@ -1650,6 +2013,23 @@ window.__ModuleLoader__.load({
1650
2013
  'card.actionReroll': 'Reroll image',
1651
2014
  'card.actionUpscale': 'Upscale image',
1652
2015
  'card.actionRemoveBg': 'Remove background',
2016
+ 'card.inpaint': 'Inpaint',
2017
+ 'inpaint.title': 'Inpainting Canvas (Paint white mask over region to edit)',
2018
+ 'inpaint.brush_size': 'Brush',
2019
+ 'inpaint.clear': 'Clear Mask',
2020
+ 'inpaint.undo': 'Undo',
2021
+ 'inpaint.apply': 'Apply Inpaint',
2022
+ 'inpaint.prompt_placeholder': 'Describe what to generate in the masked area...',
2023
+ 'inpaint.copied': '✓ Instruction Copied!',
2024
+ 'matrix.title': 'Style Matrix Benchmark (2×2)',
2025
+ 'matrix.blind_on': 'Blind Mode Active',
2026
+ 'matrix.blind_toggle': 'Reveal Styles',
2027
+ 'matrix.use_style': 'Use Style',
2028
+ 'matrix.copied': '✓ Selected!',
2029
+ 'f.fallbackProviders': 'Fallback Providers Chain',
2030
+ 'f.fallbackProvidersHint': 'Comma-separated list of fallback providers to cascade to on 429, 5xx, or quota limits (e.g. replicate, custom, gemini).',
2031
+ 'p.fallbackProviders': 'replicate, custom, gemini',
2032
+
1653
2033
  'card.reroll': 'Reroll',
1654
2034
  'card.upscale': 'Upscale 2x/4x',
1655
2035
  'card.removeBg': 'Remove BG',
@@ -1854,6 +2234,23 @@ window.__ModuleLoader__.load({
1854
2234
  'card.actionReroll': '重新生成图像',
1855
2235
  'card.actionUpscale': '放大图像',
1856
2236
  'card.actionRemoveBg': '移除背景',
2237
+ 'card.inpaint': '局部重绘',
2238
+ 'inpaint.title': '画布局部重绘(涂抹白色蒙版标记区域)',
2239
+ 'inpaint.brush_size': '画笔大小',
2240
+ 'inpaint.clear': '清空蒙版',
2241
+ 'inpaint.undo': '撤销',
2242
+ 'inpaint.apply': '生成局部重绘',
2243
+ 'inpaint.prompt_placeholder': '描述在涂抹区域生成的内容...',
2244
+ 'inpaint.copied': '✓ 已复制重绘指令!',
2245
+ 'matrix.title': '2×2 风格探索矩阵',
2246
+ 'matrix.blind_on': '盲测模式进行中',
2247
+ 'matrix.blind_toggle': '显示风格标签',
2248
+ 'matrix.use_style': '使用此风格',
2249
+ 'matrix.copied': '✓ 风格已选定!',
2250
+ 'f.fallbackProviders': '备用提供商级联',
2251
+ 'f.fallbackProvidersHint': '当主提供商遇到 429 限流、5xx 故障或余额不足时依次自动切换的备用服务商列表(如 replicate, custom, gemini)。',
2252
+ 'p.fallbackProviders': 'replicate, custom, gemini',
2253
+
1857
2254
  'card.reroll': '重绘',
1858
2255
  'card.upscale': '放大 2x/4x',
1859
2256
  'card.removeBg': '抠图背景',
@@ -2078,6 +2475,7 @@ window.__ModuleLoader__.load({
2078
2475
  'assemble_image_grid',
2079
2476
  'smart_crop_image',
2080
2477
  'export_asset_pack',
2478
+ 'generate_ui_asset',
2081
2479
  ]
2082
2480
 
2083
2481
  for (const toolName of toolviewTools) {
@@ -2094,6 +2492,18 @@ window.__ModuleLoader__.load({
2094
2492
  )
2095
2493
  }
2096
2494
 
2495
+ registerSlotWhenReady('tool.call.toolview', () =>
2496
+ ctx.slots.register(
2497
+ {
2498
+ name: 'tool.call.toolview',
2499
+ key: 'generate_style_matrix',
2500
+ locale: NS,
2501
+ inject: () => ({ sessions: ctx.sessions }),
2502
+ },
2503
+ (props) => react.createElement(ErrorBoundary, null, react.createElement(StyleMatrixCard, props))
2504
+ )
2505
+ )
2506
+
2097
2507
 
2098
2508
  // Native Sidebar Right Pane Tab & BetterSidebar
2099
2509
  if (typeof ctx.inject === 'function') {