@goodandready/dsh-image-gen 0.10.19 → 0.10.20

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.
Files changed (2) hide show
  1. package/lib/client.js +132 -939
  2. package/package.json +3 -3
package/lib/client.js CHANGED
@@ -764,7 +764,137 @@ window.__ModuleLoader__.load({
764
764
  selected.seed !== undefined && react.createElement('span', { className: 'ig-badge' }, 'Seed: ' + selected.seed),
765
765
  selected.provider && react.createElement('span', { className: 'ig-badge' }, 'Provider: ' + selected.provider),
766
766
  selected.width && react.createElement('span', { className: 'ig-badge' }, selected.width + '×' + selected.height),
767
- selected.cost ? react.createElement('span', { className: 'ig-badge' }, '
767
+ selected.cost ? react.createElement('span', { className: 'ig-badge' }, '$' + Number(selected.cost).toFixed(4)) : null
768
+ ),
769
+ react.createElement(
770
+ 'div',
771
+ { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
772
+ react.createElement('span', { style: { fontSize: '11px', fontWeight: '600', color: 'var(--dsw-alias-label-secondary)' } }, 'Prompt:'),
773
+ react.createElement(
774
+ 'p',
775
+ {
776
+ style: {
777
+ fontSize: '12px',
778
+ color: 'var(--dsw-alias-label-primary)',
779
+ lineHeight: '1.4',
780
+ background: 'var(--dsw-alias-bg-layer-2)',
781
+ padding: '8px',
782
+ borderRadius: '6px',
783
+ margin: 0,
784
+ },
785
+ },
786
+ selected.prompt || 'No prompt recorded'
787
+ )
788
+ ),
789
+ react.createElement(
790
+ 'div',
791
+ { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '4px' } },
792
+ react.createElement(
793
+ 'button',
794
+ {
795
+ type: 'button',
796
+ className: 'ig-tab-btn',
797
+ style: { border: '1px solid var(--dsw-alias-border-l2)' },
798
+ onClick: () => {
799
+ if (typeof navigator !== 'undefined' && navigator.clipboard && selected.prompt) {
800
+ navigator.clipboard.writeText(selected.prompt)
801
+ setCopiedPrompt(true)
802
+ setTimeout(() => setCopiedPrompt(false), 2000)
803
+ }
804
+ },
805
+ },
806
+ copiedPrompt ? '✓ Copied!' : t('gallery.copy_prompt')
807
+ ),
808
+ react.createElement(
809
+ 'button',
810
+ {
811
+ type: 'button',
812
+ className: 'ig-save-btn',
813
+ onClick: () => {
814
+ const url = selected.thumbnailUrl || (selected.attachmentId ? '/dsh-image-gen/image?id=' + encodeURIComponent(selected.attachmentId) : '')
815
+ if (typeof navigator !== 'undefined' && navigator.clipboard && url) {
816
+ navigator.clipboard.writeText('![' + (selected.prompt || 'Image') + '](' + url + ')')
817
+ setCopiedLink(true)
818
+ setTimeout(() => setCopiedLink(false), 2000)
819
+ }
820
+ },
821
+ },
822
+ copiedLink ? '✓ Copied!' : t('gallery.copy_link')
823
+ )
824
+ )
825
+ )
826
+ )
827
+ )
828
+ }
829
+
830
+ // -------------------------------------------------------------- Header Quick-Access Chip
831
+ function GalleryHeaderChip(props) {
832
+ const [open, setOpen] = react.useState(false)
833
+ return react.createElement(
834
+ 'div',
835
+ { style: { display: 'inline-flex', alignItems: 'center', position: 'relative' } },
836
+ react.createElement(
837
+ 'button',
838
+ {
839
+ type: 'button',
840
+ title: 'Image Studio Gallery',
841
+ onClick: () => setOpen(!open),
842
+ style: {
843
+ appearance: 'none',
844
+ background: 'none',
845
+ border: '1px solid var(--dsw-alias-border-l2)',
846
+ borderRadius: '6px',
847
+ padding: '4px 8px',
848
+ cursor: 'pointer',
849
+ display: 'flex',
850
+ alignItems: 'center',
851
+ gap: '5px',
852
+ fontSize: '12px',
853
+ color: 'var(--dsw-alias-label-secondary)',
854
+ },
855
+ },
856
+ react.createElement('span', null, '🖼️'),
857
+ react.createElement('span', null, 'Gallery')
858
+ ),
859
+ open &&
860
+ react.createElement(
861
+ 'div',
862
+ {
863
+ style: {
864
+ position: 'fixed',
865
+ top: '56px',
866
+ right: '16px',
867
+ width: '380px',
868
+ maxHeight: '520px',
869
+ background: 'var(--dsw-alias-bg-layer-3)',
870
+ border: '1px solid var(--dsw-alias-border-l2)',
871
+ borderRadius: '12px',
872
+ boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
873
+ zIndex: 9999,
874
+ padding: '16px',
875
+ overflowY: 'auto',
876
+ },
877
+ },
878
+ react.createElement(
879
+ 'div',
880
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' } },
881
+ react.createElement('span', { style: { fontWeight: '700', fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } }, 'Image Studio Gallery'),
882
+ react.createElement(
883
+ 'button',
884
+ {
885
+ type: 'button',
886
+ onClick: () => setOpen(false),
887
+ style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '14px', color: 'var(--dsw-alias-label-secondary)' },
888
+ },
889
+ '✕'
890
+ )
891
+ ),
892
+ react.createElement(GalleryView, { ctx: props.ctx })
893
+ )
894
+ )
895
+ }
896
+
897
+ function FalSettingsCard(props) {
768
898
  const t = props.t || ((k) => k)
769
899
  const [open, setOpen] = react.useState(props.defaultOpen ?? true)
770
900
  const [activeTab, setActiveTab] = react.useState('general')
@@ -1832,941 +1962,4 @@ window.__ModuleLoader__.load({
1832
1962
  exports.inject = inject
1833
1963
  return module.exports
1834
1964
  },
1835
- })
1836
- + Number(selected.cost).toFixed(4)) : null
1837
- ),
1838
- react.createElement(
1839
- 'div',
1840
- { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
1841
- react.createElement('span', { style: { fontSize: '11px', fontWeight: '600', color: 'var(--dsw-alias-label-secondary)' } }, 'Prompt:'),
1842
- react.createElement(
1843
- 'p',
1844
- {
1845
- style: {
1846
- fontSize: '12px',
1847
- color: 'var(--dsw-alias-label-primary)',
1848
- lineHeight: '1.4',
1849
- background: 'var(--dsw-alias-bg-layer-2)',
1850
- padding: '8px',
1851
- borderRadius: '6px',
1852
- margin: 0,
1853
- },
1854
- },
1855
- selected.prompt || 'No prompt recorded'
1856
- )
1857
- ),
1858
- react.createElement(
1859
- 'div',
1860
- { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '4px' } },
1861
- react.createElement(
1862
- 'button',
1863
- {
1864
- type: 'button',
1865
- className: 'ig-tab-btn',
1866
- style: { border: '1px solid var(--dsw-alias-border-l2)' },
1867
- onClick: () => {
1868
- if (typeof navigator !== 'undefined' && navigator.clipboard && selected.prompt) {
1869
- navigator.clipboard.writeText(selected.prompt)
1870
- setCopiedPrompt(true)
1871
- setTimeout(() => setCopiedPrompt(false), 2000)
1872
- }
1873
- },
1874
- },
1875
- copiedPrompt ? '✓ Copied!' : t('gallery.copy_prompt')
1876
- ),
1877
- react.createElement(
1878
- 'button',
1879
- {
1880
- type: 'button',
1881
- className: 'ig-save-btn',
1882
- onClick: () => {
1883
- const url = selected.thumbnailUrl || (selected.attachmentId ? '/dsh-image-gen/image?id=' + encodeURIComponent(selected.attachmentId) : '')
1884
- if (typeof navigator !== 'undefined' && navigator.clipboard && url) {
1885
- navigator.clipboard.writeText('![' + (selected.prompt || 'Image') + '](' + url + ')')
1886
- setCopiedLink(true)
1887
- setTimeout(() => setCopiedLink(false), 2000)
1888
- }
1889
- },
1890
- },
1891
- copiedLink ? '✓ Copied!' : t('gallery.copy_link')
1892
- )
1893
- )
1894
- )
1895
- )
1896
- )
1897
- }
1898
-
1899
- // -------------------------------------------------------------- Header Quick-Access Chip
1900
- function GalleryHeaderChip(props) {
1901
- const [open, setOpen] = react.useState(false)
1902
- return react.createElement(
1903
- 'div',
1904
- { style: { display: 'inline-flex', alignItems: 'center', position: 'relative' } },
1905
- react.createElement(
1906
- 'button',
1907
- {
1908
- type: 'button',
1909
- title: 'Image Studio Gallery',
1910
- onClick: () => setOpen(!open),
1911
- style: {
1912
- appearance: 'none',
1913
- background: 'none',
1914
- border: '1px solid var(--dsw-alias-border-l2)',
1915
- borderRadius: '6px',
1916
- padding: '4px 8px',
1917
- cursor: 'pointer',
1918
- display: 'flex',
1919
- alignItems: 'center',
1920
- gap: '5px',
1921
- fontSize: '12px',
1922
- color: 'var(--dsw-alias-label-secondary)',
1923
- },
1924
- },
1925
- react.createElement('span', null, '🖼️'),
1926
- react.createElement('span', null, 'Gallery')
1927
- ),
1928
- open &&
1929
- react.createElement(
1930
- 'div',
1931
- {
1932
- style: {
1933
- position: 'fixed',
1934
- top: '56px',
1935
- right: '16px',
1936
- width: '380px',
1937
- maxHeight: '520px',
1938
- background: 'var(--dsw-alias-bg-layer-3)',
1939
- border: '1px solid var(--dsw-alias-border-l2)',
1940
- borderRadius: '12px',
1941
- boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
1942
- zIndex: 9999,
1943
- padding: '16px',
1944
- overflowY: 'auto',
1945
- },
1946
- },
1947
- react.createElement(
1948
- 'div',
1949
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' } },
1950
- react.createElement('span', { style: { fontWeight: '700', fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } }, 'Image Studio Gallery'),
1951
- react.createElement(
1952
- 'button',
1953
- {
1954
- type: 'button',
1955
- onClick: () => setOpen(false),
1956
- style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '14px', color: 'var(--dsw-alias-label-secondary)' },
1957
- },
1958
- '✕'
1959
- )
1960
- ),
1961
- react.createElement(GalleryView, { ctx: props.ctx })
1962
- )
1963
- )
1964
- }
1965
-
1966
- function FalSettingsCard(props) {
1967
- const t = props.t || ((k) => k)
1968
- const [open, setOpen] = react.useState(props.defaultOpen ?? true)
1969
- const [activeTab, setActiveTab] = react.useState('general')
1970
-
1971
- react.useEffect(() => {
1972
- ensureCss()
1973
- }, [])
1974
-
1975
- const state = (typeof props.useFalSettingsCard === 'function'
1976
- ? props.useFalSettingsCard((s) => s)
1977
- : null) || props.state || { available: true, writable: true, provider: { text: 'fal' } }
1978
-
1979
- const disabled = !state.writable
1980
- const currentProvider = (state.provider && state.provider.text) || 'fal'
1981
- const blocked = !state.dirty || state.invalid || state.saving
1982
-
1983
- const fieldProps = {
1984
- overriddenLabel: t('settings.overridden'),
1985
- resetLabel: t('settings.reset'),
1986
- invalidLabel: t('settings.invalidNumber'),
1987
- disabled,
1988
- }
1989
-
1990
- if (!state.available) {
1991
- return react.createElement(
1992
- 'li',
1993
- { className: 'ig-section-card', style: { listStyle: 'none' } },
1994
- react.createElement('p', { className: 'ig-field-hint' }, t('settings.unavailable'))
1995
- )
1996
- }
1997
-
1998
- const tabs = [
1999
- { id: 'general', label: t('tab.general'), icon: '⚙️' },
2000
- { id: 'provider', label: t('tab.provider') + ' (' + currentProvider.toUpperCase() + ')', icon: '🔌' },
2001
- { id: 'enhancer', label: t('tab.enhancer'), icon: '✨' },
2002
- { id: 'safety', label: t('tab.safety'), icon: '🛡️' },
2003
- { id: 'cache', label: t('tab.cache'), icon: '⚡' },
2004
- ]
2005
-
2006
- const currentTabFields = FIELDS.filter((entry) => {
2007
- if (entry.tab !== activeTab) return false
2008
- if (entry.when && !entry.when.includes(currentProvider)) return false
2009
- return true
2010
- })
2011
-
2012
- const diskCacheOn = state.diskCache && state.diskCache.text !== 'false'
2013
- const qualityGateOn = state.qualityGate && state.qualityGate.text !== 'false'
2014
- const budgetText = (state.dailyBudgetUsd && state.dailyBudgetUsd.text) || '0'
2015
- const loopLimit = (state.loopGuardLimit && state.loopGuardLimit.text) || '3'
2016
-
2017
- return react.createElement(
2018
- 'li',
2019
- { className: 'ig-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
2020
- // Header
2021
- react.createElement(
2022
- 'button',
2023
- {
2024
- type: 'button',
2025
- style: {
2026
- background: 'none',
2027
- border: 'none',
2028
- cursor: 'pointer',
2029
- display: 'flex',
2030
- alignItems: 'center',
2031
- width: '100%',
2032
- padding: 0,
2033
- textAlign: 'left',
2034
- },
2035
- 'aria-expanded': open,
2036
- onClick: () => setOpen(!open),
2037
- },
2038
- react.createElement(
2039
- 'div',
2040
- { style: { flex: 1 } },
2041
- react.createElement(
2042
- 'div',
2043
- { style: { fontWeight: 700, fontSize: '16px', display: 'flex', alignItems: 'center', gap: '8px' } },
2044
- '🎨 ' + t('settings.title')
2045
- ),
2046
- react.createElement(
2047
- 'div',
2048
- { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
2049
- t('settings.description')
2050
- )
2051
- ),
2052
- state.dirty
2053
- ? react.createElement('span', { className: 'ig-badge ig-badge-warn', style: { marginRight: '8px' } }, t('settings.unsaved'))
2054
- : null,
2055
- react.createElement(
2056
- 'span',
2057
- { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
2058
- ChevronIconNode
2059
- )
2060
- ),
2061
- open
2062
- ? react.createElement(
2063
- 'div',
2064
- { className: 'ig-page' },
2065
- // Quick Stats Row
2066
- react.createElement(
2067
- 'div',
2068
- { className: 'ig-grid-4' },
2069
- react.createElement(
2070
- 'div',
2071
- { className: 'ig-stat-box' },
2072
- react.createElement('div', { className: 'ig-stat-val' }, currentProvider.toUpperCase()),
2073
- react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.active_provider'))
2074
- ),
2075
- react.createElement(
2076
- 'div',
2077
- { className: 'ig-stat-box' },
2078
- react.createElement('div', { className: 'ig-stat-val' }, ((state.defaultSize && state.defaultSize.text) || '4:3') + ' · ' + ((state.defaultFormat && state.defaultFormat.text) || 'png')),
2079
- react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.format_size'))
2080
- ),
2081
- react.createElement(
2082
- 'div',
2083
- { className: 'ig-stat-box' },
2084
- react.createElement(
2085
- 'div',
2086
- { className: 'ig-stat-val' },
2087
- react.createElement(
2088
- 'span',
2089
- { className: qualityGateOn ? 'ig-badge ig-badge-ok' : 'ig-badge ig-badge-warn' },
2090
- qualityGateOn ? 'Gate Active' : 'Off'
2091
- ),
2092
- ' ',
2093
- react.createElement('span', { className: 'ig-badge ig-badge-ok' }, 'Guard ' + loopLimit)
2094
- ),
2095
- react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.safety_status'))
2096
- ),
2097
- react.createElement(
2098
- 'div',
2099
- { className: 'ig-stat-box' },
2100
- react.createElement(
2101
- 'div',
2102
- { className: 'ig-stat-val' },
2103
- react.createElement(
2104
- 'span',
2105
- { className: diskCacheOn ? 'ig-badge ig-badge-ok' : 'ig-badge ig-badge-warn' },
2106
- diskCacheOn ? 'Cache ON' : 'Off'
2107
- ),
2108
- ' ',
2109
- budgetText !== '0' ? '$' + budgetText : 'No limit'
2110
- ),
2111
- react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.budget_cache'))
2112
- )
2113
- ),
2114
- // Tab Navigation
2115
- react.createElement(
2116
- 'div',
2117
- { className: 'ig-tabs' },
2118
- tabs.map((tab) =>
2119
- react.createElement(
2120
- 'button',
2121
- {
2122
- key: tab.id,
2123
- type: 'button',
2124
- className: activeTab === tab.id ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
2125
- onClick: () => setActiveTab(tab.id),
2126
- },
2127
- tab.icon + ' ' + tab.label
2128
- )
2129
- )
2130
- ),
2131
- // Active Tab Fields
2132
- react.createElement(
2133
- 'div',
2134
- { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
2135
- currentTabFields.map((entry) =>
2136
- react.createElement(Field, {
2137
- key: entry.field,
2138
- entry,
2139
- state: state[entry.field] ?? { text: '', overridden: false, invalid: false },
2140
- fieldProps,
2141
- t,
2142
- onEdit: (val) => props.edit && props.edit(entry.field, val),
2143
- onReset: () => props.resetField && props.resetField(entry.field),
2144
- })
2145
- )
2146
- ),
2147
- // Footer Action Bar
2148
- react.createElement(
2149
- 'div',
2150
- {
2151
- style: {
2152
- display: 'flex',
2153
- justifyContent: 'flex-end',
2154
- alignItems: 'center',
2155
- gap: '10px',
2156
- paddingTop: '12px',
2157
- borderTop: '1px solid var(--dsw-alias-border-l2)',
2158
- },
2159
- },
2160
- state.failed
2161
- ? react.createElement('span', { className: 'ig-alert-err', style: { marginRight: 'auto', padding: '4px 10px' } }, t('settings.saveFailed'))
2162
- : null,
2163
- react.createElement(
2164
- 'button',
2165
- {
2166
- type: 'button',
2167
- className: 'ig-btn',
2168
- disabled: !state.dirty || state.saving,
2169
- onClick: props.discard,
2170
- },
2171
- t('settings.discard')
2172
- ),
2173
- react.createElement(
2174
- 'button',
2175
- {
2176
- type: 'button',
2177
- className: 'ig-btn ig-btn-primary',
2178
- disabled: blocked,
2179
- onClick: props.save,
2180
- },
2181
- t(state.saving ? 'settings.saving' : 'settings.save')
2182
- )
2183
- )
2184
- )
2185
- : null
2186
- )
2187
- }
2188
-
2189
- // -------------------------------------------------------------- Toolview Component
2190
- const IMAGE_URL_RE = /https?:\/\/[^\s)]+\.(png|jpg|jpeg|webp|gif)(\?[^\s)]*)?/i
2191
-
2192
- function readResult(block) {
2193
- if (!block) return { text: '', url: '', attachment: null }
2194
- const text = block.output || block.text || ''
2195
- const attachment = block.attachment || (block.attachments && block.attachments[0]) || null
2196
- const linked = text.match(IMAGE_URL_RE)
2197
- return {
2198
- attachment,
2199
- url: linked ? linked[0] : '',
2200
- text: linked ? text.replace(linked[0], '').trim() : text,
2201
- }
2202
- }
2203
-
2204
- function FalImageCard(props) {
2205
- const block = props.block
2206
- const running = !('kind' in (block || {}))
2207
- const failed = block && (block.isError || block.error !== undefined)
2208
- const parsed = readResult(block)
2209
- const t = props.t || ((k) => k)
2210
-
2211
- react.useEffect(() => {
2212
- ensureCss()
2213
- }, [])
2214
-
2215
- let args = {}
2216
- let prompt = ''
2217
- try {
2218
- const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
2219
- if (raw) {
2220
- args = JSON.parse(raw)
2221
- prompt = args.prompt || ''
2222
- }
2223
- } catch (_) {}
2224
-
2225
- const [copied, setCopied] = react.useState(false)
2226
-
2227
- const sendActionPrompt = async (text) => {
2228
- try {
2229
- const sessions = props.sessions
2230
- const current = sessions && sessions.list && sessions.list.current
2231
- const binding = current && sessions.binding(current)
2232
- const session = binding && binding.session
2233
- if (session && session.prompt) await session.prompt([{ type: 'text', text }], 'queue')
2234
- } catch (_) {}
2235
- }
2236
-
2237
- const onCopyPrompt = () => {
2238
- if (prompt && typeof navigator !== 'undefined' && navigator.clipboard) {
2239
- navigator.clipboard.writeText(prompt)
2240
- setCopied(true)
2241
- setTimeout(() => setCopied(false), 2000)
2242
- }
2243
- }
2244
-
2245
- const onReroll = () => {
2246
- const nextSeed = typeof args.seed === 'number' ? args.seed + 1 : Math.floor(Math.random() * 100000)
2247
- sendActionPrompt(t('card.actionReroll') + ' (' + nextSeed + '): ' + (prompt || ''))
2248
- }
2249
-
2250
- const onUpscale = () => {
2251
- const ref = parsed.attachment ? (parsed.attachment.attachmentId || parsed.attachment.id) : (parsed.url || '')
2252
- sendActionPrompt(t('card.actionUpscale') + ': upscale_image(image: "' + ref + '")')
2253
- }
2254
-
2255
- const onRemoveBg = () => {
2256
- const ref = parsed.attachment ? (parsed.attachment.attachmentId || parsed.attachment.id) : (parsed.url || '')
2257
- sendActionPrompt(t('card.actionRemoveBg') + ': remove_background(image: "' + ref + '")')
2258
- }
2259
-
2260
- const head = react.createElement(
2261
- 'div',
2262
- { className: 'fal_head' },
2263
- running ? '⏳ ' + t('card.generating') : failed ? '❌ ' + t('card.failed') : '🖼️ ' + t('card.image')
2264
- )
2265
-
2266
- const body = []
2267
- if (prompt) {
2268
- body.push(react.createElement('div', { className: 'fal-prompt', key: 'p' }, prompt))
2269
- }
2270
- if (failed) {
2271
- body.push(react.createElement('div', { className: 'fal-err', key: 'e' }, parsed.text || t('card.unknownError')))
2272
- } else if (!parsed.attachment && parsed.url) {
2273
- body.push(react.createElement('img', {
2274
- key: 'i',
2275
- src: parsed.url,
2276
- alt: prompt || 'generated image',
2277
- loading: 'lazy',
2278
- style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
2279
- }))
2280
- } else if (parsed.attachment) {
2281
- const a = parsed.attachment
2282
- const query = 'id=' + encodeURIComponent(a.attachmentId ?? a.id ?? '')
2283
- + '&mt=' + encodeURIComponent(a.mediaType || 'image/png')
2284
- + '&b=' + encodeURIComponent(String(a.bytes ?? 0))
2285
- + '&w=' + encodeURIComponent(String(a.width ?? 0))
2286
- + '&h=' + encodeURIComponent(String(a.height ?? 0))
2287
- body.push(react.createElement('img', {
2288
- key: 'i',
2289
- src: '/dsh-image-gen/image?' + query,
2290
- alt: prompt || 'generated image',
2291
- loading: 'lazy',
2292
- style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
2293
- }))
2294
- }
2295
-
2296
- const actions = []
2297
- if (!running && !failed && (parsed.url || parsed.attachment)) {
2298
- actions.push(
2299
- react.createElement('button', { key: 'reroll', type: 'button', className: 'ig-btn', onClick: onReroll }, '🎲 ' + t('card.reroll')),
2300
- react.createElement('button', { key: 'upscale', type: 'button', className: 'ig-btn', onClick: onUpscale }, '🔍 ' + t('card.upscale')),
2301
- react.createElement('button', { key: 'removeBg', type: 'button', className: 'ig-btn', onClick: onRemoveBg }, '✂️ ' + t('card.removeBg')),
2302
- react.createElement('button', { key: 'copy', type: 'button', className: 'ig-btn', onClick: onCopyPrompt }, copied ? '✓ Copied' : '📋 ' + t('card.copyPrompt'))
2303
- )
2304
- }
2305
-
2306
- return react.createElement(
2307
- 'div',
2308
- { className: 'ig-section-card', style: { margin: '8px 0' } },
2309
- head,
2310
- body,
2311
- actions.length > 0 ? react.createElement('div', { className: 'ig-row', style: { marginTop: '8px' } }, actions) : null
2312
- )
2313
- }
2314
-
2315
- // -------------------------------------------------------------- Dictionaries
2316
- const en = {
2317
- 'settings.navLabel': 'Image Studio',
2318
- 'settings.title': 'Image Studio',
2319
- 'settings.description': 'AI Image generation, inpainting, variations, upscale & vectorization.',
2320
- 'settings.unavailable': 'Plugin settings are initializing. They will appear automatically in a few seconds.',
2321
- 'settings.unsaved': 'Unsaved changes',
2322
- 'settings.saving': 'Saving…',
2323
- 'settings.save': 'Save settings',
2324
- 'settings.discard': 'Discard changes',
2325
- 'settings.saveFailed': 'Failed to save settings. Please check credentials and try again.',
2326
- 'settings.readOnly': 'Settings are read-only in this context.',
2327
- 'settings.overridden': 'overridden',
2328
- 'settings.reset': 'Reset to default',
2329
- 'settings.invalidNumber': 'Please enter a valid number',
2330
- 'settings.collapse': 'Collapse',
2331
- 'settings.expand': 'Expand',
2332
- 'settings.credentialHint': 'API keys are stored in DSH Credentials (~/.dsh/.credentials.yaml) and never leaked in config files.',
2333
-
2334
- 'tab.general': 'General',
2335
- 'tab.provider': 'Provider',
2336
- 'tab.enhancer': 'Prompt & Styles',
2337
- 'tab.safety': 'Safety & Budget',
2338
- 'tab.cache': 'Cache & Storage',
2339
-
2340
- 'stat.active_provider': 'Active Provider',
2341
- 'stat.format_size': 'Default Specs',
2342
- 'stat.safety_status': 'Safety & Loop Guard',
2343
- 'stat.budget_cache': 'Budget & Cache',
2344
-
2345
- 'card.generating': 'Generating image…',
2346
- 'card.failed': 'Generation failed',
2347
- 'card.image': 'Image Studio Result',
2348
- 'card.unknownError': 'An unknown error occurred during generation.',
2349
- 'card.sizePrefix': 'Size',
2350
- 'card.seedPrefix': 'Seed',
2351
- 'card.actionReroll': 'Reroll image',
2352
- 'card.actionUpscale': 'Upscale image',
2353
- 'card.actionRemoveBg': 'Remove background',
2354
- 'card.reroll': 'Reroll',
2355
- 'card.upscale': 'Upscale 2x/4x',
2356
- 'card.removeBg': 'Remove BG',
2357
- 'card.copyPrompt': 'Copy Prompt',
2358
- 'card.origPrompt': 'Original Prompt',
2359
-
2360
- 'f.enabled': 'Master Switch',
2361
- 'f.enabledHint': 'Enable or disable image generation tools across the workspace.',
2362
- 'f.provider': 'Image Provider',
2363
- 'f.providerHint': 'Backend service used for image generation.',
2364
- 'f.inherit': 'Default / Inherit',
2365
- 'f.needFal': 'Requires FAL API key',
2366
- 'f.needCustom': 'Requires OpenAI-compatible endpoint',
2367
- 'f.needCodex': 'Built-in Codex subscription',
2368
- 'f.needGrok': 'Built-in Grok subscription',
2369
- 'f.needLocal': 'Local ComfyUI or A1111',
2370
- 'f.needSeedream': 'Requires SeaDream API key',
2371
- 'f.needGemini': 'Requires Google Gemini API key',
2372
- 'f.needReplicate': 'Requires Replicate API token',
2373
-
2374
- 'f.defaultSize': 'Default Aspect Ratio',
2375
- 'f.defaultSizeHint': 'Aspect ratio and resolution applied when image_size is omitted.',
2376
- 'f.defaultFormat': 'Output Format',
2377
- 'f.defaultFormatHint': 'Target file format (PNG, JPEG, or WEBP).',
2378
- 'f.deliverAs': 'Delivery Mode',
2379
- 'f.deliverAsHint': 'How images reach conversation: "link" (text LLM safe) or "image" (multimodal vision).',
2380
- 'f.outputDir': 'Output Directory',
2381
- 'f.outputDirHint': 'Directory path where generated images are saved on disk.',
2382
- 'p.outputDir': 'generated/images',
2383
- 'f.historyLimit': 'History Limit',
2384
- 'f.historyLimitHint': 'Maximum number of recent generation records kept in memory.',
2385
- 'p.historyLimit': '50',
2386
-
2387
- 'f.model': 'FAL Model ID',
2388
- 'f.modelHint': 'Target model on FAL endpoint (e.g. fal-ai/flux-2/klein/9b).',
2389
- 'p.model': 'fal-ai/flux-2/klein/9b',
2390
- 'f.apiKeyEnv': 'FAL Key Reference',
2391
- 'f.apiKeyEnvHint': 'Credential reference holding the FAL API key.',
2392
- 'p.apiKeyEnv': 'FAL_API_KEY',
2393
- 'f.baseURL': 'FAL Base URL',
2394
- 'f.baseURLHint': 'FAL queue service endpoint.',
2395
- 'p.baseURL': 'https://queue.fal.run',
2396
- 'f.pollIntervalMs': 'Poll Interval (ms)',
2397
- 'f.pollIntervalMsHint': 'Frequency of checking task progress on asynchronous queues.',
2398
- 'p.pollIntervalMs': '2000',
2399
- 'f.timeoutMs': 'Timeout (ms)',
2400
- 'f.timeoutMsHint': 'Maximum overall timeout before failing a generation request.',
2401
- 'p.timeoutMs': '180000',
2402
-
2403
- 'f.customBaseURL': 'Custom API Base URL',
2404
- 'f.customBaseURLHint': 'OpenAI-compatible image endpoint root (e.g. https://api.openai.com/v1).',
2405
- 'p.customBaseURL': 'https://api.openai.com/v1',
2406
- 'f.customModel': 'Custom Model ID',
2407
- 'f.customModelHint': 'Model name sent to custom images endpoint.',
2408
- 'p.customModel': 'dall-e-3',
2409
- 'f.customKeyEnv': 'Custom Key Reference',
2410
- 'f.customKeyEnvHint': 'Credential reference holding the API key for custom endpoint.',
2411
- 'p.customKeyEnv': 'OPENAI_API_KEY',
2412
- 'f.customSize': 'Custom Image Size',
2413
- 'f.customSizeHint': 'Fixed dimension sent to custom endpoint (e.g. 1024x1024).',
2414
- 'p.customSize': '1024x1024',
2415
-
2416
- 'f.replicateModel': 'Replicate Model ID',
2417
- 'f.replicateModelHint': 'Target model identifier on Replicate.',
2418
- 'p.replicateModel': 'black-forest-labs/flux-schnell',
2419
- 'f.replicateKeyEnv': 'Replicate Key Reference',
2420
- 'f.replicateKeyEnvHint': 'Credential reference holding the Replicate API token.',
2421
- 'p.replicateKeyEnv': 'REPLICATE_API_TOKEN',
2422
-
2423
- 'f.seedreamModel': 'SeaDream Model ID',
2424
- 'f.seedreamModelHint': 'ByteDance SeaDream model identifier.',
2425
- 'p.seedreamModel': 'seedream-4.0',
2426
- 'f.seedreamKeyEnv': 'SeaDream Key Reference',
2427
- 'f.seedreamKeyEnvHint': 'Credential reference holding the SeaDream API key.',
2428
- 'p.seedreamKeyEnv': 'SEEDREAM_API_KEY',
2429
- 'f.seedreamBaseURL': 'SeaDream Base URL',
2430
- 'f.seedreamBaseURLHint': 'SeaDream API endpoint URL (ByteDance/Volcengine Ark).',
2431
- 'p.seedreamBaseURL': 'https://api.bytedanceapi.com/v1',
2432
-
2433
- 'f.geminiModel': 'Gemini Model ID',
2434
- 'f.geminiModelHint': 'Google Gemini image generation model identifier.',
2435
- 'p.geminiModel': 'gemini-2.0-flash-exp-image-generation',
2436
- 'f.geminiKeyEnv': 'Gemini Key Reference',
2437
- 'f.geminiKeyEnvHint': 'Credential reference holding Google Gemini API key.',
2438
- 'p.geminiKeyEnv': 'GEMINI_API_KEY',
2439
-
2440
- 'f.localKind': 'Local Engine Architecture',
2441
- 'f.localKindHint': 'Select between ComfyUI API or Automatic1111 WebUI backend.',
2442
- 'f.localBaseURL': 'Local Server URL',
2443
- 'f.localBaseURLHint': 'Server address (e.g. http://127.0.0.1:8188 for ComfyUI, http://127.0.0.1:7860 for A1111).',
2444
- 'p.localBaseURL': 'http://127.0.0.1:8188',
2445
- 'f.localModel': 'Local Model / Checkpoint',
2446
- 'f.localModelHint': 'Checkpoint name or ComfyUI workflow name.',
2447
- 'p.localModel': 'v1-5-pruned-emaonly.safetensors',
2448
- 'f.localSteps': 'Sampling Steps',
2449
- 'f.localStepsHint': 'Number of denoising sampling steps.',
2450
- 'p.localSteps': '20',
2451
- 'f.localCfg': 'CFG Scale',
2452
- 'f.localCfgHint': 'Classifier-free guidance scale.',
2453
- 'p.localCfg': '7',
2454
- 'f.localComfyUrl': 'Local ComfyUI URL (Legacy)',
2455
- 'f.localComfyUrlHint': 'Direct ComfyUI endpoint URL.',
2456
- 'p.localComfyUrl': 'http://127.0.0.1:8188',
2457
- 'f.localA1111Url': 'Local A1111 URL (Legacy)',
2458
- 'f.localA1111UrlHint': 'Direct Automatic1111 endpoint URL.',
2459
- 'p.localA1111Url': 'http://127.0.0.1:7860',
2460
-
2461
- 'f.subscriptionQuality': 'Subscription Quality',
2462
- 'f.subscriptionQualityHint': 'Target quality profile when using Codex or Grok subscriptions.',
2463
-
2464
- 'f.enhancePrompt': 'LLM Prompt Enhancer',
2465
- 'f.enhancePromptHint': 'Expand concise prompts with rich artistic details before generation.',
2466
- 'f.enableLlmEnhancer': 'Enable LLM Enhancer (Legacy)',
2467
- 'f.enableLlmEnhancerHint': 'Legacy toggle for LLM prompt expansion.',
2468
- 'f.enhanceModel': 'Enhancement Model',
2469
- 'f.enhanceModelHint': 'Dedicated LLM model used to expand prompts (empty = conversation model).',
2470
- 'p.enhanceModel': 'Leave empty for default chat model',
2471
- 'f.enhanceBelowChars': 'Enhancement Threshold',
2472
- 'f.enhanceBelowCharsHint': 'Prompts longer than this character count will skip expansion.',
2473
- 'p.enhanceBelowChars': '200',
2474
- 'f.stylePreset': 'Artistic Style Preset',
2475
- 'f.stylePresetHint': 'Automatic visual style suffix added to all generated prompts.',
2476
-
2477
- 'f.qualityGate': 'Silent Quality Gate',
2478
- 'f.qualityGateHint': 'Automatically inspects generated frames and silently re-rolls blank or broken outputs.',
2479
- 'f.dailyBudgetUsd': 'Daily Spend Budget ($ USD)',
2480
- 'f.dailyBudgetUsdHint': 'Hard spending cap per day across all visual generation tools (0 = no limit).',
2481
- 'p.dailyBudgetUsd': '0.00',
2482
- 'f.loopGuardLimit': 'Session Loop Guard Limit',
2483
- 'f.loopGuardLimitHint': 'Maximum consecutive image generations allowed before requiring user input (0 = off).',
2484
- 'p.loopGuardLimit': '3',
2485
-
2486
- 'f.diskCache': 'Content-Addressed Disk Cache',
2487
- 'f.diskCacheHint': 'Instant <50ms retrieval and zero API cost for repeated identical generations.',
2488
- 'f.cacheBySeed': 'Cache by Seed + Prompt',
2489
- 'f.cacheBySeedHint': 'Reuse existing result if the exact same seed and prompt are requested.',
2490
- 'f.cacheByPrompt': 'Cache by Prompt Only',
2491
- 'f.cacheByPromptHint': 'Reuse existing result if the prompt has already been generated.',
2492
- 'f.pruneDays': 'Retention Period (Days)',
2493
- 'f.pruneDaysHint': 'Automatically delete image files and cache older than this many days (0 = keep forever).',
2494
- 'p.pruneDays': '0',
2495
- 'f.cacheTtlDays': 'Cache Retention (Legacy)',
2496
- 'f.cacheTtlDaysHint': 'Legacy name for retention period in days.',
2497
- 'p.cacheTtlDays': '0',
2498
- }
2499
-
2500
- const ru = {
2501
- 'settings.navLabel': 'Image Studio',
2502
- 'settings.title': 'Генерация изображений',
2503
- 'settings.description': 'Мультипровайдерная студия: генерация, инпейнтинг, вариации, апскейл и векторизация.',
2504
- 'settings.unavailable': 'Настройки плагина инициализируются. Они появятся автоматически через несколько секунд.',
2505
- 'settings.unsaved': 'Несохранённые изменения',
2506
- 'settings.saving': 'Сохранение…',
2507
- 'settings.save': 'Сохранить настройки',
2508
- 'settings.discard': 'Сбросить изменения',
2509
- 'settings.saveFailed': 'Не удалось сохранить настройки. Проверьте параметры и попробуйте снова.',
2510
- 'settings.readOnly': 'Настройки доступны только для чтения.',
2511
- 'settings.overridden': 'переопределено',
2512
- 'settings.reset': 'Сбросить к умолчанию',
2513
- 'settings.invalidNumber': 'Введите корректное число',
2514
- 'settings.collapse': 'Свернуть',
2515
- 'settings.expand': 'Развернуть',
2516
- 'settings.credentialHint': 'Ключи API хранятся в DSH Credentials (~/.dsh/.credentials.yaml) и не попадают в файлы конфигурации.',
2517
-
2518
- 'tab.general': 'Основные',
2519
- 'tab.provider': 'Провайдер',
2520
- 'tab.enhancer': 'Промпт и стили',
2521
- 'tab.safety': 'Безопасность и бюджет',
2522
- 'tab.cache': 'Кэш и хранение',
2523
-
2524
- 'stat.active_provider': 'Активный провайдер',
2525
- 'stat.format_size': 'Параметры по умолчанию',
2526
- 'stat.safety_status': 'Защита и Loop Guard',
2527
- 'stat.budget_cache': 'Бюджет и кэширование',
2528
-
2529
- 'card.generating': 'Генерация изображения…',
2530
- 'card.failed': 'Ошибка генерации',
2531
- 'card.image': 'Результат Image Studio',
2532
- 'card.unknownError': 'Неизвестная ошибка во время создания изображения.',
2533
- 'card.sizePrefix': 'Размер',
2534
- 'card.seedPrefix': 'Сид',
2535
- 'card.actionReroll': 'Перегенерировать картинку',
2536
- 'card.actionUpscale': 'Увеличить разрешение',
2537
- 'card.actionRemoveBg': 'Удалить фон',
2538
- 'card.reroll': 'Реролл',
2539
- 'card.upscale': 'Апскейл 2x/4x',
2540
- 'card.removeBg': 'Без фона',
2541
- 'card.copyPrompt': 'Копировать промпт',
2542
- 'card.origPrompt': 'Исходный промпт',
2543
-
2544
- 'f.enabled': 'Главный выключатель',
2545
- 'f.enabledHint': 'Включение или отключение всех инструментов генерации картинок.',
2546
- 'f.provider': 'Провайдер генерации',
2547
- 'f.providerHint': 'Сервис, выполняющий непосредственное создание изображений.',
2548
- 'f.inherit': 'По умолчанию / наследовать',
2549
- 'f.needFal': 'Требуется ключ FAL API',
2550
- 'f.needCustom': 'Требуется OpenAI-совместимый эндпоинт',
2551
- 'f.needCodex': 'Встроенная подписка Codex',
2552
- 'f.needGrok': 'Встроенная подписка Grok',
2553
- 'f.needLocal': 'Локальный ComfyUI или Automatic1111',
2554
- 'f.needSeedream': 'Требуется ключ SeaDream API',
2555
- 'f.needGemini': 'Требуется ключ Google Gemini API',
2556
- 'f.needReplicate': 'Требуется токен Replicate API',
2557
-
2558
- 'f.defaultSize': 'Соотношение сторон по умолчанию',
2559
- 'f.defaultSizeHint': 'Разрешение и пропорции кадра, когда параметр image_size не передан.',
2560
- 'f.defaultFormat': 'Формат файла',
2561
- 'f.defaultFormatHint': 'Формат сохранения результатов (PNG, JPEG или WEBP).',
2562
- 'f.deliverAs': 'Способ доставки',
2563
- 'f.deliverAsHint': 'Способ передачи: "link" (безопасно для текстовых моделей) или "image" (для vision-моделей).',
2564
- 'f.outputDir': 'Каталог сохранения',
2565
- 'f.outputDirHint': 'Путь к папке сохранения сгенерированных файлов на диске.',
2566
- 'p.outputDir': 'generated/images',
2567
- 'f.historyLimit': 'Лимит истории',
2568
- 'f.historyLimitHint': 'Максимальное число последних генераций, удерживаемых в памяти.',
2569
- 'p.historyLimit': '50',
2570
-
2571
- 'f.model': 'Модель FAL',
2572
- 'f.modelHint': 'Идентификатор модели на сервисе FAL (например, fal-ai/flux-2/klein/9b).',
2573
- 'p.model': 'fal-ai/flux-2/klein/9b',
2574
- 'f.apiKeyEnv': 'Ключ FAL (credential-ref)',
2575
- 'f.apiKeyEnvHint': 'Ссылка на ключ доступа FAL в хранилище credentials.',
2576
- 'p.apiKeyEnv': 'FAL_API_KEY',
2577
- 'f.baseURL': 'Базовый URL FAL',
2578
- 'f.baseURLHint': 'Адрес сервиса очередей FAL.',
2579
- 'p.baseURL': 'https://queue.fal.run',
2580
- 'f.pollIntervalMs': 'Интервал опроса (мс)',
2581
- 'f.pollIntervalMsHint': 'Периодичность проверки статуса при асинхронной генерации.',
2582
- 'p.pollIntervalMs': '2000',
2583
- 'f.timeoutMs': 'Таймаут (мс)',
2584
- 'f.timeoutMsHint': 'Предельное время ожидания генерации до возврата ошибки.',
2585
- 'p.timeoutMs': '180000',
2586
-
2587
- 'f.customBaseURL': 'URL стороннего API',
2588
- 'f.customBaseURLHint': 'Адрес OpenAI-совместимого эндпоинта (например, https://api.openai.com/v1).',
2589
- 'p.customBaseURL': 'https://api.openai.com/v1',
2590
- 'f.customModel': 'Модель стороннего API',
2591
- 'f.customModelHint': 'Имя модели для отправки в сторонний API.',
2592
- 'p.customModel': 'dall-e-3',
2593
- 'f.customKeyEnv': 'Ключ стороннего API',
2594
- 'f.customKeyEnvHint': 'Имя ссылки на ключ для стороннего API.',
2595
- 'p.customKeyEnv': 'OPENAI_API_KEY',
2596
- 'f.customSize': 'Фиксированный размер',
2597
- 'f.customSizeHint': 'Точный размер кадра для стороннего API (например, 1024x1024).',
2598
- 'p.customSize': '1024x1024',
2599
-
2600
- 'f.replicateModel': 'Модель Replicate',
2601
- 'f.replicateModelHint': 'Идентификатор модели на Replicate.',
2602
- 'p.replicateModel': 'black-forest-labs/flux-schnell',
2603
- 'f.replicateKeyEnv': 'Ключ Replicate',
2604
- 'f.replicateKeyEnvHint': 'Имя ссылки на API-токен Replicate.',
2605
- 'p.replicateKeyEnv': 'REPLICATE_API_TOKEN',
2606
-
2607
- 'f.seedreamModel': 'Модель SeaDream',
2608
- 'f.seedreamModelHint': 'Идентификатор модели ByteDance SeaDream.',
2609
- 'p.seedreamModel': 'seedream-4.0',
2610
- 'f.seedreamKeyEnv': 'Ключ SeaDream',
2611
- 'f.seedreamKeyEnvHint': 'Имя ссылки на ключ API SeaDream.',
2612
- 'p.seedreamKeyEnv': 'SEEDREAM_API_KEY',
2613
- 'f.seedreamBaseURL': 'Базовый URL SeaDream',
2614
- 'f.seedreamBaseURLHint': 'Адрес API SeaDream (ByteDance/Volcengine Ark).',
2615
- 'p.seedreamBaseURL': 'https://api.bytedanceapi.com/v1',
2616
-
2617
- 'f.geminiModel': 'Модель Gemini',
2618
- 'f.geminiModelHint': 'Идентификатор модели генерации Google Gemini.',
2619
- 'p.geminiModel': 'gemini-2.0-flash-exp-image-generation',
2620
- 'f.geminiKeyEnv': 'Ключ Gemini',
2621
- 'f.geminiKeyEnvHint': 'Имя ссылки на ключ Google Gemini API.',
2622
- 'p.geminiKeyEnv': 'GEMINI_API_KEY',
2623
-
2624
- 'f.localKind': 'Тип локального движка',
2625
- 'f.localKindHint': 'Выбор между ComfyUI API и Automatic1111 WebUI.',
2626
- 'f.localBaseURL': 'Адрес локального сервера',
2627
- 'f.localBaseURLHint': 'Адрес локального сервера (ComfyUI: 8188, A1111: 7860).',
2628
- 'p.localBaseURL': 'http://127.0.0.1:8188',
2629
- 'f.localModel': 'Модель / Чекпоинт',
2630
- 'f.localModelHint': 'Имя чекпоинта или воркфлоу ComfyUI.',
2631
- 'p.localModel': 'v1-5-pruned-emaonly.safetensors',
2632
- 'f.localSteps': 'Шаги сэмплирования',
2633
- 'f.localStepsHint': 'Количество шагов генерации.',
2634
- 'p.localSteps': '20',
2635
- 'f.localCfg': 'Шкала CFG',
2636
- 'f.localCfgHint': 'Сила следования промпту.',
2637
- 'p.localCfg': '7',
2638
- 'f.localComfyUrl': 'Локальный ComfyUI URL (Legacy)',
2639
- 'f.localComfyUrlHint': 'Прямой адрес ComfyUI.',
2640
- 'p.localComfyUrl': 'http://127.0.0.1:8188',
2641
- 'f.localA1111Url': 'Локальный A1111 URL (Legacy)',
2642
- 'f.localA1111UrlHint': 'Прямой адрес Automatic1111.',
2643
- 'p.localA1111Url': 'http://127.0.0.1:7860',
2644
-
2645
- 'f.subscriptionQuality': 'Качество подписки',
2646
- 'f.subscriptionQualityHint': 'Профиль качества для провайдеров Codex и Grok.',
2647
-
2648
- 'f.enhancePrompt': 'LLM-улучшение промпта',
2649
- 'f.enhancePromptHint': 'Автоматическое обогащение коротких запросов художественными деталями.',
2650
- 'f.enableLlmEnhancer': 'Включить LLM Enhancer (Legacy)',
2651
- 'f.enableLlmEnhancerHint': 'Устаревший переключатель улучшения промптов.',
2652
- 'f.enhanceModel': 'Модель улучшения',
2653
- 'f.enhanceModelHint': 'Модель для расширения промптов (пусто = основная модель чата).',
2654
- 'p.enhanceModel': 'Пусто для основной модели чата',
2655
- 'f.enhanceBelowChars': 'Порог длины промпта',
2656
- 'f.enhanceBelowCharsHint': 'Промпты длиннее этого количества символов не расширяются.',
2657
- 'p.enhanceBelowChars': '200',
2658
- 'f.stylePreset': 'Художественный стиль',
2659
- 'f.stylePresetHint': 'Автоматический суффикс стиля для всех создаваемых изображений.',
2660
-
2661
- 'f.qualityGate': 'Контроль качества (Quality Gate)',
2662
- 'f.qualityGateHint': 'Автоматическая проверка кадров и скрытый реролл при дефектах или пустом кадре.',
2663
- 'f.dailyBudgetUsd': 'Дневной бюджет ($ USD)',
2664
- 'f.dailyBudgetUsdHint': 'Предельный лимит затрат в сутки по всем графическим инструментам (0 = без лимита).',
2665
- 'p.dailyBudgetUsd': '0.00',
2666
- 'f.loopGuardLimit': 'Защита от циклов (Loop Guard)',
2667
- 'f.loopGuardLimitHint': 'Максимум последовательных генераций подряд без участия пользователя (0 = выкл).',
2668
- 'p.loopGuardLimit': '3',
2669
-
2670
- 'f.diskCache': 'Дисковый кэш по хэшу',
2671
- 'f.diskCacheHint': 'Мгновенная выдача (<50 мс) и нулевая стоимость для повторных идентичных генераций.',
2672
- 'f.cacheBySeed': 'Кэш по сиду и промпту',
2673
- 'f.cacheBySeedHint': 'Возвращать кэш при полном совпадении сида и текста промпта.',
2674
- 'f.cacheByPrompt': 'Кэш по тексту промпта',
2675
- 'f.cacheByPromptHint': 'Возвращать кэш при повторении того же текста промпта.',
2676
- 'f.pruneDays': 'Срок хранения файлов (дни)',
2677
- 'f.pruneDaysHint': 'Автоматическое удаление файлов и записей старше указанного числа дней (0 = бессрочно).',
2678
- 'p.pruneDays': '0',
2679
- 'f.cacheTtlDays': 'Срок хранения кэша (Legacy)',
2680
- 'f.cacheTtlDaysHint': 'Устаревшее имя параметра срока хранения.',
2681
- 'p.cacheTtlDays': '0',
2682
- }
2683
-
2684
- // -------------------------------------------------------------- Registration & Apply
2685
- const inject = ['slots', 'settingsScope', 'locale', 'sessions']
2686
-
2687
- function apply(ctx) {
2688
- if (ctx.locale && typeof ctx.locale.define === 'function') {
2689
- ctx.locale.define('en', NS, en)
2690
- ctx.locale.define('ru', NS, ru)
2691
- }
2692
-
2693
- let card
2694
- const cardOnce = () => {
2695
- if (card === undefined) {
2696
- const scope = ((ctx.get && ctx.get('lanSettings')) || ctx.settingsScope).bind({ namespace: SETTINGS_NS })
2697
- card = new FalSettingsCardController(scope)
2698
- }
2699
- return card
2700
- }
2701
-
2702
- function registerSlotWhenReady(slotName, registerFn) {
2703
- if (!ctx.slots) return
2704
- if (typeof ctx.slots.inject === 'function') {
2705
- try {
2706
- ctx.slots.inject(slotName, () => {
2707
- try {
2708
- return registerFn()
2709
- } catch (err) {
2710
- console.warn('[dsh-image-gen] Error registering slot ' + slotName + ':', err)
2711
- }
2712
- })
2713
- return
2714
- } catch (err) {
2715
- console.warn('[dsh-image-gen] Failed to inject slot ' + slotName + ':', err)
2716
- }
2717
- }
2718
- if (typeof ctx.slots.register === 'function') {
2719
- try {
2720
- registerFn()
2721
- } catch (err) {
2722
- console.warn('[dsh-image-gen] Failed direct registration for ' + slotName + ':', err)
2723
- }
2724
- }
2725
- }
2726
-
2727
- // Register toolviews for all 8 visual tools
2728
- const toolviewTools = [
2729
- 'generate_image',
2730
- 'edit_image',
2731
- 'vary_image',
2732
- 'blend_images',
2733
- 'generate_image_pack',
2734
- 'remove_background',
2735
- 'upscale_image',
2736
- 'vectorize_image',
2737
- 'assemble_image_grid',
2738
- ]
2739
-
2740
- for (const toolName of toolviewTools) {
2741
- registerSlotWhenReady('tool.call.toolview', () =>
2742
- ctx.slots.register(
2743
- {
2744
- name: 'tool.call.toolview',
2745
- key: toolName,
2746
- locale: NS,
2747
- inject: () => ({ sessions: ctx.sessions }),
2748
- },
2749
- (props) => react.createElement(ErrorBoundary, null, react.createElement(FalImageCard, props))
2750
- )
2751
- )
2752
- }
2753
-
2754
- // Settings card item
2755
- registerSlotWhenReady('settings.plugin.item', () =>
2756
- ctx.slots.register(
2757
- {
2758
- name: 'settings.plugin.item',
2759
- key: SETTINGS_NS,
2760
- locale: NS,
2761
- inject: () => cardOnce().inject(),
2762
- },
2763
- (props) => react.createElement(ErrorBoundary, null, react.createElement(FalSettingsCard, props))
2764
- )
2765
- )
2766
- }
2767
-
2768
- exports.apply = apply
2769
- exports.inject = inject
2770
- return module.exports
2771
- },
2772
- })
1965
+ })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.10.19",
4
- "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers \u2014 the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
3
+ "version": "0.10.20",
4
+ "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -66,6 +66,6 @@
66
66
  "@deepseek-ai/schemastery": "^3.18.1"
67
67
  },
68
68
  "scripts": {
69
- "test": "node --test test/*.test.mjs"
69
+ "test": "node --check lib/client.js && node --test test/*.test.mjs"
70
70
  }
71
71
  }