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