@goodandready/dsh-image-gen 0.10.35 → 0.11.1
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 +994 -161
- package/lib/comfy-workflow-helpers.js +161 -0
- package/lib/fallback-router.js +25 -3
- package/lib/index.js +259 -138
- package/lib/provider-utils.js +0 -2
- package/lib/providers/backends/local.js +33 -11
- package/lib/providers.js +9 -30
- package/lib/register-tools.js +3 -1
- package/lib/settings-route.js +112 -0
- package/lib/theme-pair-helpers.js +83 -0
- package/lib/tools/anchor.js +1 -2
- package/lib/tools/editing.js +2 -34
- package/lib/tools/frontend.js +2 -35
- package/lib/tools/generation-pack.js +2 -4
- package/lib/tools/generation.js +2 -23
- package/lib/tools/inspect.js +6 -45
- package/lib/tools/pattern.js +10 -0
- package/lib/tools/processing-advanced.js +0 -32
- package/lib/tools/processing-basic.js +2 -25
- package/lib/tools/style-matrix.js +1 -2
- package/lib/tools/theme-pair.js +250 -0
- package/lib/tools/ui-asset.js +1 -1
- package/lib/updater.js +0 -9
- package/lib/vault.js +183 -0
- package/package.json +7 -1
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
|
|
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
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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,
|
|
1483
1537
|
alt: prompt || 'generated image',
|
|
1484
|
-
|
|
1485
|
-
style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
|
|
1538
|
+
t,
|
|
1486
1539
|
}))
|
|
1487
1540
|
}
|
|
1488
1541
|
|
|
@@ -1595,30 +1648,128 @@ 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)
|
|
1657
|
+
|
|
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])
|
|
1599
1717
|
|
|
1600
|
-
|
|
1601
|
-
|
|
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, #301).
|
|
1602
1755
|
|
|
1603
|
-
function
|
|
1756
|
+
function InpaintCanvas(props) {
|
|
1757
|
+
const { imgUrl, targetRef, onClose, t } = props
|
|
1604
1758
|
const canvasRef = react.useRef(null)
|
|
1605
1759
|
const isDrawingRef = react.useRef(false)
|
|
1760
|
+
const undoStackRef = react.useRef([])
|
|
1606
1761
|
const [brushSize, setBrushSize] = react.useState(24)
|
|
1607
|
-
const [toolMode, setToolMode] = react.useState('brush')
|
|
1608
|
-
const [inpaintPrompt, setInpaintPrompt] = react.useState('')
|
|
1609
|
-
const [copied, setCopied] = react.useState(false)
|
|
1762
|
+
const [toolMode, setToolMode] = react.useState('brush') // 'brush' | 'eraser'
|
|
1610
1763
|
const [hasStrokes, setHasStrokes] = react.useState(false)
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
const imgUrl = (parsed && (parsed.url || (parsed.attachment && attachmentImageUrl(parsed.attachment)))) || ''
|
|
1614
|
-
const targetRef = (parsed && (parsed.attachment?.attachmentId || parsed.path || parsed.url)) || 'current image'
|
|
1764
|
+
const [copied, setCopied] = react.useState(false)
|
|
1765
|
+
const [inpaintPrompt, setInpaintPrompt] = react.useState('')
|
|
1615
1766
|
|
|
1616
1767
|
const initCanvas = react.useCallback(() => {
|
|
1617
1768
|
const canvas = canvasRef.current
|
|
1618
1769
|
if (!canvas) return
|
|
1619
1770
|
const ctx = canvas.getContext('2d')
|
|
1620
1771
|
if (!ctx) return
|
|
1621
|
-
ctx.fillStyle = '
|
|
1772
|
+
ctx.fillStyle = 'black'
|
|
1622
1773
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
1623
1774
|
undoStackRef.current = [ctx.getImageData(0, 0, canvas.width, canvas.height)]
|
|
1624
1775
|
}, [])
|
|
@@ -1627,9 +1778,7 @@ window.__ModuleLoader__.load({
|
|
|
1627
1778
|
initCanvas()
|
|
1628
1779
|
}, [initCanvas])
|
|
1629
1780
|
|
|
1630
|
-
function getPos(e) {
|
|
1631
|
-
const canvas = canvasRef.current
|
|
1632
|
-
if (!canvas) return { x: 0, y: 0 }
|
|
1781
|
+
function getPos(e, canvas) {
|
|
1633
1782
|
const rect = canvas.getBoundingClientRect()
|
|
1634
1783
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX
|
|
1635
1784
|
const clientY = e.touches ? e.touches[0].clientY : e.clientY
|
|
@@ -1643,42 +1792,40 @@ window.__ModuleLoader__.load({
|
|
|
1643
1792
|
|
|
1644
1793
|
function startDraw(e) {
|
|
1645
1794
|
if (e.touches && e.touches.length > 1) return
|
|
1646
|
-
e.preventDefault()
|
|
1647
1795
|
const canvas = canvasRef.current
|
|
1648
1796
|
if (!canvas) return
|
|
1649
1797
|
const ctx = canvas.getContext('2d')
|
|
1650
1798
|
if (!ctx) return
|
|
1651
|
-
|
|
1652
1799
|
isDrawingRef.current = true
|
|
1653
|
-
const pos = getPos(e)
|
|
1800
|
+
const pos = getPos(e, canvas)
|
|
1801
|
+
ctx.beginPath()
|
|
1802
|
+
ctx.moveTo(pos.x, pos.y)
|
|
1654
1803
|
ctx.lineWidth = brushSize
|
|
1655
1804
|
ctx.lineCap = 'round'
|
|
1656
1805
|
ctx.lineJoin = 'round'
|
|
1657
|
-
const color = toolMode === 'eraser' ? '
|
|
1806
|
+
const color = toolMode === 'eraser' ? 'black' : 'white'
|
|
1658
1807
|
ctx.strokeStyle = color
|
|
1659
1808
|
ctx.fillStyle = color
|
|
1660
1809
|
|
|
1661
|
-
ctx.beginPath()
|
|
1662
1810
|
ctx.arc(pos.x, pos.y, brushSize / 2, 0, Math.PI * 2)
|
|
1663
1811
|
ctx.fill()
|
|
1664
1812
|
ctx.beginPath()
|
|
1665
1813
|
ctx.moveTo(pos.x, pos.y)
|
|
1814
|
+
setHasStrokes(true)
|
|
1666
1815
|
}
|
|
1667
1816
|
|
|
1668
1817
|
function draw(e) {
|
|
1669
1818
|
if (!isDrawingRef.current) return
|
|
1670
1819
|
if (e.touches && e.touches.length > 1) return
|
|
1671
|
-
e.preventDefault()
|
|
1672
1820
|
const canvas = canvasRef.current
|
|
1673
1821
|
if (!canvas) return
|
|
1674
1822
|
const ctx = canvas.getContext('2d')
|
|
1675
1823
|
if (!ctx) return
|
|
1676
|
-
|
|
1677
|
-
const pos = getPos(e)
|
|
1824
|
+
const pos = getPos(e, canvas)
|
|
1678
1825
|
ctx.lineWidth = brushSize
|
|
1679
1826
|
ctx.lineCap = 'round'
|
|
1680
1827
|
ctx.lineJoin = 'round'
|
|
1681
|
-
const color = toolMode === 'eraser' ? '
|
|
1828
|
+
const color = toolMode === 'eraser' ? 'black' : 'white'
|
|
1682
1829
|
ctx.strokeStyle = color
|
|
1683
1830
|
ctx.lineTo(pos.x, pos.y)
|
|
1684
1831
|
ctx.stroke()
|
|
@@ -1852,7 +1999,7 @@ window.__ModuleLoader__.load({
|
|
|
1852
1999
|
borderRadius: '8px',
|
|
1853
2000
|
overflow: 'hidden',
|
|
1854
2001
|
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1855
|
-
background: '
|
|
2002
|
+
background: 'black',
|
|
1856
2003
|
margin: '4px 0',
|
|
1857
2004
|
userSelect: 'none',
|
|
1858
2005
|
touchAction: 'none',
|
|
@@ -1918,9 +2065,11 @@ window.__ModuleLoader__.load({
|
|
|
1918
2065
|
)
|
|
1919
2066
|
)
|
|
1920
2067
|
}
|
|
1921
|
-
|
|
2068
|
+
var InpaintCanvasOverlay = InpaintCanvas
|
|
2069
|
+
// 106-style-matrix-view.js — 2×2 Style Matrix toolview with Blind A/B Compare (#286, #297, #301).
|
|
1922
2070
|
|
|
1923
2071
|
function StyleMatrixCard(props) {
|
|
2072
|
+
const t = props.t || ((k) => k)
|
|
1924
2073
|
const block = props.block || {}
|
|
1925
2074
|
const parsed = react.useMemo(() => {
|
|
1926
2075
|
const raw = block.output || block.text || ''
|
|
@@ -2013,7 +2162,7 @@ window.__ModuleLoader__.load({
|
|
|
2013
2162
|
left: 0,
|
|
2014
2163
|
right: 0,
|
|
2015
2164
|
bottom: 0,
|
|
2016
|
-
background: '
|
|
2165
|
+
background: 'var(--dsw-alias-bg-overlay, rgb(0 0 0 / 85%))',
|
|
2017
2166
|
display: 'flex',
|
|
2018
2167
|
alignItems: 'center',
|
|
2019
2168
|
justifyContent: 'center',
|
|
@@ -2030,11 +2179,116 @@ window.__ModuleLoader__.load({
|
|
|
2030
2179
|
) : null
|
|
2031
2180
|
)
|
|
2032
2181
|
}
|
|
2182
|
+
// -------------------------------------------------------------- Theme Pair Toolview (#189, #296)
|
|
2183
|
+
function ThemePairView(props) {
|
|
2184
|
+
const block = props.block
|
|
2185
|
+
const t = props.t || ((k) => k)
|
|
2186
|
+
const [copiedHtml, setCopiedHtml] = react.useState(false)
|
|
2187
|
+
const [copiedCss, setCopiedCss] = react.useState(false)
|
|
2188
|
+
const [themeTab, setThemeTab] = react.useState('light')
|
|
2189
|
+
|
|
2190
|
+
const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
|
|
2191
|
+
let args = {}
|
|
2192
|
+
try { if (raw) args = JSON.parse(raw) } catch (_) { /* invalid json */ }
|
|
2193
|
+
|
|
2194
|
+
let data = {}
|
|
2195
|
+
try {
|
|
2196
|
+
const out = block && (block.output || block.result || block.text || '')
|
|
2197
|
+
if (typeof out === 'object') data = out
|
|
2198
|
+
else if (typeof out === 'string') data = JSON.parse(out)
|
|
2199
|
+
} catch (_) { /* invalid json */ }
|
|
2200
|
+
|
|
2201
|
+
const light = data.light || {}
|
|
2202
|
+
const dark = data.dark || {}
|
|
2203
|
+
const htmlSnippet = data.html_snippet || ''
|
|
2204
|
+
const cssSnippet = data.css_snippet || ''
|
|
2205
|
+
|
|
2206
|
+
const copyCode = (text, isHtml) => {
|
|
2207
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
2208
|
+
navigator.clipboard.writeText(text)
|
|
2209
|
+
if (isHtml) {
|
|
2210
|
+
setCopiedHtml(true)
|
|
2211
|
+
setTimeout(() => setCopiedHtml(false), 2000)
|
|
2212
|
+
} else {
|
|
2213
|
+
setCopiedCss(true)
|
|
2214
|
+
setTimeout(() => setCopiedCss(false), 2000)
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
const activeImg = themeTab === 'light' ? light : dark
|
|
2220
|
+
|
|
2221
|
+
return react.createElement(
|
|
2222
|
+
'div',
|
|
2223
|
+
{ className: 'ig-page', style: { padding: '4px 0', gap: '10px' } },
|
|
2224
|
+
react.createElement('div', { className: 'fal_head' }, '🌓 ' + (t('themePair.title') || 'Theme Pair Graphic')),
|
|
2225
|
+
args.prompt ? react.createElement('div', { className: 'ig-prompt' }, args.prompt) : null,
|
|
2226
|
+
react.createElement(
|
|
2227
|
+
'div',
|
|
2228
|
+
{ className: 'ig-tabs' },
|
|
2229
|
+
react.createElement(
|
|
2230
|
+
'button',
|
|
2231
|
+
{
|
|
2232
|
+
type: 'button',
|
|
2233
|
+
className: themeTab === 'light' ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
|
|
2234
|
+
onClick: () => setThemeTab('light'),
|
|
2235
|
+
},
|
|
2236
|
+
'☀️ ' + (t('themePair.light') || 'Light Theme')
|
|
2237
|
+
),
|
|
2238
|
+
react.createElement(
|
|
2239
|
+
'button',
|
|
2240
|
+
{
|
|
2241
|
+
type: 'button',
|
|
2242
|
+
className: themeTab === 'dark' ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
|
|
2243
|
+
onClick: () => setThemeTab('dark'),
|
|
2244
|
+
},
|
|
2245
|
+
'🌙 ' + (t('themePair.dark') || 'Dark Theme')
|
|
2246
|
+
)
|
|
2247
|
+
),
|
|
2248
|
+
activeImg.url || activeImg.path ? react.createElement('img', {
|
|
2249
|
+
src: activeImg.url || activeImg.path,
|
|
2250
|
+
alt: args.prompt || 'theme image',
|
|
2251
|
+
loading: 'lazy',
|
|
2252
|
+
style: {
|
|
2253
|
+
width: '100%',
|
|
2254
|
+
maxHeight: '400px',
|
|
2255
|
+
objectFit: 'contain',
|
|
2256
|
+
borderRadius: '8px',
|
|
2257
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
2258
|
+
background: themeTab === 'light' ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-3)',
|
|
2259
|
+
},
|
|
2260
|
+
}) : null,
|
|
2261
|
+
htmlSnippet ? react.createElement(
|
|
2262
|
+
'div',
|
|
2263
|
+
{ style: { display: 'flex', gap: '8px', marginTop: '4px' } },
|
|
2264
|
+
react.createElement(
|
|
2265
|
+
'button',
|
|
2266
|
+
{
|
|
2267
|
+
type: 'button',
|
|
2268
|
+
className: 'ig-btn',
|
|
2269
|
+
onClick: () => copyCode(htmlSnippet, true),
|
|
2270
|
+
},
|
|
2271
|
+
copiedHtml ? '✓ ' + (t('themePair.copiedHtml') || 'Copied HTML') : '📋 ' + (t('themePair.copyHtml') || 'Copy <picture> HTML')
|
|
2272
|
+
),
|
|
2273
|
+
cssSnippet ? react.createElement(
|
|
2274
|
+
'button',
|
|
2275
|
+
{
|
|
2276
|
+
type: 'button',
|
|
2277
|
+
className: 'ig-btn',
|
|
2278
|
+
onClick: () => copyCode(cssSnippet, false),
|
|
2279
|
+
},
|
|
2280
|
+
copiedCss ? '✓ ' + (t('themePair.copiedCss') || 'Copied CSS') : '🎨 ' + (t('themePair.copyCss') || 'Copy Theme CSS')
|
|
2281
|
+
) : null
|
|
2282
|
+
) : null
|
|
2283
|
+
)
|
|
2284
|
+
}
|
|
2285
|
+
var ThemePairToolView = ThemePairView
|
|
2033
2286
|
const en = {
|
|
2034
2287
|
'settings.navLabel': 'Image Studio',
|
|
2035
2288
|
'settings.title': 'Image Studio',
|
|
2036
2289
|
'settings.description': 'AI Image generation, inpainting, variations, upscale & vectorization.',
|
|
2037
|
-
'settings.unavailable': 'Plugin settings are
|
|
2290
|
+
'settings.unavailable': 'Plugin settings are unavailable: the host settings namespace is not ready.',
|
|
2291
|
+
'settings.loading': 'Loading plugin settings…',
|
|
2038
2292
|
'settings.unsaved': 'Unsaved changes',
|
|
2039
2293
|
'settings.saving': 'Saving…',
|
|
2040
2294
|
'settings.save': 'Save settings',
|
|
@@ -2258,6 +2512,22 @@ window.__ModuleLoader__.load({
|
|
|
2258
2512
|
'f.cacheTtlDays': 'Cache Retention (Legacy)',
|
|
2259
2513
|
'f.cacheTtlDaysHint': 'Legacy name for retention period in days.',
|
|
2260
2514
|
'p.cacheTtlDays': '0',
|
|
2515
|
+
|
|
2516
|
+
'studio.title': 'Image Studio',
|
|
2517
|
+
'studio.canvas': 'Studio Canvas',
|
|
2518
|
+
'studio.vault': 'Asset Vault',
|
|
2519
|
+
'studio.parameters': 'Generation Parameters',
|
|
2520
|
+
'studio.generate': 'Generate in Chat',
|
|
2521
|
+
'studio.recent': 'Recent Generations',
|
|
2522
|
+
'vault.title': 'Asset Browser & Vault',
|
|
2523
|
+
'vault.search': 'Search prompt or tags...',
|
|
2524
|
+
'vault.loadMore': 'Load more assets',
|
|
2525
|
+
'vault.empty': 'No assets found in vault.',
|
|
2526
|
+
'vault.inspector': 'Asset Inspector',
|
|
2527
|
+
'vault.reroll': 'Re-roll with seed',
|
|
2528
|
+
'vault.insert': 'Insert in Chat',
|
|
2529
|
+
'vault.download': 'Download',
|
|
2530
|
+
'vault.delete': 'Delete',
|
|
2261
2531
|
}
|
|
2262
2532
|
|
|
2263
2533
|
const zh = {
|
|
@@ -2331,6 +2601,7 @@ window.__ModuleLoader__.load({
|
|
|
2331
2601
|
'settings.reset': '重置',
|
|
2332
2602
|
'settings.invalidNumber': '请输入有效数值',
|
|
2333
2603
|
'settings.unavailable': '设置命名空间当前不可用。',
|
|
2604
|
+
'settings.loading': '正在加载插件设置…',
|
|
2334
2605
|
|
|
2335
2606
|
'f.enabled': '启用图像生成',
|
|
2336
2607
|
'f.enabledHint': '关闭后将禁用所有图像生成及处理工具。',
|
|
@@ -2476,49 +2747,38 @@ 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
|
|
2482
|
-
// Dual compatibility with DSH 0.1.5-rc.3 (settingsScope) and 0.1.6+ (configForms) (#291)
|
|
2769
|
+
// Dual compatibility with DSH 0.1.5-rc.3 (settingsScope) and 0.1.6+ (configForms) (#291, #295)
|
|
2483
2770
|
const inject = ['slots', 'locale', 'sessions']
|
|
2484
2771
|
|
|
2485
2772
|
function resolveActiveScope(ctx) {
|
|
2486
2773
|
if (!ctx) return null
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
try {
|
|
2496
|
-
const cf = ctx.get('configForms')
|
|
2497
|
-
if (cf && typeof cf.get === 'function') {
|
|
2498
|
-
const s = cf.get(SETTINGS_NS)
|
|
2499
|
-
if (s) return s
|
|
2500
|
-
}
|
|
2501
|
-
} catch (_) { /* scope unavailable */ }
|
|
2502
|
-
try {
|
|
2503
|
-
const ss = ctx.get('settingsScope')
|
|
2504
|
-
if (ss && typeof ss.bind === 'function') {
|
|
2505
|
-
const s = ss.bind({ namespace: SETTINGS_NS })
|
|
2506
|
-
if (s) return s
|
|
2507
|
-
}
|
|
2508
|
-
} catch (_) { /* scope unavailable */ }
|
|
2509
|
-
}
|
|
2510
|
-
if (ctx.configForms && typeof ctx.configForms.get === 'function') {
|
|
2511
|
-
try {
|
|
2512
|
-
const s = ctx.configForms.get(SETTINGS_NS)
|
|
2513
|
-
if (s) return s
|
|
2514
|
-
} catch (_) { /* scope unavailable */ }
|
|
2515
|
-
}
|
|
2516
|
-
if (ctx.settingsScope && typeof ctx.settingsScope.bind === 'function') {
|
|
2517
|
-
try {
|
|
2518
|
-
const s = ctx.settingsScope.bind({ namespace: SETTINGS_NS })
|
|
2519
|
-
if (s) return s
|
|
2520
|
-
} catch (_) { /* scope unavailable */ }
|
|
2521
|
-
}
|
|
2774
|
+
try {
|
|
2775
|
+
const lan = (ctx.get && ctx.get('lanSettings')) || ctx.lanSettings
|
|
2776
|
+
if (lan?.get) return lan.get(SETTINGS_NS)
|
|
2777
|
+
const cf = (ctx.get && ctx.get('configForms')) || ctx.configForms
|
|
2778
|
+
if (cf?.get) return cf.get(SETTINGS_NS)
|
|
2779
|
+
const ss = (ctx.get && ctx.get('settingsScope')) || ctx.settingsScope
|
|
2780
|
+
if (ss?.bind) return ss.bind({ namespace: SETTINGS_NS })
|
|
2781
|
+
} catch (_) { /* scope unavailable */ }
|
|
2522
2782
|
return null
|
|
2523
2783
|
}
|
|
2524
2784
|
|
|
@@ -2526,70 +2786,93 @@ window.__ModuleLoader__.load({
|
|
|
2526
2786
|
const listeners = new Set()
|
|
2527
2787
|
let activeTarget = resolveActiveScope(ctx)
|
|
2528
2788
|
let activeUnsub = null
|
|
2789
|
+
let httpSnapshot = { status: 'loading', writable: true, value: {} }
|
|
2790
|
+
|
|
2791
|
+
const notify = () => { for (const fn of listeners) fn() }
|
|
2792
|
+
|
|
2793
|
+
const syncHttpConfig = () => {
|
|
2794
|
+
if (typeof fetch !== 'function') return
|
|
2795
|
+
fetch('/dsh-image-gen/config', { cache: 'no-store' })
|
|
2796
|
+
.then((r) => r.json())
|
|
2797
|
+
.then((data) => {
|
|
2798
|
+
if (data && data.ok && data.config) {
|
|
2799
|
+
httpSnapshot = { status: 'ready', writable: true, value: data.config }
|
|
2800
|
+
notify()
|
|
2801
|
+
}
|
|
2802
|
+
})
|
|
2803
|
+
.catch(() => {})
|
|
2804
|
+
}
|
|
2805
|
+
syncHttpConfig()
|
|
2529
2806
|
|
|
2530
2807
|
const bindTarget = (target) => {
|
|
2531
2808
|
if (!target || target === activeTarget) return
|
|
2532
2809
|
if (typeof activeUnsub === 'function') {
|
|
2533
|
-
try { activeUnsub() } catch
|
|
2810
|
+
try { activeUnsub() } catch { /* ignore */ }
|
|
2534
2811
|
activeUnsub = null
|
|
2535
2812
|
}
|
|
2536
2813
|
activeTarget = target
|
|
2537
2814
|
if (typeof activeTarget.subscribe === 'function') {
|
|
2538
2815
|
try {
|
|
2539
|
-
activeUnsub = activeTarget.subscribe(
|
|
2540
|
-
|
|
2541
|
-
})
|
|
2542
|
-
} catch (_) { /* scope unavailable */ }
|
|
2816
|
+
activeUnsub = activeTarget.subscribe(notify)
|
|
2817
|
+
} catch { /* ignore */ }
|
|
2543
2818
|
}
|
|
2544
|
-
|
|
2819
|
+
notify()
|
|
2545
2820
|
}
|
|
2546
2821
|
|
|
2547
|
-
if (activeTarget
|
|
2548
|
-
try {
|
|
2549
|
-
activeUnsub = activeTarget.subscribe(() => {
|
|
2550
|
-
for (const fn of listeners) fn()
|
|
2551
|
-
})
|
|
2552
|
-
} catch (_) { /* scope unavailable */ }
|
|
2822
|
+
if (activeTarget?.subscribe) {
|
|
2823
|
+
try { activeUnsub = activeTarget.subscribe(notify) } catch { /* ignore */ }
|
|
2553
2824
|
}
|
|
2554
2825
|
|
|
2555
2826
|
if (typeof ctx?.inject === 'function') {
|
|
2556
2827
|
try {
|
|
2557
2828
|
ctx.inject(['configForms'], (sctx) => {
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
if (s) bindTarget(s)
|
|
2561
|
-
} catch (_) { /* scope unavailable */ }
|
|
2829
|
+
const s = sctx?.configForms?.get?.(SETTINGS_NS)
|
|
2830
|
+
if (s) bindTarget(s)
|
|
2562
2831
|
})
|
|
2563
|
-
} catch (_) { /* scope unavailable */ }
|
|
2564
|
-
try {
|
|
2565
2832
|
ctx.inject(['settingsScope'], (sctx) => {
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
if (s) bindTarget(s)
|
|
2569
|
-
} catch (_) { /* scope unavailable */ }
|
|
2833
|
+
const s = sctx?.settingsScope?.bind?.({ namespace: SETTINGS_NS })
|
|
2834
|
+
if (s) bindTarget(s)
|
|
2570
2835
|
})
|
|
2571
|
-
} catch
|
|
2836
|
+
} catch { /* ignore */ }
|
|
2572
2837
|
}
|
|
2573
2838
|
|
|
2574
2839
|
return {
|
|
2575
2840
|
getSnapshot() {
|
|
2576
2841
|
const target = activeTarget || resolveActiveScope(ctx)
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
return
|
|
2842
|
+
const snap = target?.getSnapshot ? target.getSnapshot() : null
|
|
2843
|
+
if (snap?.status === 'ready' && snap.writable) return snap
|
|
2844
|
+
if (httpSnapshot.status === 'ready') return httpSnapshot
|
|
2845
|
+
return snap || httpSnapshot
|
|
2581
2846
|
},
|
|
2582
2847
|
async set(key, val) {
|
|
2583
2848
|
const target = activeTarget || resolveActiveScope(ctx)
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
}
|
|
2849
|
+
const snap = target?.getSnapshot ? target.getSnapshot() : null
|
|
2850
|
+
if (snap?.status === 'ready' && snap.writable && target.set) return target.set(key, val)
|
|
2851
|
+
httpSnapshot = { ...httpSnapshot, value: { ...httpSnapshot.value, [key]: val } }
|
|
2852
|
+
notify()
|
|
2853
|
+
try {
|
|
2854
|
+
await fetch('/dsh-image-gen/config', {
|
|
2855
|
+
method: 'PUT',
|
|
2856
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2857
|
+
body: JSON.stringify({ [key]: val }),
|
|
2858
|
+
})
|
|
2859
|
+
} catch { /* ignore */ }
|
|
2587
2860
|
},
|
|
2588
2861
|
async delete(key) {
|
|
2589
2862
|
const target = activeTarget || resolveActiveScope(ctx)
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
}
|
|
2863
|
+
const snap = target?.getSnapshot ? target.getSnapshot() : null
|
|
2864
|
+
if (snap?.status === 'ready' && snap.writable && target.delete) return target.delete(key)
|
|
2865
|
+
const nextVal = { ...httpSnapshot.value }
|
|
2866
|
+
delete nextVal[key]
|
|
2867
|
+
httpSnapshot = { ...httpSnapshot, value: nextVal }
|
|
2868
|
+
notify()
|
|
2869
|
+
try {
|
|
2870
|
+
await fetch('/dsh-image-gen/config', {
|
|
2871
|
+
method: 'PUT',
|
|
2872
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2873
|
+
body: JSON.stringify({ [key]: undefined }),
|
|
2874
|
+
})
|
|
2875
|
+
} catch { /* ignore */ }
|
|
2593
2876
|
},
|
|
2594
2877
|
subscribe(fn) {
|
|
2595
2878
|
listeners.add(fn)
|
|
@@ -2618,27 +2901,17 @@ window.__ModuleLoader__.load({
|
|
|
2618
2901
|
if (typeof ctx.slots.inject === 'function') {
|
|
2619
2902
|
try {
|
|
2620
2903
|
ctx.slots.inject(slotName, () => {
|
|
2621
|
-
try {
|
|
2622
|
-
return registerFn()
|
|
2623
|
-
} catch (err) {
|
|
2624
|
-
console.warn('[dsh-image-gen] Error registering slot ' + slotName + ':', err)
|
|
2625
|
-
}
|
|
2904
|
+
try { return registerFn() } catch (err) { console.warn('[dsh-image-gen] Slot err ' + slotName + ':', err) }
|
|
2626
2905
|
})
|
|
2627
2906
|
return
|
|
2628
|
-
} catch (err) {
|
|
2629
|
-
console.warn('[dsh-image-gen] Failed to inject slot ' + slotName + ':', err)
|
|
2630
|
-
}
|
|
2907
|
+
} catch (err) { console.warn('[dsh-image-gen] Failed inject ' + slotName + ':', err) }
|
|
2631
2908
|
}
|
|
2632
2909
|
if (typeof ctx.slots.register === 'function') {
|
|
2633
|
-
try {
|
|
2634
|
-
registerFn()
|
|
2635
|
-
} catch (err) {
|
|
2636
|
-
console.warn('[dsh-image-gen] Failed direct registration for ' + slotName + ':', err)
|
|
2637
|
-
}
|
|
2910
|
+
try { registerFn() } catch (err) { console.warn('[dsh-image-gen] Direct reg err ' + slotName + ':', err) }
|
|
2638
2911
|
}
|
|
2639
2912
|
}
|
|
2640
2913
|
|
|
2641
|
-
// Register toolviews for all
|
|
2914
|
+
// Register toolviews for all visual tools
|
|
2642
2915
|
const toolviewTools = [
|
|
2643
2916
|
'generate_image',
|
|
2644
2917
|
'edit_image',
|
|
@@ -2682,6 +2955,17 @@ window.__ModuleLoader__.load({
|
|
|
2682
2955
|
)
|
|
2683
2956
|
)
|
|
2684
2957
|
|
|
2958
|
+
registerSlotWhenReady('tool.call.toolview', () =>
|
|
2959
|
+
ctx.slots.register(
|
|
2960
|
+
{
|
|
2961
|
+
name: 'tool.call.toolview',
|
|
2962
|
+
key: 'generate_theme_pair',
|
|
2963
|
+
locale: NS,
|
|
2964
|
+
inject: () => ({ sessions: ctx.sessions }),
|
|
2965
|
+
},
|
|
2966
|
+
(props) => react.createElement(ErrorBoundary, null, react.createElement(ThemePairView, props))
|
|
2967
|
+
)
|
|
2968
|
+
)
|
|
2685
2969
|
|
|
2686
2970
|
// Native Sidebar Right Pane Tab & BetterSidebar
|
|
2687
2971
|
if (typeof ctx.inject === 'function') {
|
|
@@ -2691,16 +2975,16 @@ window.__ModuleLoader__.load({
|
|
|
2691
2975
|
if (!tabs || typeof tabs.register !== 'function') return
|
|
2692
2976
|
try {
|
|
2693
2977
|
const def = {
|
|
2694
|
-
id: '@goodandready/dsh-image-gen:
|
|
2695
|
-
kind: 'image-
|
|
2978
|
+
id: '@goodandready/dsh-image-gen:studio',
|
|
2979
|
+
kind: 'image-studio',
|
|
2696
2980
|
priority: 'extension',
|
|
2697
|
-
title: () => '
|
|
2981
|
+
title: () => 'Image Studio',
|
|
2698
2982
|
guide: [
|
|
2699
2983
|
{
|
|
2700
2984
|
order: 45,
|
|
2701
|
-
title: () => 'Image Studio
|
|
2702
|
-
description: () => '
|
|
2703
|
-
icon: () => react.createElement('span', null, '
|
|
2985
|
+
title: () => 'Image Studio & Vault',
|
|
2986
|
+
description: () => 'Interactive visual studio & asset browser',
|
|
2987
|
+
icon: () => react.createElement('span', null, '🎨'),
|
|
2704
2988
|
},
|
|
2705
2989
|
],
|
|
2706
2990
|
}
|
|
@@ -2720,7 +3004,7 @@ window.__ModuleLoader__.load({
|
|
|
2720
3004
|
locale: NS,
|
|
2721
3005
|
inject: () => ({ ctx }),
|
|
2722
3006
|
},
|
|
2723
|
-
(props) => react.createElement(ErrorBoundary, null, react.createElement(
|
|
3007
|
+
(props) => react.createElement(ErrorBoundary, null, react.createElement(ImageStudioView, { ...props, ctx }))
|
|
2724
3008
|
)
|
|
2725
3009
|
} catch (e) {
|
|
2726
3010
|
console.warn('[dsh-image-gen] native sidebar pane tab register failed', e)
|
|
@@ -2736,7 +3020,7 @@ window.__ModuleLoader__.load({
|
|
|
2736
3020
|
console.warn('[dsh-image-gen] native sidebar registration failed', e)
|
|
2737
3021
|
}
|
|
2738
3022
|
})
|
|
2739
|
-
} catch (
|
|
3023
|
+
} catch (_) { /* native sidebar unavailable */ }
|
|
2740
3024
|
|
|
2741
3025
|
try {
|
|
2742
3026
|
ctx.inject(['betterSidebar'], (sctx) => {
|
|
@@ -2748,13 +3032,13 @@ window.__ModuleLoader__.load({
|
|
|
2748
3032
|
title: () => 'Gallery',
|
|
2749
3033
|
icon: () => react.createElement('span', null, '🖼️'),
|
|
2750
3034
|
order: 45,
|
|
2751
|
-
component: ({ scope }) => react.createElement(ErrorBoundary, null, react.createElement(
|
|
3035
|
+
component: ({ scope }) => react.createElement(ErrorBoundary, null, react.createElement(ImageStudioView, { ctx, scope })),
|
|
2752
3036
|
})
|
|
2753
3037
|
} catch (e) {
|
|
2754
3038
|
console.warn('[dsh-image-gen] betterSidebar registerTab failed', e)
|
|
2755
3039
|
}
|
|
2756
3040
|
})
|
|
2757
|
-
} catch (
|
|
3041
|
+
} catch (_) { /* betterSidebar unavailable */ }
|
|
2758
3042
|
}
|
|
2759
3043
|
|
|
2760
3044
|
// Conversation header utilities chip (quick-access gallery button in chat header)
|
|
@@ -2771,25 +3055,7 @@ window.__ModuleLoader__.load({
|
|
|
2771
3055
|
)
|
|
2772
3056
|
)
|
|
2773
3057
|
|
|
2774
|
-
//
|
|
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.
|
|
2778
|
-
registerSlotWhenReady('plugins.item', () =>
|
|
2779
|
-
ctx.slots.register(
|
|
2780
|
-
{
|
|
2781
|
-
name: 'plugins.item',
|
|
2782
|
-
id: 'dsh-image-gen',
|
|
2783
|
-
order: 60,
|
|
2784
|
-
label: () => 'Image Studio',
|
|
2785
|
-
locale: NS,
|
|
2786
|
-
inject: () => cardOnce().inject(),
|
|
2787
|
-
},
|
|
2788
|
-
(props) => react.createElement(ErrorBoundary, null, react.createElement(FalSettingsCard, props))
|
|
2789
|
-
)
|
|
2790
|
-
)
|
|
2791
|
-
|
|
2792
|
-
// Settings card item (legacy seat, kept as a fallback)
|
|
3058
|
+
// Settings card item — sole slot registration for settings card (#208, #300)
|
|
2793
3059
|
registerSlotWhenReady('settings.plugin.item', () =>
|
|
2794
3060
|
ctx.slots.register(
|
|
2795
3061
|
{
|
|
@@ -2802,7 +3068,574 @@ window.__ModuleLoader__.load({
|
|
|
2802
3068
|
)
|
|
2803
3069
|
)
|
|
2804
3070
|
}
|
|
3071
|
+
// -------------------------------------------------------------- Asset Vault View (#159)
|
|
3072
|
+
function AssetVaultView(props) {
|
|
3073
|
+
const { ctx, onSelectAsset } = props
|
|
3074
|
+
const [items, setItems] = react.useState([])
|
|
3075
|
+
const [loading, setLoading] = react.useState(false)
|
|
3076
|
+
const [q, setQ] = react.useState('')
|
|
3077
|
+
const [provider, setProvider] = react.useState('all')
|
|
3078
|
+
const [aspect, setAspect] = react.useState('all')
|
|
3079
|
+
const [sort, setSort] = react.useState('newest')
|
|
3080
|
+
const [offset, setOffset] = react.useState(0)
|
|
3081
|
+
const [hasMore, setHasMore] = react.useState(false)
|
|
3082
|
+
const [selected, setSelected] = react.useState(null)
|
|
3083
|
+
const [feedback, setFeedback] = react.useState('')
|
|
3084
|
+
|
|
3085
|
+
const showFeedback = (msg) => {
|
|
3086
|
+
setFeedback(msg)
|
|
3087
|
+
setTimeout(() => setFeedback(''), 2500)
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
const loadItems = (newOffset = 0, append = false) => {
|
|
3091
|
+
setLoading(true)
|
|
3092
|
+
const params = new URLSearchParams({
|
|
3093
|
+
q,
|
|
3094
|
+
provider: provider === 'all' ? '' : provider,
|
|
3095
|
+
aspect: aspect === 'all' ? '' : aspect,
|
|
3096
|
+
sort,
|
|
3097
|
+
offset: String(newOffset),
|
|
3098
|
+
limit: '24',
|
|
3099
|
+
})
|
|
3100
|
+
fetch('/dsh-image-gen/vault?' + params.toString())
|
|
3101
|
+
.then((r) => r.json())
|
|
3102
|
+
.then((data) => {
|
|
3103
|
+
setLoading(false)
|
|
3104
|
+
if (data && data.ok) {
|
|
3105
|
+
setItems(append ? (prev) => [...prev, ...data.items] : data.items)
|
|
3106
|
+
setOffset(data.offset + data.items.length)
|
|
3107
|
+
setHasMore(data.hasMore)
|
|
3108
|
+
}
|
|
3109
|
+
})
|
|
3110
|
+
.catch(() => setLoading(false))
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
react.useEffect(() => {
|
|
3114
|
+
loadItems(0, false)
|
|
3115
|
+
}, [q, provider, aspect, sort])
|
|
3116
|
+
|
|
3117
|
+
const handleDelete = (id) => {
|
|
3118
|
+
if (!window.confirm('Are you sure you want to delete this asset?')) return
|
|
3119
|
+
fetch('/dsh-image-gen/vault?id=' + encodeURIComponent(id), { method: 'DELETE' })
|
|
3120
|
+
.then((r) => r.json())
|
|
3121
|
+
.then((res) => {
|
|
3122
|
+
if (res.ok) {
|
|
3123
|
+
setItems((prev) => prev.filter((it) => it.id !== id && it.attachmentId !== id))
|
|
3124
|
+
if (selected && (selected.id === id || selected.attachmentId === id)) {
|
|
3125
|
+
setSelected(null)
|
|
3126
|
+
}
|
|
3127
|
+
showFeedback('Asset deleted')
|
|
3128
|
+
}
|
|
3129
|
+
})
|
|
3130
|
+
.catch(() => showFeedback('Failed to delete asset'))
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
const handleInsert = (item) => {
|
|
3134
|
+
const link = ' + ')'
|
|
3135
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
3136
|
+
navigator.clipboard.writeText(link)
|
|
3137
|
+
showFeedback('Markdown snippet copied to clipboard!')
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
const handleReroll = (item) => {
|
|
3142
|
+
if (typeof onSelectAsset === 'function') {
|
|
3143
|
+
onSelectAsset(item)
|
|
3144
|
+
return
|
|
3145
|
+
}
|
|
3146
|
+
const cmd = '/image ' + (item.prompt || '') + (item.seed !== undefined ? ' --seed ' + item.seed : '')
|
|
3147
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
3148
|
+
navigator.clipboard.writeText(cmd)
|
|
3149
|
+
showFeedback('Prompt & seed copied to clipboard!')
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
return react.createElement(
|
|
3154
|
+
'div',
|
|
3155
|
+
{ className: 'ig-vault-container', style: { display: 'flex', flexDirection: 'column', gap: '14px', width: '100%' } },
|
|
3156
|
+
react.createElement(
|
|
3157
|
+
'div',
|
|
3158
|
+
{ className: 'ig-vault-header' },
|
|
3159
|
+
react.createElement('input', {
|
|
3160
|
+
type: 'text',
|
|
3161
|
+
className: 'ig-input',
|
|
3162
|
+
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)' },
|
|
3163
|
+
placeholder: 'Search prompt or tags...',
|
|
3164
|
+
value: q,
|
|
3165
|
+
onChange: (e) => setQ(e.target.value),
|
|
3166
|
+
}),
|
|
3167
|
+
react.createElement(
|
|
3168
|
+
'select',
|
|
3169
|
+
{
|
|
3170
|
+
className: 'ig-select',
|
|
3171
|
+
value: provider,
|
|
3172
|
+
onChange: (e) => setProvider(e.target.value),
|
|
3173
|
+
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)' },
|
|
3174
|
+
},
|
|
3175
|
+
react.createElement('option', { value: 'all' }, 'All Providers'),
|
|
3176
|
+
react.createElement('option', { value: 'fal' }, 'FAL.ai'),
|
|
3177
|
+
react.createElement('option', { value: 'openai' }, 'OpenAI'),
|
|
3178
|
+
react.createElement('option', { value: 'local' }, 'Local (ComfyUI)')
|
|
3179
|
+
),
|
|
3180
|
+
react.createElement(
|
|
3181
|
+
'select',
|
|
3182
|
+
{
|
|
3183
|
+
className: 'ig-select',
|
|
3184
|
+
value: aspect,
|
|
3185
|
+
onChange: (e) => setAspect(e.target.value),
|
|
3186
|
+
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)' },
|
|
3187
|
+
},
|
|
3188
|
+
react.createElement('option', { value: 'all' }, 'All Ratios'),
|
|
3189
|
+
react.createElement('option', { value: '1:1' }, '1:1 Square'),
|
|
3190
|
+
react.createElement('option', { value: '16:9' }, '16:9 Landscape'),
|
|
3191
|
+
react.createElement('option', { value: '9:16' }, '9:16 Portrait'),
|
|
3192
|
+
react.createElement('option', { value: '4:3' }, '4:3 Standard'),
|
|
3193
|
+
react.createElement('option', { value: '21:9' }, '21:9 Ultrawide')
|
|
3194
|
+
),
|
|
3195
|
+
react.createElement(
|
|
3196
|
+
'button',
|
|
3197
|
+
{
|
|
3198
|
+
type: 'button',
|
|
3199
|
+
className: 'ig-tab-btn',
|
|
3200
|
+
onClick: () => setSort(sort === 'newest' ? 'oldest' : 'newest'),
|
|
3201
|
+
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' },
|
|
3202
|
+
},
|
|
3203
|
+
sort === 'newest' ? '↓ Newest' : '↑ Oldest'
|
|
3204
|
+
)
|
|
3205
|
+
),
|
|
3206
|
+
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),
|
|
3207
|
+
items.length === 0 && !loading
|
|
3208
|
+
? react.createElement('div', { style: { padding: '32px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, 'No assets found in vault.')
|
|
3209
|
+
: react.createElement(
|
|
3210
|
+
'div',
|
|
3211
|
+
{ className: 'ig-vault-grid' },
|
|
3212
|
+
items.map((item) =>
|
|
3213
|
+
react.createElement(
|
|
3214
|
+
'div',
|
|
3215
|
+
{ key: item.id || item.attachmentId, className: 'ig-vault-card' },
|
|
3216
|
+
react.createElement('img', {
|
|
3217
|
+
src: item.thumbnailUrl || item.url,
|
|
3218
|
+
className: 'ig-vault-thumb',
|
|
3219
|
+
loading: 'lazy',
|
|
3220
|
+
onClick: () => setSelected(item),
|
|
3221
|
+
}),
|
|
3222
|
+
react.createElement(
|
|
3223
|
+
'div',
|
|
3224
|
+
{ className: 'ig-vault-body' },
|
|
3225
|
+
react.createElement('div', { className: 'ig-vault-prompt', title: item.prompt }, item.prompt || 'Untitled'),
|
|
3226
|
+
react.createElement(
|
|
3227
|
+
'div',
|
|
3228
|
+
{ className: 'ig-vault-meta' },
|
|
3229
|
+
react.createElement('span', null, item.provider || 'local'),
|
|
3230
|
+
item.seed !== undefined && react.createElement('span', null, 'Seed ' + item.seed)
|
|
3231
|
+
),
|
|
3232
|
+
react.createElement(
|
|
3233
|
+
'div',
|
|
3234
|
+
{ style: { display: 'flex', gap: '4px', marginTop: '6px' } },
|
|
3235
|
+
react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleReroll(item), title: 'Re-roll with seed' }, '🎲 Re-roll'),
|
|
3236
|
+
react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleInsert(item), title: 'Insert into chat' }, '💬 Insert'),
|
|
3237
|
+
react.createElement('button', { type: 'button', className: 'ig-rev-btn', onClick: () => handleDelete(item.id || item.attachmentId), title: 'Delete' }, '🗑️')
|
|
3238
|
+
)
|
|
3239
|
+
)
|
|
3240
|
+
)
|
|
3241
|
+
)
|
|
3242
|
+
),
|
|
3243
|
+
hasMore &&
|
|
3244
|
+
react.createElement(
|
|
3245
|
+
'div',
|
|
3246
|
+
{ style: { textAlign: 'center', margin: '14px 0' } },
|
|
3247
|
+
react.createElement(
|
|
3248
|
+
'button',
|
|
3249
|
+
{
|
|
3250
|
+
type: 'button',
|
|
3251
|
+
className: 'ig-tab-btn',
|
|
3252
|
+
onClick: () => loadItems(offset, true),
|
|
3253
|
+
disabled: loading,
|
|
3254
|
+
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' },
|
|
3255
|
+
},
|
|
3256
|
+
loading ? 'Loading...' : 'Load more assets'
|
|
3257
|
+
)
|
|
3258
|
+
),
|
|
3259
|
+
selected &&
|
|
3260
|
+
react.createElement(
|
|
3261
|
+
'div',
|
|
3262
|
+
{
|
|
3263
|
+
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' },
|
|
3264
|
+
onClick: () => setSelected(null),
|
|
3265
|
+
},
|
|
3266
|
+
react.createElement(
|
|
3267
|
+
'div',
|
|
3268
|
+
{
|
|
3269
|
+
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' },
|
|
3270
|
+
onClick: (e) => e.stopPropagation(),
|
|
3271
|
+
},
|
|
3272
|
+
react.createElement(
|
|
3273
|
+
'div',
|
|
3274
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3275
|
+
react.createElement('span', { style: { fontWeight: '700', fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } }, 'Asset Inspector'),
|
|
3276
|
+
react.createElement('button', { type: 'button', onClick: () => setSelected(null), style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px', color: 'var(--dsw-alias-label-secondary)' } }, '✕')
|
|
3277
|
+
),
|
|
3278
|
+
react.createElement('img', {
|
|
3279
|
+
src: selected.url || selected.thumbnailUrl,
|
|
3280
|
+
style: { width: '100%', borderRadius: '8px', maxHeight: '360px', objectFit: 'contain', background: 'var(--dsw-alias-bg-layer-2)' },
|
|
3281
|
+
}),
|
|
3282
|
+
react.createElement(
|
|
3283
|
+
'div',
|
|
3284
|
+
{ style: { display: 'flex', flexWrap: 'wrap', gap: '6px', fontSize: '11px' } },
|
|
3285
|
+
selected.seed !== undefined && react.createElement('span', { className: 'ig-badge' }, 'Seed: ' + selected.seed),
|
|
3286
|
+
selected.provider && react.createElement('span', { className: 'ig-badge' }, 'Provider: ' + selected.provider),
|
|
3287
|
+
selected.width && react.createElement('span', { className: 'ig-badge' }, selected.width + '×' + selected.height),
|
|
3288
|
+
selected.cost ? react.createElement('span', { className: 'ig-badge' }, '$' + Number(selected.cost).toFixed(4)) : null
|
|
3289
|
+
),
|
|
3290
|
+
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'),
|
|
3291
|
+
react.createElement(
|
|
3292
|
+
'div',
|
|
3293
|
+
{ style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '6px' } },
|
|
3294
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleReroll(selected) }, '🎲 Re-roll'),
|
|
3295
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleInsert(selected) }, '💬 Insert in Chat'),
|
|
3296
|
+
react.createElement(
|
|
3297
|
+
'a',
|
|
3298
|
+
{ href: selected.url || selected.thumbnailUrl, download: 'vault-asset-' + (selected.id || 'image') + '.png', className: 'ig-tab-btn', style: { textDecoration: 'none', display: 'inline-flex', alignItems: 'center' } },
|
|
3299
|
+
'⬇ Download'
|
|
3300
|
+
),
|
|
3301
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', style: { color: 'var(--dsw-alias-state-error-primary)' }, onClick: () => handleDelete(selected.id || selected.attachmentId) }, '🗑️ Delete')
|
|
3302
|
+
)
|
|
3303
|
+
)
|
|
3304
|
+
)
|
|
3305
|
+
)
|
|
3306
|
+
}
|
|
3307
|
+
// -------------------------------------------------------------- Fullscreen Image Studio (#158)
|
|
3308
|
+
function ImageStudioView(props) {
|
|
3309
|
+
const { ctx } = props
|
|
3310
|
+
const [viewMode, setViewMode] = react.useState('studio') // 'studio' | 'vault'
|
|
3311
|
+
const [isFullscreen, setIsFullscreen] = react.useState(false)
|
|
3312
|
+
const [layoutGrid, setLayoutGrid] = react.useState('1x1') // '1x1' | '2x2' | '1x4'
|
|
3313
|
+
const [prompt, setPrompt] = react.useState(() => {
|
|
3314
|
+
try { return window.localStorage.getItem('dsh_studio_prompt') || '' } catch (_) { return '' }
|
|
3315
|
+
})
|
|
3316
|
+
const [stylePreset, setStylePreset] = react.useState('none')
|
|
3317
|
+
const [aspectRatio, setAspectRatio] = react.useState('1:1')
|
|
3318
|
+
const [provider, setProvider] = react.useState('fal')
|
|
3319
|
+
const [seed, setSeed] = react.useState('')
|
|
3320
|
+
const [activeImages, setActiveImages] = react.useState([])
|
|
3321
|
+
const [recentStrip, setRecentStrip] = react.useState([])
|
|
3322
|
+
const [status, setStatus] = react.useState('')
|
|
3323
|
+
|
|
3324
|
+
const presets = [
|
|
3325
|
+
{ id: 'none', label: 'Default' },
|
|
3326
|
+
{ id: 'photorealistic', label: 'Photo' },
|
|
3327
|
+
{ id: 'anime', label: 'Anime' },
|
|
3328
|
+
{ id: 'cyberpunk', label: 'Cyberpunk' },
|
|
3329
|
+
{ id: 'flat_vector', label: 'Vector' },
|
|
3330
|
+
{ id: '3d_render', label: '3D Render' },
|
|
3331
|
+
]
|
|
3332
|
+
|
|
3333
|
+
const refreshRecent = () => {
|
|
3334
|
+
fetch('/dsh-image-gen/vault?limit=12')
|
|
3335
|
+
.then((r) => r.json())
|
|
3336
|
+
.then((data) => {
|
|
3337
|
+
if (data && data.ok && Array.isArray(data.items)) {
|
|
3338
|
+
setRecentStrip(data.items)
|
|
3339
|
+
if (activeImages.length === 0 && data.items.length > 0) {
|
|
3340
|
+
setActiveImages([data.items[0]])
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
})
|
|
3344
|
+
.catch(() => {})
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
react.useEffect(() => {
|
|
3348
|
+
refreshRecent()
|
|
3349
|
+
}, [])
|
|
3350
|
+
|
|
3351
|
+
const updatePrompt = (val) => {
|
|
3352
|
+
setPrompt(val)
|
|
3353
|
+
try { window.localStorage.setItem('dsh_studio_prompt', val) } catch (_) { /* storage unavailable */ }
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
const handleSelectFromVault = (asset) => {
|
|
3357
|
+
if (!asset) return
|
|
3358
|
+
setActiveImages([asset])
|
|
3359
|
+
if (asset.prompt) updatePrompt(asset.prompt)
|
|
3360
|
+
if (asset.seed !== undefined) setSeed(String(asset.seed))
|
|
3361
|
+
if (asset.provider) setProvider(asset.provider)
|
|
3362
|
+
setViewMode('studio')
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
const handleCopySnippet = (format) => {
|
|
3366
|
+
const first = activeImages[0]
|
|
3367
|
+
if (!first) return
|
|
3368
|
+
const url = first.url || first.thumbnailUrl
|
|
3369
|
+
let snippet = ''
|
|
3370
|
+
if (format === 'markdown') snippet = ''
|
|
3371
|
+
if (format === 'html') snippet = '<img src="' + url + '" alt="' + (first.prompt || 'Artwork') + '" />'
|
|
3372
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
3373
|
+
navigator.clipboard.writeText(snippet)
|
|
3374
|
+
setStatus('Copied ' + format.toUpperCase() + ' snippet!')
|
|
3375
|
+
setTimeout(() => setStatus(''), 2000)
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
const containerStyle = isFullscreen
|
|
3380
|
+
? { position: 'fixed', inset: 0, zIndex: 99999, background: 'var(--dsw-alias-bg-layer-1)', padding: '20px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '14px' }
|
|
3381
|
+
: { display: 'flex', flexDirection: 'column', gap: '14px', width: '100%' }
|
|
2805
3382
|
|
|
3383
|
+
return react.createElement(
|
|
3384
|
+
'div',
|
|
3385
|
+
{ className: 'ig-studio-container', style: containerStyle },
|
|
3386
|
+
react.createElement(
|
|
3387
|
+
'div',
|
|
3388
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '10px', borderBottom: '1px solid var(--dsw-alias-border-l2)', paddingBottom: '10px' } },
|
|
3389
|
+
react.createElement(
|
|
3390
|
+
'div',
|
|
3391
|
+
{ style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
3392
|
+
react.createElement('span', { style: { fontWeight: '700', fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, '🎨 Image Studio'),
|
|
3393
|
+
react.createElement(
|
|
3394
|
+
'button',
|
|
3395
|
+
{
|
|
3396
|
+
type: 'button',
|
|
3397
|
+
className: 'ig-tab-btn ' + (viewMode === 'studio' ? 'active' : ''),
|
|
3398
|
+
onClick: () => setViewMode('studio'),
|
|
3399
|
+
},
|
|
3400
|
+
'Studio Canvas'
|
|
3401
|
+
),
|
|
3402
|
+
react.createElement(
|
|
3403
|
+
'button',
|
|
3404
|
+
{
|
|
3405
|
+
type: 'button',
|
|
3406
|
+
className: 'ig-tab-btn ' + (viewMode === 'vault' ? 'active' : ''),
|
|
3407
|
+
onClick: () => setViewMode('vault'),
|
|
3408
|
+
},
|
|
3409
|
+
'🗃️ Asset Vault'
|
|
3410
|
+
)
|
|
3411
|
+
),
|
|
3412
|
+
react.createElement(
|
|
3413
|
+
'div',
|
|
3414
|
+
{ style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
3415
|
+
viewMode === 'studio' &&
|
|
3416
|
+
react.createElement(
|
|
3417
|
+
'div',
|
|
3418
|
+
{ style: { display: 'flex', gap: '4px' } },
|
|
3419
|
+
['1x1', '2x2', '1x4'].map((grid) =>
|
|
3420
|
+
react.createElement(
|
|
3421
|
+
'button',
|
|
3422
|
+
{
|
|
3423
|
+
key: grid,
|
|
3424
|
+
type: 'button',
|
|
3425
|
+
className: 'ig-tab-btn ' + (layoutGrid === grid ? 'active' : ''),
|
|
3426
|
+
onClick: () => setLayoutGrid(grid),
|
|
3427
|
+
title: 'Grid layout: ' + grid,
|
|
3428
|
+
},
|
|
3429
|
+
grid
|
|
3430
|
+
)
|
|
3431
|
+
)
|
|
3432
|
+
),
|
|
3433
|
+
react.createElement(
|
|
3434
|
+
'button',
|
|
3435
|
+
{
|
|
3436
|
+
type: 'button',
|
|
3437
|
+
className: 'ig-tab-btn',
|
|
3438
|
+
onClick: () => setIsFullscreen(!isFullscreen),
|
|
3439
|
+
style: { border: '1px solid var(--dsw-alias-border-l2)' },
|
|
3440
|
+
},
|
|
3441
|
+
isFullscreen ? '✕ Exit Fullscreen' : '⛶ Fullscreen'
|
|
3442
|
+
)
|
|
3443
|
+
)
|
|
3444
|
+
),
|
|
3445
|
+
viewMode === 'vault'
|
|
3446
|
+
? react.createElement(AssetVaultView, { ctx, onSelectAsset: handleSelectFromVault })
|
|
3447
|
+
: react.createElement(
|
|
3448
|
+
react.Fragment,
|
|
3449
|
+
null,
|
|
3450
|
+
react.createElement(
|
|
3451
|
+
'div',
|
|
3452
|
+
{ className: 'ig-studio-layout' },
|
|
3453
|
+
react.createElement(
|
|
3454
|
+
'div',
|
|
3455
|
+
{ className: 'ig-studio-sidebar' },
|
|
3456
|
+
react.createElement('span', { style: { fontWeight: '600', fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, 'Generation Parameters'),
|
|
3457
|
+
react.createElement(
|
|
3458
|
+
'div',
|
|
3459
|
+
null,
|
|
3460
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Prompt:'),
|
|
3461
|
+
react.createElement('textarea', {
|
|
3462
|
+
className: 'ig-input',
|
|
3463
|
+
rows: 4,
|
|
3464
|
+
style: { width: '100%', resize: 'vertical', borderRadius: '6px', padding: '8px', fontSize: '12px' },
|
|
3465
|
+
placeholder: 'Describe your vision...',
|
|
3466
|
+
value: prompt,
|
|
3467
|
+
onChange: (e) => updatePrompt(e.target.value),
|
|
3468
|
+
})
|
|
3469
|
+
),
|
|
3470
|
+
react.createElement(
|
|
3471
|
+
'div',
|
|
3472
|
+
null,
|
|
3473
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Style Preset:'),
|
|
3474
|
+
react.createElement(
|
|
3475
|
+
'div',
|
|
3476
|
+
{ style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
|
|
3477
|
+
presets.map((p) =>
|
|
3478
|
+
react.createElement(
|
|
3479
|
+
'button',
|
|
3480
|
+
{
|
|
3481
|
+
key: p.id,
|
|
3482
|
+
type: 'button',
|
|
3483
|
+
className: 'ig-tab-btn ' + (stylePreset === p.id ? 'active' : ''),
|
|
3484
|
+
style: { fontSize: '11px', padding: '3px 8px' },
|
|
3485
|
+
onClick: () => setStylePreset(p.id),
|
|
3486
|
+
},
|
|
3487
|
+
p.label
|
|
3488
|
+
)
|
|
3489
|
+
)
|
|
3490
|
+
)
|
|
3491
|
+
),
|
|
3492
|
+
react.createElement(
|
|
3493
|
+
'div',
|
|
3494
|
+
{ style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' } },
|
|
3495
|
+
react.createElement(
|
|
3496
|
+
'div',
|
|
3497
|
+
null,
|
|
3498
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Aspect Ratio:'),
|
|
3499
|
+
react.createElement(
|
|
3500
|
+
'select',
|
|
3501
|
+
{
|
|
3502
|
+
className: 'ig-select',
|
|
3503
|
+
style: { width: '100%', padding: '6px', fontSize: '12px' },
|
|
3504
|
+
value: aspectRatio,
|
|
3505
|
+
onChange: (e) => setAspectRatio(e.target.value),
|
|
3506
|
+
},
|
|
3507
|
+
react.createElement('option', { value: '1:1' }, '1:1 Square'),
|
|
3508
|
+
react.createElement('option', { value: '16:9' }, '16:9 Landscape'),
|
|
3509
|
+
react.createElement('option', { value: '9:16' }, '9:16 Portrait'),
|
|
3510
|
+
react.createElement('option', { value: '4:3' }, '4:3 Standard'),
|
|
3511
|
+
react.createElement('option', { value: '21:9' }, '21:9 Ultrawide')
|
|
3512
|
+
)
|
|
3513
|
+
),
|
|
3514
|
+
react.createElement(
|
|
3515
|
+
'div',
|
|
3516
|
+
null,
|
|
3517
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Provider:'),
|
|
3518
|
+
react.createElement(
|
|
3519
|
+
'select',
|
|
3520
|
+
{
|
|
3521
|
+
className: 'ig-select',
|
|
3522
|
+
style: { width: '100%', padding: '6px', fontSize: '12px' },
|
|
3523
|
+
value: provider,
|
|
3524
|
+
onChange: (e) => setProvider(e.target.value),
|
|
3525
|
+
},
|
|
3526
|
+
react.createElement('option', { value: 'fal' }, 'FAL.ai'),
|
|
3527
|
+
react.createElement('option', { value: 'openai' }, 'OpenAI'),
|
|
3528
|
+
react.createElement('option', { value: 'local' }, 'Local (ComfyUI)')
|
|
3529
|
+
)
|
|
3530
|
+
)
|
|
3531
|
+
),
|
|
3532
|
+
react.createElement(
|
|
3533
|
+
'div',
|
|
3534
|
+
null,
|
|
3535
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Seed (optional):'),
|
|
3536
|
+
react.createElement(
|
|
3537
|
+
'div',
|
|
3538
|
+
{ style: { display: 'flex', gap: '6px' } },
|
|
3539
|
+
react.createElement('input', {
|
|
3540
|
+
type: 'number',
|
|
3541
|
+
className: 'ig-input',
|
|
3542
|
+
style: { flex: 1, padding: '6px', fontSize: '12px' },
|
|
3543
|
+
placeholder: 'Random',
|
|
3544
|
+
value: seed,
|
|
3545
|
+
onChange: (e) => setSeed(e.target.value),
|
|
3546
|
+
}),
|
|
3547
|
+
react.createElement(
|
|
3548
|
+
'button',
|
|
3549
|
+
{
|
|
3550
|
+
type: 'button',
|
|
3551
|
+
className: 'ig-tab-btn',
|
|
3552
|
+
onClick: () => setSeed(String(Math.floor(Math.random() * 2147483647))),
|
|
3553
|
+
title: 'Randomize seed',
|
|
3554
|
+
},
|
|
3555
|
+
'🎲'
|
|
3556
|
+
)
|
|
3557
|
+
)
|
|
3558
|
+
),
|
|
3559
|
+
react.createElement(
|
|
3560
|
+
'button',
|
|
3561
|
+
{
|
|
3562
|
+
type: 'button',
|
|
3563
|
+
className: 'ig-save-btn',
|
|
3564
|
+
style: { marginTop: '8px', padding: '10px' },
|
|
3565
|
+
onClick: () => {
|
|
3566
|
+
const cmd = '/image ' + (prompt || 'Artwork') + (stylePreset !== 'none' ? ' --style ' + stylePreset : '') + ' --aspect ' + aspectRatio + ' --provider ' + provider + (seed ? ' --seed ' + seed : '')
|
|
3567
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
3568
|
+
navigator.clipboard.writeText(cmd)
|
|
3569
|
+
setStatus('Copied generation command to clipboard!')
|
|
3570
|
+
setTimeout(() => setStatus(''), 2500)
|
|
3571
|
+
}
|
|
3572
|
+
},
|
|
3573
|
+
},
|
|
3574
|
+
'🎨 Generate in Chat'
|
|
3575
|
+
),
|
|
3576
|
+
status && react.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-state-brand-primary)', textAlign: 'center' } }, status)
|
|
3577
|
+
),
|
|
3578
|
+
react.createElement(
|
|
3579
|
+
'div',
|
|
3580
|
+
{ className: 'ig-studio-canvas' },
|
|
3581
|
+
activeImages.length === 0
|
|
3582
|
+
? react.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, 'Select an image from the recent strip below or click Generate.')
|
|
3583
|
+
: react.createElement(
|
|
3584
|
+
'div',
|
|
3585
|
+
{
|
|
3586
|
+
style: {
|
|
3587
|
+
display: 'grid',
|
|
3588
|
+
gridTemplateColumns: layoutGrid === '2x2' ? 'repeat(2, 1fr)' : layoutGrid === '1x4' ? 'repeat(4, 1fr)' : '1fr',
|
|
3589
|
+
gap: '12px',
|
|
3590
|
+
width: '100%',
|
|
3591
|
+
maxHeight: '440px',
|
|
3592
|
+
overflowY: 'auto',
|
|
3593
|
+
},
|
|
3594
|
+
},
|
|
3595
|
+
(layoutGrid === '1x1' ? activeImages.slice(0, 1) : activeImages.slice(0, layoutGrid === '2x2' ? 4 : 4)).map((img, i) =>
|
|
3596
|
+
react.createElement('img', {
|
|
3597
|
+
key: img.id || i,
|
|
3598
|
+
src: img.url || img.thumbnailUrl,
|
|
3599
|
+
style: { width: '100%', height: 'auto', maxHeight: '400px', objectFit: 'contain', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-3)' },
|
|
3600
|
+
})
|
|
3601
|
+
)
|
|
3602
|
+
),
|
|
3603
|
+
activeImages.length > 0 &&
|
|
3604
|
+
react.createElement(
|
|
3605
|
+
'div',
|
|
3606
|
+
{ style: { display: 'flex', gap: '8px', marginTop: '10px' } },
|
|
3607
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('markdown') }, 'Copy Markdown'),
|
|
3608
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('html') }, 'Copy HTML'),
|
|
3609
|
+
react.createElement(
|
|
3610
|
+
'a',
|
|
3611
|
+
{ href: activeImages[0].url || activeImages[0].thumbnailUrl, download: 'studio-image.png', className: 'ig-tab-btn', style: { textDecoration: 'none' } },
|
|
3612
|
+
'⬇ Download'
|
|
3613
|
+
)
|
|
3614
|
+
)
|
|
3615
|
+
)
|
|
3616
|
+
),
|
|
3617
|
+
react.createElement(
|
|
3618
|
+
'div',
|
|
3619
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '10px' } },
|
|
3620
|
+
react.createElement('span', { style: { fontSize: '12px', fontWeight: '600', color: 'var(--dsw-alias-label-secondary)' } }, 'Recent Generations:'),
|
|
3621
|
+
react.createElement(
|
|
3622
|
+
'div',
|
|
3623
|
+
{ className: 'ig-studio-strip', style: { display: 'flex', gap: '8px', overflowX: 'auto', padding: '4px 0' } },
|
|
3624
|
+
recentStrip.map((item) =>
|
|
3625
|
+
react.createElement('img', {
|
|
3626
|
+
key: item.id || item.attachmentId,
|
|
3627
|
+
src: item.thumbnailUrl || item.url,
|
|
3628
|
+
className: 'ig-studio-strip-thumb',
|
|
3629
|
+
style: { width: '64px', height: '64px', borderRadius: '6px', objectFit: 'cover', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2)', flexShrink: 0 },
|
|
3630
|
+
title: item.prompt || 'Artwork',
|
|
3631
|
+
onClick: () => handleSelectFromVault(item),
|
|
3632
|
+
})
|
|
3633
|
+
)
|
|
3634
|
+
)
|
|
3635
|
+
)
|
|
3636
|
+
)
|
|
3637
|
+
)
|
|
3638
|
+
}
|
|
2806
3639
|
exports.apply = apply
|
|
2807
3640
|
exports.inject = inject
|
|
2808
3641
|
return module.exports
|