@goodandready/dsh-image-gen 0.10.12 → 0.10.14
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/README.md +12 -0
- package/README.ru.md +12 -0
- package/lib/client.js +1058 -1050
- package/lib/index.js +51 -10
- package/lib/providers.js +1 -1
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -1,285 +1,183 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Renders a configuration card inside Web GUI 设置 → 插件 → 插件配置,
|
|
4
|
-
// editing the `dsh-image-gen` dsh-settings namespace that the Host
|
|
5
|
-
// plugin registers. The card is a hand-written ModuleLoader module: it
|
|
6
|
-
// depends only on services the official client runtime provides (`slots`,
|
|
7
|
-
// `settingsScope`, `locale`, `react`), so it ships inside this npm package
|
|
8
|
-
// with no build step. The card chrome mirrors the official plugin-card
|
|
9
|
-
// chrome in a self-contained slice.
|
|
10
|
-
|
|
1
|
+
// Plugin Settings Card (settings.plugin.item) and Toolviews for @goodandready/dsh-image-gen
|
|
11
2
|
window.__ModuleLoader__.load({
|
|
12
3
|
id: '@goodandready/dsh-image-gen',
|
|
13
4
|
factory: (require) => {
|
|
14
5
|
var module = { exports: {} }
|
|
15
6
|
var exports = module.exports
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
7
|
+
const react = require('react')
|
|
8
|
+
const React = react
|
|
9
|
+
|
|
10
|
+
let react_jsx_runtime = null
|
|
11
|
+
try {
|
|
12
|
+
react_jsx_runtime = require('react/jsx-runtime')
|
|
13
|
+
} catch (_) {
|
|
14
|
+
react_jsx_runtime = {
|
|
15
|
+
jsx: (type, props, key) => react.createElement(type, key !== undefined ? { ...props, key } : props),
|
|
16
|
+
jsxs: (type, props, key) => react.createElement(type, key !== undefined ? { ...props, key } : props),
|
|
25
17
|
}
|
|
26
|
-
return null
|
|
27
18
|
}
|
|
28
|
-
|
|
19
|
+
|
|
20
|
+
const NS = 'dsh-image-gen'
|
|
21
|
+
const SETTINGS_NS = 'dsh-image-gen'
|
|
22
|
+
const TITLE = 'Image Studio'
|
|
23
|
+
const SUBTITLE = 'Multi-provider AI image generation, editing, upscale & vectorization.'
|
|
29
24
|
|
|
30
25
|
// Ядровый шеврон раскрытия; в урезанной сборке — запасной SVG той же формы.
|
|
31
26
|
let ChevronIcon = null
|
|
32
27
|
try {
|
|
33
28
|
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
34
29
|
ChevronIcon = primitives && primitives.IconChevronDownOutline14
|
|
35
|
-
} catch (
|
|
30
|
+
} catch (_) {
|
|
36
31
|
ChevronIcon = null
|
|
37
32
|
}
|
|
38
|
-
const ChevronIconNode = ChevronIcon
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------- css
|
|
49
|
-
const css =
|
|
50
|
-
'.fal_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
51
|
-
'.fal_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}' +
|
|
52
|
-
'.fal_head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
53
|
-
'.fal_head:hover{background:var(--dsw-alias-interactive-bg-hover)}' +
|
|
54
|
-
'.fal_headText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}' +
|
|
55
|
-
'.fal_title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
56
|
-
'.fal_sub{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
57
|
-
'.fal_pending{color:var(--dsw-alias-state-warn-primary);font-size:13px}' +
|
|
58
|
-
'.fal_chevron{color:var(--dsw-alias-label-tertiary);transition:transform .12s}' +
|
|
59
|
-
'.fal_chevronOpen{transform:rotate(180deg)}' +
|
|
60
|
-
'.fal_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}' +
|
|
61
|
-
'.fal_readOnly{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px;padding:12px 0}' +
|
|
62
|
-
'.fal_foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px}' +
|
|
63
|
-
'.fal_failed{color:var(--dsw-alias-state-error-primary);margin:0 auto 0 0;font-size:13px}' +
|
|
64
|
-
'.fal_discard,.fal_save{appearance:none;font:inherit;cursor:pointer;border-radius:8px;padding:5px 14px;font-size:13px}' +
|
|
65
|
-
'.fal_discard{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}' +
|
|
66
|
-
'.fal_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}' +
|
|
67
|
-
'.fal_save{border:1px solid transparent;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}' +
|
|
68
|
-
'.fal_save:hover:not(:disabled){opacity:.9}' +
|
|
69
|
-
'.fal_discard:active:not(:disabled),.fal_save:active:not(:disabled){transform:translateY(1px)}' +
|
|
70
|
-
'.fal_discard:focus-visible:not(:disabled),.fal_save:focus-visible:not(:disabled){outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}' +
|
|
71
|
-
'.fal_discard:disabled,.fal_save:disabled{opacity:.5;cursor:default}' +
|
|
72
|
-
'.fal_field{display:flex;flex-direction:column;gap:6px;padding:12px 0}' +
|
|
73
|
-
'.fal_fieldHead{align-items:center;gap:12px;display:flex}' +
|
|
74
|
-
'.fal_label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}' +
|
|
75
|
-
'.fal_badges{align-items:center;gap:6px;display:flex}' +
|
|
76
|
-
'.fal_badge{background:var(--dsw-alias-interactive-bg-hover-accent);color:var(--dsw-alias-state-business-primary);border-radius:999px;padding:1px 6px;font-size:11px}' +
|
|
77
|
-
'.fal_reset{color:var(--dsw-alias-state-business-primary);cursor:pointer;background:0 0;border:0;padding:0;font-size:11px}' +
|
|
78
|
-
'.fal_reset:hover:not(:disabled){text-decoration:underline}' +
|
|
79
|
-
'.fal_reset:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}' +
|
|
80
|
-
'.fal_input,.fal_select{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}' +
|
|
81
|
-
'.fal_inputInvalid{border:1px solid var(--dsw-alias-state-error-primary);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}' +
|
|
82
|
-
'.fal_input:disabled,.fal_select:disabled{opacity:.6}' +
|
|
83
|
-
'.fal_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}' +
|
|
84
|
-
'.fal_invalid{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}' +
|
|
85
|
-
'.fal_sep{height:1px;background:var(--dsw-alias-border-l2);margin:2px 0 0}'
|
|
86
|
-
const tagId = 'dsh-image-gen/settings-card.module.css'
|
|
87
|
-
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {
|
|
88
|
-
const tag = document.createElement('style')
|
|
89
|
-
tag.dataset.plugin = 'dsh-image-gen'
|
|
90
|
-
tag.dataset.pluginCss = tagId
|
|
91
|
-
tag.textContent = css
|
|
92
|
-
document.head.appendChild(tag)
|
|
93
|
-
}
|
|
94
|
-
const cssDefault = {
|
|
95
|
-
badge: 'fal_badge',
|
|
96
|
-
badges: 'fal_badges',
|
|
97
|
-
body: 'fal_body',
|
|
98
|
-
card: 'fal_card',
|
|
99
|
-
cardOpen: 'fal_cardOpen',
|
|
100
|
-
chevron: 'fal_chevron',
|
|
101
|
-
chevronOpen: 'fal_chevronOpen',
|
|
102
|
-
description: 'fal_sub',
|
|
103
|
-
discard: 'fal_discard',
|
|
104
|
-
failed: 'fal_failed',
|
|
105
|
-
field: 'fal_field',
|
|
106
|
-
footer: 'fal_foot',
|
|
107
|
-
head: 'fal_fieldHead',
|
|
108
|
-
header: 'fal_head',
|
|
109
|
-
headText: 'fal_headText',
|
|
110
|
-
hint: 'fal_hint',
|
|
111
|
-
input: 'fal_input',
|
|
112
|
-
inputInvalid: 'fal_inputInvalid',
|
|
113
|
-
invalid: 'fal_invalid',
|
|
114
|
-
label: 'fal_label',
|
|
115
|
-
name: 'fal_title',
|
|
116
|
-
pending: 'fal_pending',
|
|
117
|
-
readOnly: 'fal_readOnly',
|
|
118
|
-
reset: 'fal_reset',
|
|
119
|
-
save: 'fal_save',
|
|
120
|
-
select: 'fal_select',
|
|
121
|
-
sep: 'fal_sep',
|
|
122
|
-
}
|
|
33
|
+
const ChevronIconNode = ChevronIcon
|
|
34
|
+
? react.createElement(ChevronIcon)
|
|
35
|
+
: react.createElement('svg', {
|
|
36
|
+
width: 14, height: 14, viewBox: '0 0 14 14', 'aria-hidden': true,
|
|
37
|
+
style: { display: 'block' },
|
|
38
|
+
}, react.createElement('path', {
|
|
39
|
+
d: 'M3 5.5 L7 9.5 L11 5.5',
|
|
40
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: 1.5,
|
|
41
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
42
|
+
}))
|
|
123
43
|
|
|
124
|
-
// --------------------------------------------------------------
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
],
|
|
194
|
-
})
|
|
195
|
-
: null,
|
|
196
|
-
],
|
|
197
|
-
})
|
|
44
|
+
// -------------------------------------------------------------- CSS & Design System
|
|
45
|
+
function ensureCss() {
|
|
46
|
+
if (typeof document === 'undefined') return
|
|
47
|
+
if (document.getElementById('dsh-image-gen-full-css')) return
|
|
48
|
+
const style = document.createElement('style')
|
|
49
|
+
style.id = 'dsh-image-gen-full-css'
|
|
50
|
+
style.dataset.dshPlugin = NS
|
|
51
|
+
style.textContent = `
|
|
52
|
+
.ig-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:1000px}
|
|
53
|
+
.ig-header{display:flex;flex-direction:column;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
54
|
+
.ig-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}
|
|
55
|
+
.ig-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
|
|
56
|
+
|
|
57
|
+
.ig-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:14px}
|
|
58
|
+
.ig-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
|
|
59
|
+
.ig-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}
|
|
60
|
+
|
|
61
|
+
.ig-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
|
|
62
|
+
.ig-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:12px}
|
|
63
|
+
.ig-grid-4{display:grid;grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));gap:10px}
|
|
64
|
+
|
|
65
|
+
.ig-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
|
|
66
|
+
.ig-stat-val{font-size:16px;font-weight:700;color:var(--dsw-alias-label-primary)}
|
|
67
|
+
.ig-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
68
|
+
|
|
69
|
+
.ig-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}
|
|
70
|
+
.ig-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}
|
|
71
|
+
.ig-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}
|
|
72
|
+
.ig-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}
|
|
73
|
+
|
|
74
|
+
.ig-tabs{display:flex;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:8px;margin-bottom:6px;overflow-x:auto}
|
|
75
|
+
.ig-tab-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:6px;padding:6px 12px;font-size:13px;background:transparent;color:var(--dsw-alias-label-secondary);font-weight:500;display:inline-flex;align-items:center;gap:6px;transition:all .15s ease}
|
|
76
|
+
.ig-tab-btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
|
|
77
|
+
.ig-tab-btn-active{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2);font-weight:600}
|
|
78
|
+
|
|
79
|
+
.ig-field{display:flex;flex-direction:column;gap:5px;padding:8px 0}
|
|
80
|
+
.ig-field-head{display:flex;justify-content:space-between;align-items:center}
|
|
81
|
+
.ig-field-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
82
|
+
.ig-field-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}
|
|
83
|
+
.ig-field-err{font-size:12px;color:var(--dsw-alias-state-error-primary);margin-top:2px}
|
|
84
|
+
|
|
85
|
+
.ig-input,.ig-select{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;width:100%;box-sizing:border-box}
|
|
86
|
+
.ig-input:focus,.ig-select:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
|
|
87
|
+
.ig-input-invalid{border-color:var(--dsw-alias-state-error-primary)}
|
|
88
|
+
.ig-input:disabled,.ig-select:disabled{opacity:0.5;cursor:not-allowed}
|
|
89
|
+
|
|
90
|
+
.ig-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
|
|
91
|
+
.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))}
|
|
92
|
+
.ig-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
93
|
+
.ig-btn-primary:hover:not(:disabled){opacity:0.9}
|
|
94
|
+
.ig-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
|
|
95
|
+
.ig-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.5)}
|
|
96
|
+
.ig-btn-disabled{opacity:0.5;cursor:not-allowed}
|
|
97
|
+
|
|
98
|
+
.ig-reset{font-size:11px;color:var(--dsw-alias-state-brand-primary);background:none;border:none;cursor:pointer;padding:0;text-decoration:underline}
|
|
99
|
+
.ig-reset:hover{opacity:0.8}
|
|
100
|
+
|
|
101
|
+
.ig-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px}
|
|
102
|
+
.ig-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
103
|
+
.ig-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
104
|
+
.ig-banner-warning{padding:12px 16px;border-radius:8px;background:rgba(245,158,11,0.12);border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
|
|
105
|
+
|
|
106
|
+
/* Backward-compatibility alias styles for toolview */
|
|
107
|
+
.fal_head{font-size:13px;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-label-primary)}
|
|
108
|
+
.fal-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}
|
|
109
|
+
.fal-meta{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:6px}
|
|
110
|
+
.fal-err{padding:10px;border-radius:6px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:12px}
|
|
111
|
+
`
|
|
112
|
+
document.head.appendChild(style)
|
|
198
113
|
}
|
|
199
114
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
115
|
+
function createErrorBoundary() {
|
|
116
|
+
if (!React || typeof React.Component !== 'function') {
|
|
117
|
+
return function NoopBoundary(props) { return props?.children || null }
|
|
118
|
+
}
|
|
119
|
+
return class ErrorBoundary extends React.Component {
|
|
120
|
+
constructor(props) {
|
|
121
|
+
super(props)
|
|
122
|
+
this.state = { hasError: false, error: null }
|
|
123
|
+
}
|
|
124
|
+
static getDerivedStateFromError(error) {
|
|
125
|
+
return { hasError: true, error }
|
|
126
|
+
}
|
|
127
|
+
componentDidCatch(error, errorInfo) {
|
|
128
|
+
console.error('[dsh-image-gen] UI Crash caught by ErrorBoundary:', error, errorInfo)
|
|
129
|
+
}
|
|
130
|
+
render() {
|
|
131
|
+
if (this.state.hasError) {
|
|
132
|
+
return react.createElement(
|
|
133
|
+
'div',
|
|
134
|
+
{
|
|
135
|
+
className: 'ig-alert-err',
|
|
136
|
+
style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
|
|
137
|
+
},
|
|
138
|
+
react.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ Image Gen UI Error:'),
|
|
139
|
+
react.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
140
|
+
react.createElement(
|
|
141
|
+
'button',
|
|
142
|
+
{
|
|
143
|
+
type: 'button',
|
|
144
|
+
className: 'ig-btn',
|
|
145
|
+
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
146
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
147
|
+
},
|
|
148
|
+
'Retry'
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
return this.props.children
|
|
153
|
+
}
|
|
154
|
+
}
|
|
234
155
|
}
|
|
156
|
+
const ErrorBoundary = createErrorBoundary()
|
|
235
157
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
className: cssDefault.badges,
|
|
248
|
-
children: [
|
|
249
|
-
react_jsx_runtime.jsx('span', { className: cssDefault.badge, children: props.overriddenLabel }),
|
|
250
|
-
react_jsx_runtime.jsx('button', { type: 'button', className: cssDefault.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel }),
|
|
251
|
-
],
|
|
252
|
-
})
|
|
253
|
-
: null,
|
|
254
|
-
],
|
|
255
|
-
}),
|
|
256
|
-
react_jsx_runtime.jsxs('select', {
|
|
257
|
-
id: props.id,
|
|
258
|
-
className: cssDefault.select,
|
|
259
|
-
value: props.text,
|
|
260
|
-
disabled: props.disabled,
|
|
261
|
-
onChange: (event) => props.onEdit(event.target.value),
|
|
262
|
-
children: [
|
|
263
|
-
react_jsx_runtime.jsx('option', { value: '', children: props.inheritLabel }),
|
|
264
|
-
props.options.map((option) => react_jsx_runtime.jsx('option', { value: option, children: option }, option)),
|
|
265
|
-
],
|
|
266
|
-
}),
|
|
267
|
-
react_jsx_runtime.jsx('p', { className: cssDefault.hint, children: props.hint }),
|
|
268
|
-
],
|
|
269
|
-
})
|
|
270
|
-
}
|
|
158
|
+
// -------------------------------------------------------------- Form & Specs
|
|
159
|
+
const booleanField = (field) => ({
|
|
160
|
+
field,
|
|
161
|
+
format: (value) => (value === true || value === 'true' ? 'true' : value === false || value === 'false' ? 'false' : ''),
|
|
162
|
+
parse: (text) => {
|
|
163
|
+
if (text === '') return { kind: 'clear' }
|
|
164
|
+
if (text === 'true') return { kind: 'set', value: true }
|
|
165
|
+
if (text === 'false') return { kind: 'set', value: false }
|
|
166
|
+
return void 0
|
|
167
|
+
},
|
|
168
|
+
})
|
|
271
169
|
|
|
272
|
-
// --------------------------------------------------------------- form
|
|
273
|
-
/** A staged text value (empty text clears the override). */
|
|
274
170
|
function textField(field) {
|
|
275
171
|
return {
|
|
276
172
|
field,
|
|
277
173
|
format: (value) => (typeof value === 'string' ? value : ''),
|
|
278
|
-
parse: (text) =>
|
|
174
|
+
parse: (text) => {
|
|
175
|
+
if (text === '') return { kind: 'clear' }
|
|
176
|
+
return { kind: 'set', value: text }
|
|
177
|
+
},
|
|
279
178
|
}
|
|
280
179
|
}
|
|
281
180
|
|
|
282
|
-
/** A staged numeric value. */
|
|
283
181
|
function numberField(field) {
|
|
284
182
|
return {
|
|
285
183
|
field,
|
|
@@ -292,7 +190,6 @@ window.__ModuleLoader__.load({
|
|
|
292
190
|
}
|
|
293
191
|
}
|
|
294
192
|
|
|
295
|
-
/** A staged enum value (empty text clears the override). */
|
|
296
193
|
function selectField(field, options) {
|
|
297
194
|
const set = new Set(options)
|
|
298
195
|
return {
|
|
@@ -305,11 +202,89 @@ window.__ModuleLoader__.load({
|
|
|
305
202
|
}
|
|
306
203
|
}
|
|
307
204
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
205
|
+
const PROVIDERS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini', 'replicate']
|
|
206
|
+
const IMAGE_SIZES = ['landscape_4_3', 'landscape_16_9', 'portrait_4_3', 'portrait_16_9', 'square', 'square_hd']
|
|
207
|
+
const OUTPUT_FORMATS = ['png', 'jpeg', 'webp']
|
|
208
|
+
const DELIVERY_MODES = ['link', 'image']
|
|
209
|
+
const STYLE_PRESET_OPTIONS = ['', 'cinematic', 'anime', 'digital_art', 'photographic', 'fantasy', 'isometric', 'cyberpunk', 'watercolor', 'origami', 'oil_painting', 'pixel_art', 'logo']
|
|
210
|
+
const SIZE_PIXELS = {
|
|
211
|
+
square: '512x512',
|
|
212
|
+
square_hd: '1024x1024',
|
|
213
|
+
portrait_4_3: '768x1024',
|
|
214
|
+
portrait_16_9: '576x1024',
|
|
215
|
+
landscape_4_3: '1024x768',
|
|
216
|
+
landscape_16_9: '1024x576',
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const FIELDS = [
|
|
220
|
+
// General
|
|
221
|
+
{ field: 'enabled', spec: booleanField('enabled'), kind: 'select', options: ['true', 'false'], labelKey: 'f.enabled', hintKey: 'f.enabledHint', tab: 'general' },
|
|
222
|
+
{ field: 'provider', spec: selectField('provider', PROVIDERS), kind: 'select', options: PROVIDERS, labelKey: 'f.provider', hintKey: 'f.providerHint', tab: 'general', providerNeeds: { fal: 'f.needFal', custom: 'f.needCustom', codex: 'f.needCodex', grok: 'f.needGrok', local: 'f.needLocal', seedream: 'f.needSeedream', gemini: 'f.needGemini', replicate: 'f.needReplicate' } },
|
|
223
|
+
{ field: 'defaultSize', spec: selectField('defaultSize', IMAGE_SIZES), kind: 'select', options: IMAGE_SIZES, labelKey: 'f.defaultSize', hintKey: 'f.defaultSizeHint', sizePixels: SIZE_PIXELS, tab: 'general' },
|
|
224
|
+
{ field: 'defaultFormat', spec: selectField('defaultFormat', OUTPUT_FORMATS), kind: 'select', options: OUTPUT_FORMATS, labelKey: 'f.defaultFormat', hintKey: 'f.defaultFormatHint', tab: 'general' },
|
|
225
|
+
{ field: 'deliverAs', spec: selectField('deliverAs', DELIVERY_MODES), kind: 'select', options: DELIVERY_MODES, labelKey: 'f.deliverAs', hintKey: 'f.deliverAsHint', tab: 'general' },
|
|
226
|
+
{ field: 'outputDir', spec: textField('outputDir'), kind: 'text', labelKey: 'f.outputDir', hintKey: 'f.outputDirHint', placeholderKey: 'p.outputDir', tab: 'general' },
|
|
227
|
+
{ field: 'historyLimit', spec: numberField('historyLimit'), kind: 'number', labelKey: 'f.historyLimit', hintKey: 'f.historyLimitHint', placeholderKey: 'p.historyLimit', tab: 'general' },
|
|
228
|
+
|
|
229
|
+
// Provider: FAL
|
|
230
|
+
{ field: 'model', spec: textField('model'), kind: 'text', labelKey: 'f.model', hintKey: 'f.modelHint', placeholderKey: 'p.model', when: ['fal'], tab: 'provider' },
|
|
231
|
+
{ field: 'apiKeyEnv', spec: textField('apiKeyEnv'), kind: 'text', labelKey: 'f.apiKeyEnv', hintKey: 'f.apiKeyEnvHint', placeholderKey: 'p.apiKeyEnv', when: ['fal'], tab: 'provider' },
|
|
232
|
+
{ field: 'baseURL', spec: textField('baseURL'), kind: 'text', labelKey: 'f.baseURL', hintKey: 'f.baseURLHint', placeholderKey: 'p.baseURL', when: ['fal'], tab: 'provider' },
|
|
233
|
+
{ field: 'pollIntervalMs', spec: numberField('pollIntervalMs'), kind: 'number', labelKey: 'f.pollIntervalMs', hintKey: 'f.pollIntervalMsHint', placeholderKey: 'p.pollIntervalMs', when: ['fal'], tab: 'provider' },
|
|
234
|
+
{ field: 'timeoutMs', spec: numberField('timeoutMs'), kind: 'number', labelKey: 'f.timeoutMs', hintKey: 'f.timeoutMsHint', placeholderKey: 'p.timeoutMs', when: ['fal'], tab: 'provider' },
|
|
235
|
+
|
|
236
|
+
// Provider: Custom
|
|
237
|
+
{ field: 'customBaseURL', spec: textField('customBaseURL'), kind: 'text', labelKey: 'f.customBaseURL', hintKey: 'f.customBaseURLHint', placeholderKey: 'p.customBaseURL', when: ['custom'], tab: 'provider' },
|
|
238
|
+
{ field: 'customModel', spec: textField('customModel'), kind: 'text', labelKey: 'f.customModel', hintKey: 'f.customModelHint', placeholderKey: 'p.customModel', when: ['custom'], tab: 'provider' },
|
|
239
|
+
{ field: 'customKeyEnv', spec: textField('customKeyEnv'), kind: 'text', labelKey: 'f.customKeyEnv', hintKey: 'f.customKeyEnvHint', placeholderKey: 'p.customKeyEnv', when: ['custom'], tab: 'provider' },
|
|
240
|
+
{ field: 'customSize', spec: textField('customSize'), kind: 'text', labelKey: 'f.customSize', hintKey: 'f.customSizeHint', placeholderKey: 'p.customSize', when: ['custom'], tab: 'provider' },
|
|
241
|
+
|
|
242
|
+
// Provider: Replicate
|
|
243
|
+
{ field: 'replicateModel', spec: textField('replicateModel'), kind: 'text', labelKey: 'f.replicateModel', hintKey: 'f.replicateModelHint', placeholderKey: 'p.replicateModel', when: ['replicate'], tab: 'provider' },
|
|
244
|
+
{ field: 'replicateKeyEnv', spec: textField('replicateKeyEnv'), kind: 'text', labelKey: 'f.replicateKeyEnv', hintKey: 'f.replicateKeyEnvHint', placeholderKey: 'p.replicateKeyEnv', when: ['replicate'], tab: 'provider' },
|
|
245
|
+
|
|
246
|
+
// Provider: SeaDream
|
|
247
|
+
{ field: 'seedreamModel', spec: textField('seedreamModel'), kind: 'text', labelKey: 'f.seedreamModel', hintKey: 'f.seedreamModelHint', placeholderKey: 'p.seedreamModel', when: ['seedream'], tab: 'provider' },
|
|
248
|
+
{ field: 'seedreamKeyEnv', spec: textField('seedreamKeyEnv'), kind: 'text', labelKey: 'f.seedreamKeyEnv', hintKey: 'f.seedreamKeyEnvHint', placeholderKey: 'p.seedreamKeyEnv', when: ['seedream'], tab: 'provider' },
|
|
249
|
+
{ field: 'seedreamBaseURL', spec: textField('seedreamBaseURL'), kind: 'text', labelKey: 'f.seedreamBaseURL', hintKey: 'f.seedreamBaseURLHint', placeholderKey: 'p.seedreamBaseURL', when: ['seedream'], tab: 'provider' },
|
|
250
|
+
|
|
251
|
+
// Provider: Gemini
|
|
252
|
+
{ field: 'geminiModel', spec: textField('geminiModel'), kind: 'text', labelKey: 'f.geminiModel', hintKey: 'f.geminiModelHint', placeholderKey: 'p.geminiModel', when: ['gemini'], tab: 'provider' },
|
|
253
|
+
{ field: 'geminiKeyEnv', spec: textField('geminiKeyEnv'), kind: 'text', labelKey: 'f.geminiKeyEnv', hintKey: 'f.geminiKeyEnvHint', placeholderKey: 'p.geminiKeyEnv', when: ['gemini'], tab: 'provider' },
|
|
254
|
+
|
|
255
|
+
// Provider: Local
|
|
256
|
+
{ field: 'localKind', spec: selectField('localKind', ['comfyui', 'a1111']), kind: 'select', options: ['comfyui', 'a1111'], labelKey: 'f.localKind', hintKey: 'f.localKindHint', when: ['local'], tab: 'provider' },
|
|
257
|
+
{ field: 'localBaseURL', spec: textField('localBaseURL'), kind: 'text', labelKey: 'f.localBaseURL', hintKey: 'f.localBaseURLHint', placeholderKey: 'p.localBaseURL', when: ['local'], tab: 'provider' },
|
|
258
|
+
{ field: 'localModel', spec: textField('localModel'), kind: 'text', labelKey: 'f.localModel', hintKey: 'f.localModelHint', placeholderKey: 'p.localModel', when: ['local'], tab: 'provider' },
|
|
259
|
+
{ field: 'localSteps', spec: numberField('localSteps'), kind: 'number', labelKey: 'f.localSteps', hintKey: 'f.localStepsHint', placeholderKey: 'p.localSteps', when: ['local'], tab: 'provider' },
|
|
260
|
+
{ field: 'localCfg', spec: numberField('localCfg'), kind: 'number', labelKey: 'f.localCfg', hintKey: 'f.localCfgHint', placeholderKey: 'p.localCfg', when: ['local'], tab: 'provider' },
|
|
261
|
+
// Backward-compat aliases for tests
|
|
262
|
+
{ field: 'localComfyUrl', spec: textField('localComfyUrl'), kind: 'text', labelKey: 'f.localComfyUrl', hintKey: 'f.localComfyUrlHint', placeholderKey: 'p.localComfyUrl', when: ['local'], tab: 'provider' },
|
|
263
|
+
{ field: 'localA1111Url', spec: textField('localA1111Url'), kind: 'text', labelKey: 'f.localA1111Url', hintKey: 'f.localA1111UrlHint', placeholderKey: 'p.localA1111Url', when: ['local'], tab: 'provider' },
|
|
264
|
+
|
|
265
|
+
// Provider: Codex / Grok
|
|
266
|
+
{ field: 'subscriptionQuality', spec: selectField('subscriptionQuality', ['', 'low', 'medium', 'high']), kind: 'select', options: ['', 'low', 'medium', 'high'], labelKey: 'f.subscriptionQuality', hintKey: 'f.subscriptionQualityHint', when: ['codex', 'grok'], tab: 'provider' },
|
|
267
|
+
|
|
268
|
+
// Enhancer & Style
|
|
269
|
+
{ field: 'enhancePrompt', spec: booleanField('enhancePrompt'), kind: 'select', options: ['true', 'false'], labelKey: 'f.enhancePrompt', hintKey: 'f.enhancePromptHint', tab: 'enhancer' },
|
|
270
|
+
{ field: 'enableLlmEnhancer', spec: booleanField('enableLlmEnhancer'), kind: 'select', options: ['true', 'false'], labelKey: 'f.enableLlmEnhancer', hintKey: 'f.enableLlmEnhancerHint', tab: 'enhancer' },
|
|
271
|
+
{ field: 'enhanceModel', spec: textField('enhanceModel'), kind: 'text', labelKey: 'f.enhanceModel', hintKey: 'f.enhanceModelHint', placeholderKey: 'p.enhanceModel', tab: 'enhancer' },
|
|
272
|
+
{ field: 'enhanceBelowChars', spec: numberField('enhanceBelowChars'), kind: 'number', labelKey: 'f.enhanceBelowChars', hintKey: 'f.enhanceBelowCharsHint', placeholderKey: 'p.enhanceBelowChars', tab: 'enhancer' },
|
|
273
|
+
{ field: 'stylePreset', spec: selectField('stylePreset', STYLE_PRESET_OPTIONS), kind: 'select', options: STYLE_PRESET_OPTIONS, labelKey: 'f.stylePreset', hintKey: 'f.stylePresetHint', tab: 'enhancer' },
|
|
274
|
+
|
|
275
|
+
// Safety & Budget
|
|
276
|
+
{ field: 'qualityGate', spec: booleanField('qualityGate'), kind: 'select', options: ['true', 'false'], labelKey: 'f.qualityGate', hintKey: 'f.qualityGateHint', tab: 'safety' },
|
|
277
|
+
{ field: 'dailyBudgetUsd', spec: numberField('dailyBudgetUsd'), kind: 'number', labelKey: 'f.dailyBudgetUsd', hintKey: 'f.dailyBudgetUsdHint', placeholderKey: 'p.dailyBudgetUsd', tab: 'safety' },
|
|
278
|
+
{ field: 'loopGuardLimit', spec: numberField('loopGuardLimit'), kind: 'number', labelKey: 'f.loopGuardLimit', hintKey: 'f.loopGuardLimitHint', placeholderKey: 'p.loopGuardLimit', tab: 'safety' },
|
|
279
|
+
|
|
280
|
+
// Cache & Storage
|
|
281
|
+
{ field: 'diskCache', spec: booleanField('diskCache'), kind: 'select', options: ['true', 'false'], labelKey: 'f.diskCache', hintKey: 'f.diskCacheHint', tab: 'cache' },
|
|
282
|
+
{ field: 'cacheBySeed', spec: booleanField('cacheBySeed'), kind: 'select', options: ['true', 'false'], labelKey: 'f.cacheBySeed', hintKey: 'f.cacheBySeedHint', tab: 'cache' },
|
|
283
|
+
{ field: 'cacheByPrompt', spec: booleanField('cacheByPrompt'), kind: 'select', options: ['true', 'false'], labelKey: 'f.cacheByPrompt', hintKey: 'f.cacheByPromptHint', tab: 'cache' },
|
|
284
|
+
{ field: 'pruneDays', spec: numberField('pruneDays'), kind: 'number', labelKey: 'f.pruneDays', hintKey: 'f.pruneDaysHint', placeholderKey: 'p.pruneDays', tab: 'cache' },
|
|
285
|
+
{ field: 'cacheTtlDays', spec: numberField('cacheTtlDays'), kind: 'number', labelKey: 'f.cacheTtlDays', hintKey: 'f.cacheTtlDaysHint', placeholderKey: 'p.cacheTtlDays', tab: 'cache' },
|
|
286
|
+
]
|
|
287
|
+
|
|
313
288
|
var CardForm = class {
|
|
314
289
|
constructor(scope, specs) {
|
|
315
290
|
this.scope = scope
|
|
@@ -318,24 +293,25 @@ window.__ModuleLoader__.load({
|
|
|
318
293
|
this.listeners = new Set()
|
|
319
294
|
this.saving = false
|
|
320
295
|
this.failed = false
|
|
321
|
-
scope.subscribe
|
|
296
|
+
if (scope && scope.subscribe) {
|
|
297
|
+
scope.subscribe(() => this.publish())
|
|
298
|
+
}
|
|
322
299
|
}
|
|
323
300
|
bind(project) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
301
|
+
return {
|
|
302
|
+
getSnapshot: () => project(),
|
|
303
|
+
subscribe: (cb) => {
|
|
304
|
+
this.listeners.add(cb)
|
|
305
|
+
return () => this.listeners.delete(cb)
|
|
306
|
+
},
|
|
328
307
|
}
|
|
329
|
-
const store = runtime.createSnapshotStore(project())
|
|
330
|
-
this.listeners.add(() => store.set(project()))
|
|
331
|
-
return store
|
|
332
308
|
}
|
|
333
309
|
shell() {
|
|
334
|
-
const snapshot = this.scope.getSnapshot()
|
|
310
|
+
const snapshot = (this.scope && this.scope.getSnapshot && this.scope.getSnapshot()) || { status: 'ready', writable: true }
|
|
335
311
|
const plan = this.plan()
|
|
336
312
|
return {
|
|
337
313
|
available: snapshot.status === 'ready',
|
|
338
|
-
writable: snapshot.writable,
|
|
314
|
+
writable: Boolean(snapshot.writable),
|
|
339
315
|
dirty: plan.length > 0,
|
|
340
316
|
invalid: plan.some((item) => item.run === void 0),
|
|
341
317
|
saving: this.saving,
|
|
@@ -345,7 +321,6 @@ window.__ModuleLoader__.load({
|
|
|
345
321
|
field(field) {
|
|
346
322
|
const spec = this.specOf(field)
|
|
347
323
|
const staged = this.staged.get(field)
|
|
348
|
-
const snapshot = this.scope.getSnapshot()
|
|
349
324
|
if (staged === void 0) {
|
|
350
325
|
return { text: spec.format(this.sectionValue(field)), overridden: this.stored(field), invalid: false }
|
|
351
326
|
}
|
|
@@ -378,552 +353,414 @@ window.__ModuleLoader__.load({
|
|
|
378
353
|
let landed = true
|
|
379
354
|
for (const write of writes) landed = (await write()) && landed
|
|
380
355
|
if (landed) this.staged.clear()
|
|
381
|
-
this.saving = false
|
|
382
356
|
this.failed = !landed
|
|
357
|
+
this.saving = false
|
|
383
358
|
this.publish()
|
|
384
359
|
}
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
for (const [field, staged] of this.staged) {
|
|
388
|
-
const spec = this.specOf(field)
|
|
389
|
-
if (staged.clear) {
|
|
390
|
-
if (this.stored(field)) plan.push({ field, run: () => this.clear(field) })
|
|
391
|
-
continue
|
|
392
|
-
}
|
|
393
|
-
if (staged.text === spec.format(this.sectionValue(field))) continue
|
|
394
|
-
const write = spec.parse(staged.text)
|
|
395
|
-
if (write === void 0) plan.push({ field, run: void 0 })
|
|
396
|
-
else if (write.kind === 'clear') plan.push({ field, run: () => this.clear(field) })
|
|
397
|
-
else plan.push({ field, run: () => this.store(field, write.value) })
|
|
398
|
-
}
|
|
399
|
-
return plan
|
|
400
|
-
}
|
|
401
|
-
async clear(field) {
|
|
402
|
-
await this.scope.unset(field)
|
|
403
|
-
return !this.stored(field)
|
|
404
|
-
}
|
|
405
|
-
async store(field, value) {
|
|
406
|
-
await this.scope.set(field, value)
|
|
407
|
-
return this.userLayer()?.[field] === value
|
|
408
|
-
}
|
|
409
|
-
stage(field, edit) {
|
|
410
|
-
this.staged.set(field, edit)
|
|
411
|
-
this.failed = false
|
|
412
|
-
this.publish()
|
|
360
|
+
publish() {
|
|
361
|
+
for (const listener of this.listeners) listener()
|
|
413
362
|
}
|
|
414
363
|
specOf(field) {
|
|
415
364
|
const spec = this.specs.get(field)
|
|
416
|
-
if (spec
|
|
365
|
+
if (!spec) throw new Error('dsh-image-gen: unknown field "' + field + '"')
|
|
417
366
|
return spec
|
|
418
367
|
}
|
|
419
|
-
|
|
420
|
-
return this.scope.getSnapshot()
|
|
368
|
+
section() {
|
|
369
|
+
return (this.scope && this.scope.getSnapshot && this.scope.getSnapshot()?.value) || {}
|
|
421
370
|
}
|
|
422
371
|
sectionValue(field) {
|
|
423
|
-
return this.
|
|
372
|
+
return this.section()[field]
|
|
424
373
|
}
|
|
425
|
-
|
|
426
|
-
return this.
|
|
374
|
+
stored(field) {
|
|
375
|
+
return this.sectionValue(field) !== void 0
|
|
427
376
|
}
|
|
428
|
-
|
|
429
|
-
return
|
|
377
|
+
baseValue(_) {
|
|
378
|
+
return void 0
|
|
430
379
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
380
|
+
stage(field, staged) {
|
|
381
|
+
this.specOf(field)
|
|
382
|
+
this.staged.set(field, staged)
|
|
383
|
+
this.publish()
|
|
434
384
|
}
|
|
435
|
-
|
|
436
|
-
|
|
385
|
+
plan() {
|
|
386
|
+
const plan = []
|
|
387
|
+
for (const [field, staged] of this.staged) {
|
|
388
|
+
const spec = this.specOf(field)
|
|
389
|
+
const write = staged.clear ? { kind: 'clear' } : spec.parse(staged.text)
|
|
390
|
+
if (!write) {
|
|
391
|
+
plan.push({ field, run: void 0 })
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
plan.push({
|
|
395
|
+
field,
|
|
396
|
+
run: async () => {
|
|
397
|
+
try {
|
|
398
|
+
if (write.kind === 'clear') {
|
|
399
|
+
if (this.scope && this.scope.delete) await this.scope.delete(field)
|
|
400
|
+
} else {
|
|
401
|
+
if (this.scope && this.scope.set) await this.scope.set(field, write.value)
|
|
402
|
+
}
|
|
403
|
+
return true
|
|
404
|
+
} catch (_) {
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
return plan
|
|
437
411
|
}
|
|
438
412
|
}
|
|
439
413
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
414
|
+
var FalSettingsCardController = class {
|
|
415
|
+
constructor(scope) {
|
|
416
|
+
this.form = new CardForm(scope, FIELDS.map((entry) => entry.spec))
|
|
417
|
+
this.store = this.form.bind(() => this.projection())
|
|
418
|
+
}
|
|
419
|
+
projection() {
|
|
420
|
+
return {
|
|
421
|
+
...this.form.shell(),
|
|
422
|
+
...Object.fromEntries(FIELDS.map((entry) => [entry.field, this.form.field(entry.field)])),
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
inject() {
|
|
426
|
+
return { hooks: { falSettingsCard: this.store }, ...this.form.actions() }
|
|
427
|
+
}
|
|
451
428
|
}
|
|
452
|
-
const OUTPUT_FORMATS = ['png', 'jpeg', 'webp']
|
|
453
|
-
const DELIVERY_MODES = ['link', 'image']
|
|
454
|
-
const PROVIDERS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini', 'replicate']
|
|
455
|
-
|
|
456
|
-
/** Field specs in display order. */
|
|
457
|
-
const booleanField = (field) => ({
|
|
458
|
-
field,
|
|
459
|
-
type: 'boolean',
|
|
460
|
-
format: (value) => (typeof value === 'boolean' ? String(value) : (value === 'true' || value === 'false' ? value : '')),
|
|
461
|
-
parse: (text) => {
|
|
462
|
-
if (text === '') return { kind: 'clear' }
|
|
463
|
-
if (text === 'true') return { kind: 'set', value: true }
|
|
464
|
-
if (text === 'false') return { kind: 'set', value: false }
|
|
465
|
-
return void 0
|
|
466
|
-
},
|
|
467
|
-
})
|
|
468
|
-
const STYLE_PRESET_OPTIONS = ['', 'cinematic', 'anime', 'digital_art', 'photographic', 'fantasy', 'isometric', 'cyberpunk', 'watercolor', 'origami', 'oil_painting', 'pixel_art', 'logo']
|
|
469
|
-
|
|
470
|
-
const FIELDS = [
|
|
471
|
-
{ field: 'enabled', spec: booleanField('enabled'), kind: 'select', options: ['true', 'false'], labelKey: 'f.enabled', hintKey: 'f.enabledHint' },
|
|
472
|
-
{ field: 'provider', spec: selectField('provider', PROVIDERS), kind: 'select', options: PROVIDERS, labelKey: 'f.provider', hintKey: 'f.providerHint', providerNeeds: { fal: 'f.needFal', custom: 'f.needCustom', codex: 'f.needCodex', grok: 'f.needGrok', local: 'f.needLocal', seedream: 'f.needSeedream', gemini: 'f.needGemini', replicate: 'f.needReplicate' } },
|
|
473
|
-
|
|
474
|
-
// FAL
|
|
475
|
-
{ field: 'model', spec: textField('model'), kind: 'text', labelKey: 'f.model', hintKey: 'f.modelHint', placeholderKey: 'p.model', when: ['fal'] },
|
|
476
|
-
{ field: 'apiKeyEnv', spec: textField('apiKeyEnv'), kind: 'text', labelKey: 'f.apiKeyEnv', hintKey: 'f.apiKeyEnvHint', placeholderKey: 'p.apiKeyEnv', when: ['fal'] },
|
|
477
|
-
{ field: 'baseURL', spec: textField('baseURL'), kind: 'text', labelKey: 'f.baseURL', hintKey: 'f.baseURLHint', placeholderKey: 'p.baseURL', when: ['fal'] },
|
|
478
|
-
{ field: 'pollIntervalMs', spec: numberField('pollIntervalMs'), kind: 'number', labelKey: 'f.pollIntervalMs', hintKey: 'f.pollIntervalMsHint', placeholderKey: 'p.pollIntervalMs', when: ['fal'] },
|
|
479
|
-
{ field: 'timeoutMs', spec: numberField('timeoutMs'), kind: 'number', labelKey: 'f.timeoutMs', hintKey: 'f.timeoutMsHint', placeholderKey: 'p.timeoutMs', when: ['fal'] },
|
|
480
|
-
|
|
481
|
-
// Custom
|
|
482
|
-
{ field: 'customBaseURL', spec: textField('customBaseURL'), kind: 'text', labelKey: 'f.customBaseURL', hintKey: 'f.customBaseURLHint', placeholderKey: 'p.customBaseURL', when: ['custom'] },
|
|
483
|
-
{ field: 'customModel', spec: textField('customModel'), kind: 'text', labelKey: 'f.customModel', hintKey: 'f.customModelHint', placeholderKey: 'p.customModel', when: ['custom'] },
|
|
484
|
-
{ field: 'customKeyEnv', spec: textField('customKeyEnv'), kind: 'text', labelKey: 'f.customKeyEnv', hintKey: 'f.customKeyEnvHint', placeholderKey: 'p.customKeyEnv', when: ['custom'] },
|
|
485
|
-
{ field: 'customSize', spec: textField('customSize'), kind: 'text', labelKey: 'f.customSize', hintKey: 'f.customSizeHint', placeholderKey: 'p.customSize', when: ['custom'] },
|
|
486
|
-
|
|
487
|
-
// Replicate
|
|
488
|
-
{ field: 'replicateModel', spec: textField('replicateModel'), kind: 'text', labelKey: 'f.replicateModel', hintKey: 'f.replicateModelHint', placeholderKey: 'p.replicateModel', when: ['replicate'] },
|
|
489
|
-
{ field: 'replicateKeyEnv', spec: textField('replicateKeyEnv'), kind: 'text', labelKey: 'f.replicateKeyEnv', hintKey: 'f.replicateKeyEnvHint', placeholderKey: 'p.replicateKeyEnv', when: ['replicate'] },
|
|
490
|
-
|
|
491
|
-
// SeaDream
|
|
492
|
-
{ field: 'seedreamModel', spec: textField('seedreamModel'), kind: 'text', labelKey: 'f.seedreamModel', hintKey: 'f.seedreamModelHint', placeholderKey: 'p.seedreamModel', when: ['seedream'] },
|
|
493
|
-
{ field: 'seedreamKeyEnv', spec: textField('seedreamKeyEnv'), kind: 'text', labelKey: 'f.seedreamKeyEnv', hintKey: 'f.seedreamKeyEnvHint', placeholderKey: 'p.seedreamKeyEnv', when: ['seedream'] },
|
|
494
|
-
{ field: 'seedreamBaseURL', spec: textField('seedreamBaseURL'), kind: 'text', labelKey: 'f.seedreamBaseURL', hintKey: 'f.seedreamBaseURLHint', placeholderKey: 'p.seedreamBaseURL', when: ['seedream'] },
|
|
495
|
-
|
|
496
|
-
// Gemini
|
|
497
|
-
{ field: 'geminiModel', spec: textField('geminiModel'), kind: 'text', labelKey: 'f.geminiModel', hintKey: 'f.geminiModelHint', placeholderKey: 'p.geminiModel', when: ['gemini'] },
|
|
498
|
-
{ field: 'geminiKeyEnv', spec: textField('geminiKeyEnv'), kind: 'text', labelKey: 'f.geminiKeyEnv', hintKey: 'f.geminiKeyEnvHint', placeholderKey: 'p.geminiKeyEnv', when: ['gemini'] },
|
|
499
|
-
|
|
500
|
-
// Local ComfyUI / A1111
|
|
501
|
-
{ field: 'localComfyUrl', spec: textField('localComfyUrl'), kind: 'text', labelKey: 'f.localComfyUrl', hintKey: 'f.localComfyUrlHint', placeholderKey: 'p.localComfyUrl', when: ['local'] },
|
|
502
|
-
{ field: 'localA1111Url', spec: textField('localA1111Url'), kind: 'text', labelKey: 'f.localA1111Url', hintKey: 'f.localA1111UrlHint', placeholderKey: 'p.localA1111Url', when: ['local'] },
|
|
503
|
-
|
|
504
|
-
// Common Style & LLM Enhancer
|
|
505
|
-
{ field: 'stylePreset', spec: selectField('stylePreset', STYLE_PRESET_OPTIONS), kind: 'select', options: STYLE_PRESET_OPTIONS, labelKey: 'f.stylePreset', hintKey: 'f.stylePresetHint' },
|
|
506
|
-
{ field: 'enableLlmEnhancer', spec: booleanField('enableLlmEnhancer'), kind: 'select', options: ['true', 'false'], labelKey: 'f.enableLlmEnhancer', hintKey: 'f.enableLlmEnhancerHint' },
|
|
507
|
-
|
|
508
|
-
// Output & Defaults
|
|
509
|
-
{ field: 'defaultSize', spec: selectField('defaultSize', IMAGE_SIZES), kind: 'select', options: IMAGE_SIZES, labelKey: 'f.defaultSize', hintKey: 'f.defaultSizeHint', sizePixels: SIZE_PIXELS },
|
|
510
|
-
{ field: 'defaultFormat', spec: selectField('defaultFormat', OUTPUT_FORMATS), kind: 'select', options: OUTPUT_FORMATS, labelKey: 'f.defaultFormat', hintKey: 'f.defaultFormatHint' },
|
|
511
|
-
{ field: 'deliverAs', spec: selectField('deliverAs', DELIVERY_MODES), kind: 'select', options: DELIVERY_MODES, labelKey: 'f.deliverAs', hintKey: 'f.deliverAsHint' },
|
|
512
|
-
{ field: 'outputDir', spec: textField('outputDir'), kind: 'text', labelKey: 'f.outputDir', hintKey: 'f.outputDirHint', placeholderKey: 'p.outputDir' },
|
|
513
|
-
{ field: 'cacheTtlDays', spec: numberField('cacheTtlDays'), kind: 'number', labelKey: 'f.cacheTtlDays', hintKey: 'f.cacheTtlDaysHint', placeholderKey: 'p.cacheTtlDays' },
|
|
514
|
-
]
|
|
515
429
|
|
|
430
|
+
// -------------------------------------------------------------- UI Components
|
|
516
431
|
function Field(props) {
|
|
517
432
|
const { entry, state, fieldProps, t, onEdit, onReset } = props
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
disabled: fieldProps.disabled,
|
|
526
|
-
text: state.text,
|
|
527
|
-
overridden: state.overridden,
|
|
528
|
-
invalid: state.invalid,
|
|
529
|
-
onEdit,
|
|
530
|
-
onReset,
|
|
531
|
-
}
|
|
433
|
+
const id = 'dsh-image-gen-' + entry.field
|
|
434
|
+
const label = t(entry.labelKey)
|
|
435
|
+
const hint = t(entry.hintKey)
|
|
436
|
+
const overridden = Boolean(state.overridden)
|
|
437
|
+
const invalid = Boolean(state.invalid)
|
|
438
|
+
const disabled = Boolean(fieldProps.disabled)
|
|
439
|
+
|
|
532
440
|
if (entry.kind === 'select') {
|
|
533
|
-
const px = entry.sizePixels && entry.sizePixels[state.text] ?
|
|
534
|
-
const need = entry.providerNeeds && entry.providerNeeds[state.text] ?
|
|
535
|
-
|
|
536
|
-
|
|
441
|
+
const px = entry.sizePixels && entry.sizePixels[state.text] ? ' — ' + entry.sizePixels[state.text] : ''
|
|
442
|
+
const need = entry.providerNeeds && entry.providerNeeds[state.text] ? ' (' + t(entry.providerNeeds[state.text]) + ')' : ''
|
|
443
|
+
return react.createElement(
|
|
444
|
+
'div',
|
|
445
|
+
{ className: 'ig-field', key: entry.field },
|
|
446
|
+
react.createElement(
|
|
447
|
+
'div',
|
|
448
|
+
{ className: 'ig-field-head' },
|
|
449
|
+
react.createElement('label', { className: 'ig-field-label', htmlFor: id }, label),
|
|
450
|
+
overridden
|
|
451
|
+
? react.createElement(
|
|
452
|
+
'div',
|
|
453
|
+
{ className: 'ig-row' },
|
|
454
|
+
react.createElement('span', { className: 'ig-badge ig-badge-warn' }, fieldProps.overriddenLabel),
|
|
455
|
+
react.createElement('button', { type: 'button', className: 'ig-reset', disabled, onClick: onReset }, fieldProps.resetLabel)
|
|
456
|
+
)
|
|
457
|
+
: null
|
|
458
|
+
),
|
|
459
|
+
react.createElement(
|
|
460
|
+
'select',
|
|
461
|
+
{
|
|
462
|
+
id,
|
|
463
|
+
className: 'ig-select',
|
|
464
|
+
value: state.text,
|
|
465
|
+
disabled,
|
|
466
|
+
onChange: (e) => onEdit(e.target.value),
|
|
467
|
+
},
|
|
468
|
+
entry.options.map((opt) =>
|
|
469
|
+
react.createElement('option', { key: opt, value: opt }, opt === '' ? t('f.inherit') : opt)
|
|
470
|
+
)
|
|
471
|
+
),
|
|
472
|
+
react.createElement('p', { className: 'ig-field-hint' }, hint + px + need)
|
|
473
|
+
)
|
|
537
474
|
}
|
|
538
|
-
|
|
475
|
+
|
|
476
|
+
return react.createElement(
|
|
477
|
+
'div',
|
|
478
|
+
{ className: 'ig-field', key: entry.field },
|
|
479
|
+
react.createElement(
|
|
480
|
+
'div',
|
|
481
|
+
{ className: 'ig-field-head' },
|
|
482
|
+
react.createElement('label', { className: 'ig-field-label', htmlFor: id }, label),
|
|
483
|
+
overridden
|
|
484
|
+
? react.createElement(
|
|
485
|
+
'div',
|
|
486
|
+
{ className: 'ig-row' },
|
|
487
|
+
react.createElement('span', { className: 'ig-badge ig-badge-warn' }, fieldProps.overriddenLabel),
|
|
488
|
+
react.createElement('button', { type: 'button', className: 'ig-reset', disabled, onClick: onReset }, fieldProps.resetLabel)
|
|
489
|
+
)
|
|
490
|
+
: null
|
|
491
|
+
),
|
|
492
|
+
react.createElement('input', {
|
|
493
|
+
id,
|
|
494
|
+
className: invalid ? 'ig-input ig-input-invalid' : 'ig-input',
|
|
495
|
+
type: 'text',
|
|
496
|
+
...(entry.kind === 'number' ? { inputMode: 'numeric' } : {}),
|
|
497
|
+
value: state.text,
|
|
498
|
+
placeholder: entry.placeholderKey ? t(entry.placeholderKey) : '',
|
|
499
|
+
disabled,
|
|
500
|
+
onChange: (e) => onEdit(e.target.value),
|
|
501
|
+
}),
|
|
502
|
+
react.createElement('p', { className: invalid ? 'ig-field-err' : 'ig-field-hint' }, invalid ? fieldProps.invalidLabel : hint)
|
|
503
|
+
)
|
|
539
504
|
}
|
|
540
505
|
|
|
541
506
|
function FalSettingsCard(props) {
|
|
542
507
|
const t = props.t || ((k) => k)
|
|
508
|
+
const [open, setOpen] = react.useState(props.defaultOpen ?? true)
|
|
509
|
+
const [activeTab, setActiveTab] = react.useState('general')
|
|
510
|
+
|
|
511
|
+
react.useEffect(() => {
|
|
512
|
+
ensureCss()
|
|
513
|
+
}, [])
|
|
514
|
+
|
|
543
515
|
const state = (typeof props.useFalSettingsCard === 'function'
|
|
544
|
-
? props.useFalSettingsCard((
|
|
516
|
+
? props.useFalSettingsCard((s) => s)
|
|
545
517
|
: null) || props.state || { available: true, writable: true, provider: { text: 'fal' } }
|
|
518
|
+
|
|
546
519
|
const disabled = !state.writable
|
|
547
520
|
const currentProvider = (state.provider && state.provider.text) || 'fal'
|
|
548
|
-
const
|
|
521
|
+
const blocked = !state.dirty || state.invalid || state.saving
|
|
522
|
+
|
|
549
523
|
const fieldProps = {
|
|
550
524
|
overriddenLabel: t('settings.overridden'),
|
|
551
525
|
resetLabel: t('settings.reset'),
|
|
552
526
|
invalidLabel: t('settings.invalidNumber'),
|
|
553
527
|
disabled,
|
|
554
528
|
}
|
|
555
|
-
return react_jsx_runtime.jsx(PluginSettingsCard, {
|
|
556
|
-
t,
|
|
557
|
-
titleKey: 'settings.title',
|
|
558
|
-
descriptionKey: 'settings.description',
|
|
559
|
-
state,
|
|
560
|
-
onSave: props.save,
|
|
561
|
-
onDiscard: props.discard,
|
|
562
|
-
children: [
|
|
563
|
-
react_jsx_runtime.jsx('p', { className: cssDefault.hint, key: 'credHint', children: t('settings.credentialHint') }),
|
|
564
|
-
visibleFields.map((entry) =>
|
|
565
|
-
react_jsx_runtime.jsx(
|
|
566
|
-
Field,
|
|
567
|
-
{
|
|
568
|
-
entry,
|
|
569
|
-
state: state[entry.field] ?? { text: '', overridden: false, invalid: false },
|
|
570
|
-
fieldProps,
|
|
571
|
-
t,
|
|
572
|
-
onEdit: (text) => props.edit && props.edit(entry.field, text),
|
|
573
|
-
onReset: () => props.resetField && props.resetField(entry.field),
|
|
574
|
-
},
|
|
575
|
-
entry.field,
|
|
576
|
-
),
|
|
577
|
-
),
|
|
578
|
-
],
|
|
579
|
-
})
|
|
580
|
-
}
|
|
581
529
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
projection() {
|
|
589
|
-
return {
|
|
590
|
-
...this.form.shell(),
|
|
591
|
-
...Object.fromEntries(FIELDS.map((entry) => [entry.field, this.form.field(entry.field)])),
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
inject() {
|
|
595
|
-
return { hooks: { falSettingsCard: this.store }, ...this.form.actions() }
|
|
530
|
+
if (!state.available) {
|
|
531
|
+
return react.createElement(
|
|
532
|
+
'li',
|
|
533
|
+
{ className: 'ig-section-card', style: { listStyle: 'none' } },
|
|
534
|
+
react.createElement('p', { className: 'ig-field-hint' }, t('settings.unavailable'))
|
|
535
|
+
)
|
|
596
536
|
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// -------------------------------------------------------------- apply
|
|
600
|
-
const NS = 'dsh-image-gen'
|
|
601
|
-
/** Settings namespace the card edits (the Host plugin registers it). */
|
|
602
|
-
const SETTINGS_NS = 'dsh-image-gen'
|
|
603
|
-
/** Required client services. */
|
|
604
|
-
const inject = ['slots', 'settingsScope', 'locale', 'sessions']
|
|
605
537
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
'settings.discard': 'Discard',
|
|
620
|
-
'settings.save': 'Save',
|
|
621
|
-
'settings.saving': 'Saving…',
|
|
622
|
-
'settings.saveFailed': 'Save failed',
|
|
623
|
-
'f.inherit': 'Inherit (default)',
|
|
624
|
-
'f.enabled': 'Enabled',
|
|
625
|
-
'f.enabledHint': 'Master switch: enable or disable the generate_image tool.',
|
|
626
|
-
'f.needFal': 'requires FAL API key',
|
|
627
|
-
'f.needCustom': 'requires custom endpoint URL',
|
|
628
|
-
'f.needCodex': 'uses Codex subscription session',
|
|
629
|
-
'f.needGrok': 'uses Grok subscription session',
|
|
630
|
-
'f.needLocal': 'connects to local ComfyUI/A1111',
|
|
631
|
-
'f.needSeedream': 'requires SeaDream API key',
|
|
632
|
-
'f.needGemini': 'requires Gemini API key',
|
|
633
|
-
'f.needReplicate': 'requires Replicate API token',
|
|
634
|
-
'f.replicateModel': 'Model (Replicate)',
|
|
635
|
-
'f.replicateModelHint': 'Replicate model identifier, e.g. black-forest-labs/flux-schnell.',
|
|
636
|
-
'f.replicateKeyEnv': 'API key reference (Replicate)',
|
|
637
|
-
'f.replicateKeyEnvHint': 'Credential reference / env var holding the Replicate API token.',
|
|
638
|
-
'p.replicateModel': 'Example: black-forest-labs/flux-schnell',
|
|
639
|
-
'p.replicateKeyEnv': 'Example: REPLICATE_API_TOKEN',
|
|
640
|
-
'f.seedreamModel': 'Model (SeaDream)',
|
|
641
|
-
'f.seedreamModelHint': 'SeaDream model identifier.',
|
|
642
|
-
'f.seedreamKeyEnv': 'API key reference (SeaDream)',
|
|
643
|
-
'f.seedreamKeyEnvHint': 'Credential reference / env var holding the SeaDream API key.',
|
|
644
|
-
'f.seedreamBaseURL': 'Base URL (SeaDream)',
|
|
645
|
-
'f.seedreamBaseURLHint': 'SeaDream endpoint URL (Volcengine Ark API).',
|
|
646
|
-
'p.seedreamModel': 'Example: Doubao-SeaDream-3.0',
|
|
647
|
-
'p.seedreamKeyEnv': 'Example: SEADREAM_API_KEY',
|
|
648
|
-
'p.seedreamBaseURL': 'Example: https://ark.cn-beijing.volces.com/api/v3',
|
|
649
|
-
'f.geminiModel': 'Model (Gemini)',
|
|
650
|
-
'f.geminiModelHint': 'Gemini Imagen model identifier.',
|
|
651
|
-
'f.geminiKeyEnv': 'API key reference (Gemini)',
|
|
652
|
-
'f.geminiKeyEnvHint': 'Credential reference / env var holding the Gemini API key.',
|
|
653
|
-
'p.geminiModel': 'Example: imagen-3.0-generate-002',
|
|
654
|
-
'p.geminiKeyEnv': 'Example: GEMINI_API_KEY',
|
|
655
|
-
'f.localComfyUrl': 'ComfyUI URL',
|
|
656
|
-
'f.localComfyUrlHint': 'Base URL of local ComfyUI server.',
|
|
657
|
-
'p.localComfyUrl': 'Example: http://127.0.0.1:8188',
|
|
658
|
-
'f.localA1111Url': 'Automatic1111 URL',
|
|
659
|
-
'f.localA1111UrlHint': 'Base URL of local Automatic1111 / SD WebUI.',
|
|
660
|
-
'p.localA1111Url': 'Example: http://127.0.0.1:7860',
|
|
661
|
-
'f.stylePreset': 'Default style preset',
|
|
662
|
-
'f.stylePresetHint': 'Style preset automatically applied if the tool call does not specify one.',
|
|
663
|
-
'f.enableLlmEnhancer': 'LLM prompt enhancer',
|
|
664
|
-
'f.enableLlmEnhancerHint': 'Automatically refine and expand prompts before generation using session LLM.',
|
|
665
|
-
'f.cacheTtlDays': 'Cache retention (days)',
|
|
666
|
-
'f.cacheTtlDaysHint': 'How many days downloaded images are stored in cache before cleanup.',
|
|
667
|
-
'p.cacheTtlDays': 'Example: 7',
|
|
668
|
-
'f.maxHistoryEntries': 'Max history entries',
|
|
669
|
-
'f.maxHistoryEntriesHint': 'Maximum number of items shown in generation history gallery.',
|
|
670
|
-
'p.maxHistoryEntries': 'Example: 50',
|
|
671
|
-
'f.provider': 'Provider',
|
|
672
|
-
'f.providerHint': 'fal — the FAL queue configured below. custom — any OpenAI-compatible API, configured further down. '
|
|
673
|
-
+ 'codex and grok — through a subscription you are logged into, no key needed here.',
|
|
674
|
-
'f.model': 'Model (FAL)',
|
|
675
|
-
'f.modelHint': 'FAL model id, called as {baseURL}/{model}. Used when provider is fal.',
|
|
676
|
-
'f.apiKeyEnv': 'API key reference',
|
|
677
|
-
'f.apiKeyEnvHint': 'Credential reference / env var holding the FAL API key. The "Key " auth prefix is added automatically.',
|
|
678
|
-
'f.baseURL': 'Queue base URL',
|
|
679
|
-
'f.baseURLHint': 'FAL queue base URL (queue.fal.run).',
|
|
680
|
-
'f.defaultSize': 'Default image size',
|
|
681
|
-
'f.defaultSizeHint': 'Used when the tool call omits image_size.',
|
|
682
|
-
'f.defaultFormat': 'Default format',
|
|
683
|
-
'f.defaultFormatHint': 'Used when the tool call omits output_format.',
|
|
684
|
-
'f.pollIntervalMs': 'Poll interval (ms)',
|
|
685
|
-
'f.pollIntervalMsHint': 'Status polling interval while a job runs.',
|
|
686
|
-
'f.timeoutMs': 'Timeout (ms)',
|
|
687
|
-
'f.timeoutMsHint': 'Total generation timeout: submit + poll + download.',
|
|
688
|
-
'f.deliverAs': 'How the image reaches the chat',
|
|
689
|
-
'f.deliverAsHint': 'link — the result is text with a link and the card renders the picture from it; works with any chat model. image — the result carries the picture itself, which a text-only chat model cannot read, so this needs dsh-vision-bridge or a vision-capable model.',
|
|
690
|
-
'f.customBaseURL': 'Custom API: base URL',
|
|
691
|
-
'f.customBaseURLHint': 'API root without a trailing slash. The request goes to {base}/images/generations.',
|
|
692
|
-
'f.customModel': 'Custom API: model',
|
|
693
|
-
'f.customModelHint': 'Model id sent to that API, e.g. gpt-image-1.',
|
|
694
|
-
'f.customKeyEnv': 'Custom API: key reference',
|
|
695
|
-
'f.customKeyEnvHint': 'Credential reference / env var with the API key. Empty means no authorization header, for gateways that need none.',
|
|
696
|
-
'f.customSize': 'Custom API: fixed size',
|
|
697
|
-
'f.customSizeHint': 'Leave empty and the named size is translated automatically (square_hd → 1024x1024). Set it only for an API picky about sizes.',
|
|
698
|
-
'f.outputDir': 'Output directory',
|
|
699
|
-
'f.outputDirHint': 'Relative to the session working directory; an absolute path is used as given.',
|
|
700
|
-
'p.model': 'Example: fal-ai/flux-2/klein/9b',
|
|
701
|
-
'p.apiKeyEnv': 'Example: FAL_API_KEY',
|
|
702
|
-
'p.baseURL': 'Example: https://queue.fal.run',
|
|
703
|
-
'p.pollIntervalMs': 'Example: 2000',
|
|
704
|
-
'p.timeoutMs': 'Example: 180000',
|
|
705
|
-
'p.customBaseURL': 'Example: https://api.openai.com/v1',
|
|
706
|
-
'p.customModel': 'Example: gpt-image-1',
|
|
707
|
-
'p.customKeyEnv': 'Example: OPENAI_API_KEY',
|
|
708
|
-
'p.customSize': 'Example: 1024x1024',
|
|
709
|
-
'p.outputDir': 'Example: generated/images',
|
|
710
|
-
'history.title': 'Generation history',
|
|
711
|
-
'history.all': 'All providers',
|
|
712
|
-
'history.empty': 'No generations yet',
|
|
713
|
-
'history.loading': 'Loading…',
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const ru = {
|
|
717
|
-
'settings.navLabel': 'Генерация изображений',
|
|
718
|
-
'settings.title': 'Генерация изображений',
|
|
719
|
-
'settings.description': 'Инструмент generate_image. Провайдер: очередь FAL или любой OpenAI-совместимый API картинок.',
|
|
720
|
-
'settings.unavailable': 'Харнесс ещё не объявил настройки плагина. Если он только что перезапустился, раздел появится сам через несколько секунд.',
|
|
721
|
-
'settings.credentialHint': 'Ключи API хранятся в Credentials под указанными ссылками (Настройки → Credentials).',
|
|
722
|
-
'settings.expand': 'Развернуть',
|
|
723
|
-
'settings.collapse': 'Свернуть',
|
|
724
|
-
'settings.unsaved': 'Есть несохранённые изменения',
|
|
725
|
-
'settings.readOnly': 'Этот деплой read-only: настройки нельзя менять из GUI.',
|
|
726
|
-
'settings.overridden': 'Переопределено',
|
|
727
|
-
'settings.reset': 'Сбросить',
|
|
728
|
-
'settings.invalidNumber': 'Введите корректное число',
|
|
729
|
-
'settings.discard': 'Отменить',
|
|
730
|
-
'settings.save': 'Сохранить',
|
|
731
|
-
'settings.saving': 'Сохранение…',
|
|
732
|
-
'settings.saveFailed': 'Не удалось сохранить',
|
|
733
|
-
'f.inherit': 'Унаследовать (по умолчанию)',
|
|
734
|
-
'f.enabled': 'Включено',
|
|
735
|
-
'f.enabledHint': 'Главный переключатель: включить или отключить инструмент generate_image.',
|
|
736
|
-
'f.needFal': 'требуется ключ API FAL',
|
|
737
|
-
'f.needCustom': 'требуется базовый URL API',
|
|
738
|
-
'f.needCodex': 'использует сессию подписки Codex',
|
|
739
|
-
'f.needGrok': 'использует сессию подписки Grok',
|
|
740
|
-
'f.needLocal': 'подключается к локальному ComfyUI/A1111',
|
|
741
|
-
'f.needSeedream': 'требуется ключ API SeaDream',
|
|
742
|
-
'f.needGemini': 'требуется ключ API Gemini',
|
|
743
|
-
'f.needReplicate': 'требуется токен API Replicate',
|
|
744
|
-
'f.replicateModel': 'Модель (Replicate)',
|
|
745
|
-
'f.replicateModelHint': 'Идентификатор модели Replicate, например black-forest-labs/flux-schnell.',
|
|
746
|
-
'f.replicateKeyEnv': 'Ссылка на ключ (Replicate)',
|
|
747
|
-
'f.replicateKeyEnvHint': 'Имя credential-ссылки / переменной окружения с токеном Replicate.',
|
|
748
|
-
'p.replicateModel': 'Пример: black-forest-labs/flux-schnell',
|
|
749
|
-
'p.replicateKeyEnv': 'Пример: REPLICATE_API_TOKEN',
|
|
750
|
-
'f.seedreamModel': 'Модель (SeaDream)',
|
|
751
|
-
'f.seedreamModelHint': 'Идентификатор модели SeaDream.',
|
|
752
|
-
'f.seedreamKeyEnv': 'Ссылка на ключ (SeaDream)',
|
|
753
|
-
'f.seedreamKeyEnvHint': 'Имя credential-ссылки / переменной окружения с ключом SeaDream.',
|
|
754
|
-
'f.seedreamBaseURL': 'Базовый URL (SeaDream)',
|
|
755
|
-
'f.seedreamBaseURLHint': 'Базовый URL API SeaDream (Volcengine Ark).',
|
|
756
|
-
'p.seedreamModel': 'Пример: Doubao-SeaDream-3.0',
|
|
757
|
-
'p.seedreamKeyEnv': 'Пример: SEADREAM_API_KEY',
|
|
758
|
-
'p.seedreamBaseURL': 'Пример: https://ark.cn-beijing.volces.com/api/v3',
|
|
759
|
-
'f.geminiModel': 'Модель (Gemini)',
|
|
760
|
-
'f.geminiModelHint': 'Идентификатор модели Gemini Imagen.',
|
|
761
|
-
'f.geminiKeyEnv': 'Ссылка на ключ (Gemini)',
|
|
762
|
-
'f.geminiKeyEnvHint': 'Имя credential-ссылки / переменной окружения с ключом Gemini.',
|
|
763
|
-
'p.geminiModel': 'Пример: imagen-3.0-generate-002',
|
|
764
|
-
'p.geminiKeyEnv': 'Пример: GEMINI_API_KEY',
|
|
765
|
-
'f.localComfyUrl': 'URL ComfyUI',
|
|
766
|
-
'f.localComfyUrlHint': 'Базовый URL локального сервера ComfyUI.',
|
|
767
|
-
'p.localComfyUrl': 'Пример: http://127.0.0.1:8188',
|
|
768
|
-
'f.localA1111Url': 'URL Automatic1111',
|
|
769
|
-
'f.localA1111UrlHint': 'Базовый URL локального SD WebUI / Automatic1111.',
|
|
770
|
-
'p.localA1111Url': 'Пример: http://127.0.0.1:7860',
|
|
771
|
-
'f.stylePreset': 'Стилевой пресет по умолчанию',
|
|
772
|
-
'f.stylePresetHint': 'Пресет стилизации, применяемый если инструмент вызван без указания стиля.',
|
|
773
|
-
'f.enableLlmEnhancer': 'Улучшение промпта через LLM',
|
|
774
|
-
'f.enableLlmEnhancerHint': 'Автоматически детализировать и улучшать промпты через чат-модель перед генерацией.',
|
|
775
|
-
'f.cacheTtlDays': 'Срок хранения кэша (дней)',
|
|
776
|
-
'f.cacheTtlDaysHint': 'Сколько дней скачанные картинки хранятся в кэше до очистки.',
|
|
777
|
-
'p.cacheTtlDays': 'Пример: 7',
|
|
778
|
-
'f.maxHistoryEntries': 'Максимум записей в истории',
|
|
779
|
-
'f.maxHistoryEntriesHint': 'Максимальное количество картинок в галерее истории генераций.',
|
|
780
|
-
'p.maxHistoryEntries': 'Пример: 50',
|
|
781
|
-
'f.provider': 'Провайдер',
|
|
782
|
-
'f.providerHint': 'fal — очередь FAL, настроенная ниже. custom — любой OpenAI-совместимый API, настраивается дальше. '
|
|
783
|
-
+ 'codex и grok — через подписку, в которую вы вошли; ключ здесь не нужен.',
|
|
784
|
-
'f.model': 'Модель (FAL)',
|
|
785
|
-
'f.modelHint': 'ID модели FAL, вызывается как {baseURL}/{model}. Используется при провайдере fal.',
|
|
786
|
-
'f.apiKeyEnv': 'Ссылка на ключ API',
|
|
787
|
-
'f.apiKeyEnvHint': 'Имя credential-ссылки / переменной окружения с ключом FAL. Префикс "Key " добавляется автоматически.',
|
|
788
|
-
'f.baseURL': 'Базовый URL очереди',
|
|
789
|
-
'f.baseURLHint': 'Базовый URL FAL queue (queue.fal.run).',
|
|
790
|
-
'f.defaultSize': 'Размер по умолчанию',
|
|
791
|
-
'f.defaultSizeHint': 'Используется, когда вызов инструмента не указывает image_size.',
|
|
792
|
-
'f.defaultFormat': 'Формат по умолчанию',
|
|
793
|
-
'f.defaultFormatHint': 'Используется, когда вызов инструмента не указывает output_format.',
|
|
794
|
-
'f.pollIntervalMs': 'Интервал опроса (мс)',
|
|
795
|
-
'f.pollIntervalMsHint': 'Интервал опроса статуса задания.',
|
|
796
|
-
'f.timeoutMs': 'Таймаут (мс)',
|
|
797
|
-
'f.timeoutMsHint': 'Общий таймаут генерации: submit + poll + download.',
|
|
798
|
-
'f.deliverAs': 'Как картинка попадает в чат',
|
|
799
|
-
'f.deliverAsHint': 'link — в результате текст со ссылкой, карточка рисует картинку по ней; работает с любой чат-моделью. image — в результате сама картинка, текстовая модель её прочитать не может, поэтому нужен dsh-vision-bridge или модель с vision.',
|
|
800
|
-
'f.customBaseURL': 'Свой API: базовый URL',
|
|
801
|
-
'f.customBaseURLHint': 'Корень API без завершающего слэша. Запрос уходит на {base}/images/generations.',
|
|
802
|
-
'f.customModel': 'Свой API: модель',
|
|
803
|
-
'f.customModelHint': 'ID модели для этого API, например gpt-image-1.',
|
|
804
|
-
'f.customKeyEnv': 'Свой API: ссылка на ключ',
|
|
805
|
-
'f.customKeyEnvHint': 'Имя credential-ссылки / переменной окружения с ключом. Пусто — без заголовка авторизации, для шлюзов, которым он не нужен.',
|
|
806
|
-
'f.customSize': 'Свой API: фиксированный размер',
|
|
807
|
-
'f.customSizeHint': 'Пусто — именованный размер переводится сам (square_hd → 1024x1024). Заполняйте только для API, придирчивого к размерам.',
|
|
808
|
-
'f.outputDir': 'Папка вывода',
|
|
809
|
-
'f.outputDirHint': 'Относительно рабочей папки сессии; абсолютный путь используется как есть.',
|
|
810
|
-
'p.model': 'Пример: fal-ai/flux-2/klein/9b',
|
|
811
|
-
'p.apiKeyEnv': 'Пример: FAL_API_KEY',
|
|
812
|
-
'p.baseURL': 'Пример: https://queue.fal.run',
|
|
813
|
-
'p.pollIntervalMs': 'Пример: 2000',
|
|
814
|
-
'p.timeoutMs': 'Пример: 180000',
|
|
815
|
-
'p.customBaseURL': 'Пример: https://api.openai.com/v1',
|
|
816
|
-
'p.customModel': 'Пример: gpt-image-1',
|
|
817
|
-
'p.customKeyEnv': 'Пример: OPENAI_API_KEY',
|
|
818
|
-
'p.customSize': 'Пример: 1024x1024',
|
|
819
|
-
'p.outputDir': 'Пример: generated/images',
|
|
820
|
-
'history.title': 'История генераций',
|
|
821
|
-
'history.all': 'Все провайдеры',
|
|
822
|
-
'history.empty': 'Ещё нет генераций',
|
|
823
|
-
'history.loading': 'Загрузка…',
|
|
824
|
-
}
|
|
538
|
+
const tabs = [
|
|
539
|
+
{ id: 'general', label: t('tab.general'), icon: '⚙️' },
|
|
540
|
+
{ id: 'provider', label: t('tab.provider') + ' (' + currentProvider.toUpperCase() + ')', icon: '🔌' },
|
|
541
|
+
{ id: 'enhancer', label: t('tab.enhancer'), icon: '✨' },
|
|
542
|
+
{ id: 'safety', label: t('tab.safety'), icon: '🛡️' },
|
|
543
|
+
{ id: 'cache', label: t('tab.cache'), icon: '⚡' },
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
const currentTabFields = FIELDS.filter((entry) => {
|
|
547
|
+
if (entry.tab !== activeTab) return false
|
|
548
|
+
if (entry.when && !entry.when.includes(currentProvider)) return false
|
|
549
|
+
return true
|
|
550
|
+
})
|
|
825
551
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
552
|
+
const diskCacheOn = state.diskCache && state.diskCache.text !== 'false'
|
|
553
|
+
const qualityGateOn = state.qualityGate && state.qualityGate.text !== 'false'
|
|
554
|
+
const budgetText = (state.dailyBudgetUsd && state.dailyBudgetUsd.text) || '0'
|
|
555
|
+
const loopLimit = (state.loopGuardLimit && state.loopGuardLimit.text) || '3'
|
|
556
|
+
|
|
557
|
+
return react.createElement(
|
|
558
|
+
'li',
|
|
559
|
+
{ className: 'ig-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
|
|
560
|
+
// Header
|
|
561
|
+
react.createElement(
|
|
562
|
+
'button',
|
|
563
|
+
{
|
|
564
|
+
type: 'button',
|
|
565
|
+
style: {
|
|
566
|
+
background: 'none',
|
|
567
|
+
border: 'none',
|
|
568
|
+
cursor: 'pointer',
|
|
569
|
+
display: 'flex',
|
|
570
|
+
alignItems: 'center',
|
|
571
|
+
width: '100%',
|
|
572
|
+
padding: 0,
|
|
573
|
+
textAlign: 'left',
|
|
574
|
+
},
|
|
575
|
+
'aria-expanded': open,
|
|
576
|
+
onClick: () => setOpen(!open),
|
|
577
|
+
},
|
|
578
|
+
react.createElement(
|
|
579
|
+
'div',
|
|
580
|
+
{ style: { flex: 1 } },
|
|
581
|
+
react.createElement(
|
|
582
|
+
'div',
|
|
583
|
+
{ style: { fontWeight: 700, fontSize: '16px', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
584
|
+
'🎨 ' + t('settings.title')
|
|
585
|
+
),
|
|
586
|
+
react.createElement(
|
|
587
|
+
'div',
|
|
588
|
+
{ style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
589
|
+
t('settings.description')
|
|
590
|
+
)
|
|
591
|
+
),
|
|
592
|
+
state.dirty
|
|
593
|
+
? react.createElement('span', { className: 'ig-badge ig-badge-warn', style: { marginRight: '8px' } }, t('settings.unsaved'))
|
|
594
|
+
: null,
|
|
595
|
+
react.createElement(
|
|
596
|
+
'span',
|
|
597
|
+
{ style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
|
|
598
|
+
ChevronIconNode
|
|
599
|
+
)
|
|
600
|
+
),
|
|
601
|
+
open
|
|
602
|
+
? react.createElement(
|
|
603
|
+
'div',
|
|
604
|
+
{ className: 'ig-page' },
|
|
605
|
+
// Quick Stats Row
|
|
606
|
+
react.createElement(
|
|
607
|
+
'div',
|
|
608
|
+
{ className: 'ig-grid-4' },
|
|
609
|
+
react.createElement(
|
|
610
|
+
'div',
|
|
611
|
+
{ className: 'ig-stat-box' },
|
|
612
|
+
react.createElement('div', { className: 'ig-stat-val' }, currentProvider.toUpperCase()),
|
|
613
|
+
react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.active_provider'))
|
|
614
|
+
),
|
|
615
|
+
react.createElement(
|
|
616
|
+
'div',
|
|
617
|
+
{ className: 'ig-stat-box' },
|
|
618
|
+
react.createElement('div', { className: 'ig-stat-val' }, ((state.defaultSize && state.defaultSize.text) || '4:3') + ' · ' + ((state.defaultFormat && state.defaultFormat.text) || 'png')),
|
|
619
|
+
react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.format_size'))
|
|
620
|
+
),
|
|
621
|
+
react.createElement(
|
|
622
|
+
'div',
|
|
623
|
+
{ className: 'ig-stat-box' },
|
|
624
|
+
react.createElement(
|
|
625
|
+
'div',
|
|
626
|
+
{ className: 'ig-stat-val' },
|
|
627
|
+
react.createElement(
|
|
628
|
+
'span',
|
|
629
|
+
{ className: qualityGateOn ? 'ig-badge ig-badge-ok' : 'ig-badge ig-badge-warn' },
|
|
630
|
+
qualityGateOn ? 'Gate Active' : 'Off'
|
|
631
|
+
),
|
|
632
|
+
' ',
|
|
633
|
+
react.createElement('span', { className: 'ig-badge ig-badge-ok' }, 'Guard ' + loopLimit)
|
|
634
|
+
),
|
|
635
|
+
react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.safety_status'))
|
|
636
|
+
),
|
|
637
|
+
react.createElement(
|
|
638
|
+
'div',
|
|
639
|
+
{ className: 'ig-stat-box' },
|
|
640
|
+
react.createElement(
|
|
641
|
+
'div',
|
|
642
|
+
{ className: 'ig-stat-val' },
|
|
643
|
+
react.createElement(
|
|
644
|
+
'span',
|
|
645
|
+
{ className: diskCacheOn ? 'ig-badge ig-badge-ok' : 'ig-badge ig-badge-warn' },
|
|
646
|
+
diskCacheOn ? 'Cache ON' : 'Off'
|
|
647
|
+
),
|
|
648
|
+
' ',
|
|
649
|
+
budgetText !== '0' ? '$' + budgetText : 'No limit'
|
|
650
|
+
),
|
|
651
|
+
react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.budget_cache'))
|
|
652
|
+
)
|
|
653
|
+
),
|
|
654
|
+
// Tab Navigation
|
|
655
|
+
react.createElement(
|
|
656
|
+
'div',
|
|
657
|
+
{ className: 'ig-tabs' },
|
|
658
|
+
tabs.map((tab) =>
|
|
659
|
+
react.createElement(
|
|
660
|
+
'button',
|
|
661
|
+
{
|
|
662
|
+
key: tab.id,
|
|
663
|
+
type: 'button',
|
|
664
|
+
className: activeTab === tab.id ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
|
|
665
|
+
onClick: () => setActiveTab(tab.id),
|
|
666
|
+
},
|
|
667
|
+
tab.icon + ' ' + tab.label
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
),
|
|
671
|
+
// Active Tab Fields
|
|
672
|
+
react.createElement(
|
|
673
|
+
'div',
|
|
674
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
|
|
675
|
+
currentTabFields.map((entry) =>
|
|
676
|
+
react.createElement(Field, {
|
|
677
|
+
key: entry.field,
|
|
678
|
+
entry,
|
|
679
|
+
state: state[entry.field] ?? { text: '', overridden: false, invalid: false },
|
|
680
|
+
fieldProps,
|
|
681
|
+
t,
|
|
682
|
+
onEdit: (val) => props.edit && props.edit(entry.field, val),
|
|
683
|
+
onReset: () => props.resetField && props.resetField(entry.field),
|
|
684
|
+
})
|
|
685
|
+
)
|
|
686
|
+
),
|
|
687
|
+
// Footer Action Bar
|
|
688
|
+
react.createElement(
|
|
689
|
+
'div',
|
|
690
|
+
{
|
|
691
|
+
style: {
|
|
692
|
+
display: 'flex',
|
|
693
|
+
justifyContent: 'flex-end',
|
|
694
|
+
alignItems: 'center',
|
|
695
|
+
gap: '10px',
|
|
696
|
+
paddingTop: '12px',
|
|
697
|
+
borderTop: '1px solid var(--dsw-alias-border-l2)',
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
state.failed
|
|
701
|
+
? react.createElement('span', { className: 'ig-alert-err', style: { marginRight: 'auto', padding: '4px 10px' } }, t('settings.saveFailed'))
|
|
702
|
+
: null,
|
|
703
|
+
react.createElement(
|
|
704
|
+
'button',
|
|
705
|
+
{
|
|
706
|
+
type: 'button',
|
|
707
|
+
className: 'ig-btn',
|
|
708
|
+
disabled: !state.dirty || state.saving,
|
|
709
|
+
onClick: props.discard,
|
|
710
|
+
},
|
|
711
|
+
t('settings.discard')
|
|
712
|
+
),
|
|
713
|
+
react.createElement(
|
|
714
|
+
'button',
|
|
715
|
+
{
|
|
716
|
+
type: 'button',
|
|
717
|
+
className: 'ig-btn ig-btn-primary',
|
|
718
|
+
disabled: blocked,
|
|
719
|
+
onClick: props.save,
|
|
720
|
+
},
|
|
721
|
+
t(state.saving ? 'settings.saving' : 'settings.save')
|
|
722
|
+
)
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
: null
|
|
726
|
+
)
|
|
845
727
|
}
|
|
846
728
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
*
|
|
850
|
-
* В режиме «image» она приходит блоком вложения. В режиме «link» результат
|
|
851
|
-
* намеренно только текстовый — иначе чат-модель без vision на нём падает —
|
|
852
|
-
* и ссылка на наш же роут разбирается отсюда.
|
|
853
|
-
*/
|
|
854
|
-
// Старое имя оставлено намеренно: ссылки в уже отправленных сообщениях
|
|
855
|
-
// указывают на прежний маршрут, и без него картинки в истории разговоров
|
|
856
|
-
// перестали бы показываться.
|
|
857
|
-
const IMAGE_URL_RE = /\/dsh-(?:fal-)?image-gen\/image\?[^\s)]+/
|
|
729
|
+
// -------------------------------------------------------------- Toolview Component
|
|
730
|
+
const IMAGE_URL_RE = /https?:\/\/[^\s)]+\.(png|jpg|jpeg|webp|gif)(\?[^\s)]*)?/i
|
|
858
731
|
|
|
859
732
|
function readResult(block) {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
for (const part of content) {
|
|
864
|
-
if (part && part.type === 'image' && part.attachment) {
|
|
865
|
-
attachment = part.attachment
|
|
866
|
-
} else if (part && part.type === 'text' && typeof part.text === 'string') {
|
|
867
|
-
text += (text ? String.fromCharCode(10) : '') + part.text
|
|
868
|
-
}
|
|
869
|
-
}
|
|
733
|
+
if (!block) return { text: '', url: '', attachment: null }
|
|
734
|
+
const text = block.output || block.text || ''
|
|
735
|
+
const attachment = block.attachment || (block.attachments && block.attachments[0]) || null
|
|
870
736
|
const linked = text.match(IMAGE_URL_RE)
|
|
871
737
|
return {
|
|
872
|
-
attachment
|
|
738
|
+
attachment,
|
|
873
739
|
url: linked ? linked[0] : '',
|
|
874
|
-
// Ссылку из подписи убираем: она уже стала картинкой.
|
|
875
740
|
text: linked ? text.replace(linked[0], '').trim() : text,
|
|
876
741
|
}
|
|
877
742
|
}
|
|
878
743
|
|
|
879
|
-
function FalImageCard(props) {
|
|
744
|
+
function FalImageCard(props) {
|
|
880
745
|
const block = props.block
|
|
881
746
|
const running = !('kind' in (block || {}))
|
|
882
747
|
const failed = block && (block.isError || block.error !== undefined)
|
|
883
748
|
const parsed = readResult(block)
|
|
749
|
+
const t = props.t || ((k) => k)
|
|
884
750
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
if (raw) args = JSON.parse(raw)
|
|
889
|
-
} catch (e) { /* ignore */ }
|
|
751
|
+
react.useEffect(() => {
|
|
752
|
+
ensureCss()
|
|
753
|
+
}, [])
|
|
890
754
|
|
|
755
|
+
let args = {}
|
|
891
756
|
let prompt = ''
|
|
892
757
|
try {
|
|
893
758
|
const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const t = props.t || ((k) => k)
|
|
898
|
-
const repeatText = (() => {
|
|
899
|
-
const parts = [String(args.prompt || '').trim()]
|
|
900
|
-
const size = args.image_size || args.size
|
|
901
|
-
const seed = args.seed
|
|
902
|
-
if (size) parts.push(`${t('card.sizePrefix')}: ${size}`)
|
|
903
|
-
if (seed !== undefined) parts.push(`${t('card.seedPrefix')}: ${seed}`)
|
|
904
|
-
return parts.filter(Boolean).join(', ')
|
|
905
|
-
})()
|
|
906
|
-
|
|
907
|
-
const [editing, setEditing] = react.useState(false)
|
|
908
|
-
const [editText, setEditText] = react.useState('')
|
|
909
|
-
|
|
910
|
-
const onEditPrompt = () => {
|
|
911
|
-
const textToInsert = prompt || repeatText || ''
|
|
912
|
-
if (typeof document !== 'undefined') {
|
|
913
|
-
const input = document.querySelector('textarea, [contenteditable="true"], input[type="text"]')
|
|
914
|
-
if (input) {
|
|
915
|
-
if (input.tagName === 'TEXTAREA' || input.tagName === 'INPUT') {
|
|
916
|
-
input.value = textToInsert
|
|
917
|
-
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
918
|
-
input.focus()
|
|
919
|
-
} else if (input.isContentEditable) {
|
|
920
|
-
input.innerText = textToInsert
|
|
921
|
-
input.focus()
|
|
922
|
-
}
|
|
923
|
-
}
|
|
759
|
+
if (raw) {
|
|
760
|
+
args = JSON.parse(raw)
|
|
761
|
+
prompt = args.prompt || ''
|
|
924
762
|
}
|
|
925
|
-
|
|
926
|
-
}
|
|
763
|
+
} catch (_) {}
|
|
927
764
|
|
|
928
765
|
const [copied, setCopied] = react.useState(false)
|
|
929
766
|
|
|
@@ -934,7 +771,7 @@ function FalImageCard(props) {
|
|
|
934
771
|
const binding = current && sessions.binding(current)
|
|
935
772
|
const session = binding && binding.session
|
|
936
773
|
if (session && session.prompt) await session.prompt([{ type: 'text', text }], 'queue')
|
|
937
|
-
} catch (
|
|
774
|
+
} catch (_) {}
|
|
938
775
|
}
|
|
939
776
|
|
|
940
777
|
const onCopyPrompt = () => {
|
|
@@ -945,57 +782,30 @@ function FalImageCard(props) {
|
|
|
945
782
|
}
|
|
946
783
|
}
|
|
947
784
|
|
|
948
|
-
const onCopySeed = () => {
|
|
949
|
-
if (args.seed !== undefined && typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
950
|
-
navigator.clipboard.writeText(String(args.seed))
|
|
951
|
-
setCopied(true)
|
|
952
|
-
setTimeout(() => setCopied(false), 2000)
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
785
|
const onReroll = () => {
|
|
957
|
-
const nextSeed =
|
|
958
|
-
sendActionPrompt(
|
|
786
|
+
const nextSeed = typeof args.seed === 'number' ? args.seed + 1 : Math.floor(Math.random() * 100000)
|
|
787
|
+
sendActionPrompt(t('card.actionReroll') + ' (' + nextSeed + '): ' + (prompt || ''))
|
|
959
788
|
}
|
|
960
789
|
|
|
961
790
|
const onUpscale = () => {
|
|
962
|
-
const ref = parsed.attachment ? parsed.attachment.attachmentId : (parsed.url || '')
|
|
963
|
-
sendActionPrompt(
|
|
791
|
+
const ref = parsed.attachment ? (parsed.attachment.attachmentId || parsed.attachment.id) : (parsed.url || '')
|
|
792
|
+
sendActionPrompt(t('card.actionUpscale') + ': upscale_image(image: "' + ref + '")')
|
|
964
793
|
}
|
|
965
794
|
|
|
966
795
|
const onRemoveBg = () => {
|
|
967
|
-
const ref = parsed.attachment ? parsed.attachment.attachmentId : (parsed.url || '')
|
|
968
|
-
sendActionPrompt(
|
|
796
|
+
const ref = parsed.attachment ? (parsed.attachment.attachmentId || parsed.attachment.id) : (parsed.url || '')
|
|
797
|
+
sendActionPrompt(t('card.actionRemoveBg') + ': remove_background(image: "' + ref + '")')
|
|
969
798
|
}
|
|
970
799
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
running ? t('card.generating') : failed ? t('card.failed') : t('card.image')
|
|
975
|
-
|
|
976
|
-
// «Повторить генерацию»: отправляет в чат новое сообщение с теми же
|
|
977
|
-
// промптом и параметрами, извлечёнными из исходного вызова инструмента.
|
|
978
|
-
// Дальше модель сама решает, как сгенерировать, — это просто повторный
|
|
979
|
-
// запрос, а не вызов инструмента из карточки.
|
|
980
|
-
|
|
981
|
-
const repeat = async () => {
|
|
982
|
-
try {
|
|
983
|
-
const sessions = props.sessions
|
|
984
|
-
const current = sessions && sessions.list && sessions.list.current
|
|
985
|
-
const binding = current && sessions.binding(current)
|
|
986
|
-
const session = binding && binding.session
|
|
987
|
-
if (!session || !session.prompt) {
|
|
988
|
-
return
|
|
989
|
-
}
|
|
990
|
-
const text = repeatText || String(args.prompt || '')
|
|
991
|
-
await session.prompt([{ type: 'text', text }], 'queue')
|
|
992
|
-
} catch (e) { /* контур сам покажет ошибку отправки */ }
|
|
993
|
-
}
|
|
800
|
+
const head = react.createElement(
|
|
801
|
+
'div',
|
|
802
|
+
{ className: 'fal_head' },
|
|
803
|
+
running ? '⏳ ' + t('card.generating') : failed ? '❌ ' + t('card.failed') : '🖼️ ' + t('card.image')
|
|
804
|
+
)
|
|
994
805
|
|
|
995
806
|
const body = []
|
|
996
|
-
if (prompt)
|
|
997
|
-
|
|
998
|
-
body.push(react.createElement('div', { className: 'fal-meta', key: 'orig' }, `${t('card.origPrompt')}: ${args.originalPrompt}`))
|
|
807
|
+
if (prompt) {
|
|
808
|
+
body.push(react.createElement('div', { className: 'fal-prompt', key: 'p' }, prompt))
|
|
999
809
|
}
|
|
1000
810
|
if (failed) {
|
|
1001
811
|
body.push(react.createElement('div', { className: 'fal-err', key: 'e' }, parsed.text || t('card.unknownError')))
|
|
@@ -1005,11 +815,9 @@ function FalImageCard(props) {
|
|
|
1005
815
|
src: parsed.url,
|
|
1006
816
|
alt: prompt || 'generated image',
|
|
1007
817
|
loading: 'lazy',
|
|
818
|
+
style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
|
|
1008
819
|
}))
|
|
1009
|
-
if (parsed.text) body.push(react.createElement('div', { className: 'fal-meta', key: 'm' }, parsed.text))
|
|
1010
820
|
} else if (parsed.attachment) {
|
|
1011
|
-
// The attachment store verifies the whole reference, so the card hands
|
|
1012
|
-
// back the metadata the tool result already carries.
|
|
1013
821
|
const a = parsed.attachment
|
|
1014
822
|
const query = 'id=' + encodeURIComponent(a.attachmentId ?? a.id ?? '')
|
|
1015
823
|
+ '&mt=' + encodeURIComponent(a.mediaType || 'image/png')
|
|
@@ -1021,281 +829,481 @@ function FalImageCard(props) {
|
|
|
1021
829
|
src: '/dsh-image-gen/image?' + query,
|
|
1022
830
|
alt: prompt || 'generated image',
|
|
1023
831
|
loading: 'lazy',
|
|
832
|
+
style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
|
|
1024
833
|
}))
|
|
1025
|
-
if (parsed.text) body.push(react.createElement('div', { className: 'fal-meta', key: 'm' }, parsed.text))
|
|
1026
|
-
} else if (!running && parsed.text) {
|
|
1027
|
-
body.push(react.createElement('div', { className: 'fal-meta', key: 'm' }, parsed.text))
|
|
1028
834
|
}
|
|
1029
835
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
// Диагностика: метаданные генерации (prompt/seed/size/format), чтобы было
|
|
1040
|
-
// понятно, по каким параметрам получилась именно эта картинка.
|
|
1041
|
-
const metaParts = []
|
|
1042
|
-
if (args.image_size || args.size) metaParts.push(`size ${args.image_size || args.size}`)
|
|
1043
|
-
if (args.output_format || args.format) metaParts.push(`format ${args.output_format || args.format}`)
|
|
1044
|
-
if (args.seed !== undefined) metaParts.push(`seed ${args.seed}`)
|
|
1045
|
-
const meta = metaParts.join(' · ')
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (!running && !failed) {
|
|
1049
|
-
const btns = []
|
|
1050
|
-
btns.push(react.createElement('button', {
|
|
1051
|
-
key: 'btn-reroll',
|
|
1052
|
-
type: 'button',
|
|
1053
|
-
className: 'fal-action-btn',
|
|
1054
|
-
onClick: onReroll,
|
|
1055
|
-
title: t('card.rerollTitle')
|
|
1056
|
-
}, '🔄 Re-roll'))
|
|
1057
|
-
|
|
1058
|
-
btns.push(react.createElement('button', {
|
|
1059
|
-
key: 'btn-up',
|
|
1060
|
-
type: 'button',
|
|
1061
|
-
className: 'fal-action-btn',
|
|
1062
|
-
onClick: onUpscale,
|
|
1063
|
-
title: t('card.upscaleTitle')
|
|
1064
|
-
}, '🔍 2x Upscale'))
|
|
1065
|
-
|
|
1066
|
-
btns.push(react.createElement('button', {
|
|
1067
|
-
key: 'btn-nobg',
|
|
1068
|
-
type: 'button',
|
|
1069
|
-
className: 'fal-action-btn',
|
|
1070
|
-
onClick: onRemoveBg,
|
|
1071
|
-
title: t('card.removeBgTitle')
|
|
1072
|
-
}, '✂️ Remove BG'))
|
|
1073
|
-
|
|
1074
|
-
if (prompt) {
|
|
1075
|
-
btns.push(react.createElement('button', {
|
|
1076
|
-
key: 'btn-copy-p',
|
|
1077
|
-
type: 'button',
|
|
1078
|
-
className: 'fal-action-btn',
|
|
1079
|
-
onClick: onCopyPrompt,
|
|
1080
|
-
title: t('card.copyPromptTitle')
|
|
1081
|
-
}, copied ? t('card.copied') : t('card.copyPrompt')))
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
if (args.seed !== undefined) {
|
|
1085
|
-
btns.push(react.createElement('button', {
|
|
1086
|
-
key: 'btn-copy-s',
|
|
1087
|
-
type: 'button',
|
|
1088
|
-
className: 'fal-action-btn',
|
|
1089
|
-
onClick: onCopySeed,
|
|
1090
|
-
title: t('card.copySeedTitle')
|
|
1091
|
-
}, `🎲 ${t('card.seedPrefix')}: ${args.seed}`))
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
body.push(react.createElement('div', { key: 'action-row', className: 'fal-btn-row' }, btns))
|
|
836
|
+
const actions = []
|
|
837
|
+
if (!running && !failed && (parsed.url || parsed.attachment)) {
|
|
838
|
+
actions.push(
|
|
839
|
+
react.createElement('button', { key: 'reroll', type: 'button', className: 'ig-btn', onClick: onReroll }, '🎲 ' + t('card.reroll')),
|
|
840
|
+
react.createElement('button', { key: 'upscale', type: 'button', className: 'ig-btn', onClick: onUpscale }, '🔍 ' + t('card.upscale')),
|
|
841
|
+
react.createElement('button', { key: 'removeBg', type: 'button', className: 'ig-btn', onClick: onRemoveBg }, '✂️ ' + t('card.removeBg')),
|
|
842
|
+
react.createElement('button', { key: 'copy', type: 'button', className: 'ig-btn', onClick: onCopyPrompt }, copied ? '✓ Copied' : '📋 ' + t('card.copyPrompt'))
|
|
843
|
+
)
|
|
1095
844
|
}
|
|
1096
845
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
className: 'fal-repeat',
|
|
1106
|
-
onClick: repeat,
|
|
1107
|
-
}, t('card.repeatGen')))
|
|
1108
|
-
}
|
|
1109
|
-
if (!running && !failed) {
|
|
1110
|
-
if (editing) {
|
|
1111
|
-
body.push(react.createElement('input', {
|
|
1112
|
-
key: 'editinput',
|
|
1113
|
-
type: 'text',
|
|
1114
|
-
className: 'fal-input',
|
|
1115
|
-
value: editText,
|
|
1116
|
-
placeholder: prompt || t('card.newPromptPlaceholder'),
|
|
1117
|
-
onChange: (e) => setEditText(e.target.value),
|
|
1118
|
-
}))
|
|
1119
|
-
body.push(react.createElement('button', {
|
|
1120
|
-
key: 'editsend',
|
|
1121
|
-
type: 'button',
|
|
1122
|
-
className: 'fal-repeat',
|
|
1123
|
-
onClick: async () => {
|
|
1124
|
-
const text = (editText || prompt || '').trim()
|
|
1125
|
-
if (!text) return
|
|
1126
|
-
try {
|
|
1127
|
-
const sessions = props.sessions
|
|
1128
|
-
const current = sessions && sessions.list && sessions.list.current
|
|
1129
|
-
const binding = current && sessions.binding(current)
|
|
1130
|
-
const session = binding && binding.session
|
|
1131
|
-
if (session && session.prompt) await session.prompt([{ type: 'text', text }], 'queue')
|
|
1132
|
-
} catch (e) { /* контур сам покажет */ }
|
|
1133
|
-
setEditing(false)
|
|
1134
|
-
setEditText('')
|
|
1135
|
-
},
|
|
1136
|
-
}, t('card.send')))
|
|
1137
|
-
} else {
|
|
1138
|
-
body.push(react.createElement('button', {
|
|
1139
|
-
key: 'edit',
|
|
1140
|
-
type: 'button',
|
|
1141
|
-
className: 'fal-repeat',
|
|
1142
|
-
onClick: () => { setEditText(prompt || ''); setEditing(true) },
|
|
1143
|
-
}, t('card.editPrompt')))
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
if (!running && !failed && downloadUrl) {
|
|
1147
|
-
body.push(react.createElement('a', {
|
|
1148
|
-
key: 'download',
|
|
1149
|
-
className: 'fal-repeat',
|
|
1150
|
-
href: downloadUrl,
|
|
1151
|
-
download: 'generated-image.png',
|
|
1152
|
-
}, t('card.download')))
|
|
1153
|
-
}
|
|
846
|
+
return react.createElement(
|
|
847
|
+
'div',
|
|
848
|
+
{ className: 'ig-section-card', style: { margin: '8px 0' } },
|
|
849
|
+
head,
|
|
850
|
+
body,
|
|
851
|
+
actions.length > 0 ? react.createElement('div', { className: 'ig-row', style: { marginTop: '8px' } }, actions) : null
|
|
852
|
+
)
|
|
853
|
+
}
|
|
1154
854
|
|
|
1155
|
-
|
|
855
|
+
// -------------------------------------------------------------- Dictionaries
|
|
856
|
+
const en = {
|
|
857
|
+
'settings.navLabel': 'Image Studio',
|
|
858
|
+
'settings.title': 'Image Studio',
|
|
859
|
+
'settings.description': 'AI Image generation, inpainting, variations, upscale & vectorization.',
|
|
860
|
+
'settings.unavailable': 'Plugin settings are initializing. They will appear automatically in a few seconds.',
|
|
861
|
+
'settings.unsaved': 'Unsaved changes',
|
|
862
|
+
'settings.saving': 'Saving…',
|
|
863
|
+
'settings.save': 'Save settings',
|
|
864
|
+
'settings.discard': 'Discard changes',
|
|
865
|
+
'settings.saveFailed': 'Failed to save settings. Please check credentials and try again.',
|
|
866
|
+
'settings.readOnly': 'Settings are read-only in this context.',
|
|
867
|
+
'settings.overridden': 'overridden',
|
|
868
|
+
'settings.reset': 'Reset to default',
|
|
869
|
+
'settings.invalidNumber': 'Please enter a valid number',
|
|
870
|
+
'settings.collapse': 'Collapse',
|
|
871
|
+
'settings.expand': 'Expand',
|
|
872
|
+
'settings.credentialHint': 'API keys are stored in DSH Credentials (~/.dsh/.credentials.yaml) and never leaked in config files.',
|
|
873
|
+
|
|
874
|
+
'tab.general': 'General',
|
|
875
|
+
'tab.provider': 'Provider',
|
|
876
|
+
'tab.enhancer': 'Prompt & Styles',
|
|
877
|
+
'tab.safety': 'Safety & Budget',
|
|
878
|
+
'tab.cache': 'Cache & Storage',
|
|
879
|
+
|
|
880
|
+
'stat.active_provider': 'Active Provider',
|
|
881
|
+
'stat.format_size': 'Default Specs',
|
|
882
|
+
'stat.safety_status': 'Safety & Loop Guard',
|
|
883
|
+
'stat.budget_cache': 'Budget & Cache',
|
|
884
|
+
|
|
885
|
+
'card.generating': 'Generating image…',
|
|
886
|
+
'card.failed': 'Generation failed',
|
|
887
|
+
'card.image': 'Image Studio Result',
|
|
888
|
+
'card.unknownError': 'An unknown error occurred during generation.',
|
|
889
|
+
'card.sizePrefix': 'Size',
|
|
890
|
+
'card.seedPrefix': 'Seed',
|
|
891
|
+
'card.actionReroll': 'Reroll image',
|
|
892
|
+
'card.actionUpscale': 'Upscale image',
|
|
893
|
+
'card.actionRemoveBg': 'Remove background',
|
|
894
|
+
'card.reroll': 'Reroll',
|
|
895
|
+
'card.upscale': 'Upscale 2x/4x',
|
|
896
|
+
'card.removeBg': 'Remove BG',
|
|
897
|
+
'card.copyPrompt': 'Copy Prompt',
|
|
898
|
+
'card.origPrompt': 'Original Prompt',
|
|
899
|
+
|
|
900
|
+
'f.enabled': 'Master Switch',
|
|
901
|
+
'f.enabledHint': 'Enable or disable image generation tools across the workspace.',
|
|
902
|
+
'f.provider': 'Image Provider',
|
|
903
|
+
'f.providerHint': 'Backend service used for image generation.',
|
|
904
|
+
'f.inherit': 'Default / Inherit',
|
|
905
|
+
'f.needFal': 'Requires FAL API key',
|
|
906
|
+
'f.needCustom': 'Requires OpenAI-compatible endpoint',
|
|
907
|
+
'f.needCodex': 'Built-in Codex subscription',
|
|
908
|
+
'f.needGrok': 'Built-in Grok subscription',
|
|
909
|
+
'f.needLocal': 'Local ComfyUI or A1111',
|
|
910
|
+
'f.needSeedream': 'Requires SeaDream API key',
|
|
911
|
+
'f.needGemini': 'Requires Google Gemini API key',
|
|
912
|
+
'f.needReplicate': 'Requires Replicate API token',
|
|
913
|
+
|
|
914
|
+
'f.defaultSize': 'Default Aspect Ratio',
|
|
915
|
+
'f.defaultSizeHint': 'Aspect ratio and resolution applied when image_size is omitted.',
|
|
916
|
+
'f.defaultFormat': 'Output Format',
|
|
917
|
+
'f.defaultFormatHint': 'Target file format (PNG, JPEG, or WEBP).',
|
|
918
|
+
'f.deliverAs': 'Delivery Mode',
|
|
919
|
+
'f.deliverAsHint': 'How images reach conversation: "link" (text LLM safe) or "image" (multimodal vision).',
|
|
920
|
+
'f.outputDir': 'Output Directory',
|
|
921
|
+
'f.outputDirHint': 'Directory path where generated images are saved on disk.',
|
|
922
|
+
'p.outputDir': 'generated/images',
|
|
923
|
+
'f.historyLimit': 'History Limit',
|
|
924
|
+
'f.historyLimitHint': 'Maximum number of recent generation records kept in memory.',
|
|
925
|
+
'p.historyLimit': '50',
|
|
926
|
+
|
|
927
|
+
'f.model': 'FAL Model ID',
|
|
928
|
+
'f.modelHint': 'Target model on FAL endpoint (e.g. fal-ai/flux-2/klein/9b).',
|
|
929
|
+
'p.model': 'fal-ai/flux-2/klein/9b',
|
|
930
|
+
'f.apiKeyEnv': 'FAL Key Reference',
|
|
931
|
+
'f.apiKeyEnvHint': 'Credential reference holding the FAL API key.',
|
|
932
|
+
'p.apiKeyEnv': 'FAL_API_KEY',
|
|
933
|
+
'f.baseURL': 'FAL Base URL',
|
|
934
|
+
'f.baseURLHint': 'FAL queue service endpoint.',
|
|
935
|
+
'p.baseURL': 'https://queue.fal.run',
|
|
936
|
+
'f.pollIntervalMs': 'Poll Interval (ms)',
|
|
937
|
+
'f.pollIntervalMsHint': 'Frequency of checking task progress on asynchronous queues.',
|
|
938
|
+
'p.pollIntervalMs': '2000',
|
|
939
|
+
'f.timeoutMs': 'Timeout (ms)',
|
|
940
|
+
'f.timeoutMsHint': 'Maximum overall timeout before failing a generation request.',
|
|
941
|
+
'p.timeoutMs': '180000',
|
|
942
|
+
|
|
943
|
+
'f.customBaseURL': 'Custom API Base URL',
|
|
944
|
+
'f.customBaseURLHint': 'OpenAI-compatible image endpoint root (e.g. https://api.openai.com/v1).',
|
|
945
|
+
'p.customBaseURL': 'https://api.openai.com/v1',
|
|
946
|
+
'f.customModel': 'Custom Model ID',
|
|
947
|
+
'f.customModelHint': 'Model name sent to custom images endpoint.',
|
|
948
|
+
'p.customModel': 'dall-e-3',
|
|
949
|
+
'f.customKeyEnv': 'Custom Key Reference',
|
|
950
|
+
'f.customKeyEnvHint': 'Credential reference holding the API key for custom endpoint.',
|
|
951
|
+
'p.customKeyEnv': 'OPENAI_API_KEY',
|
|
952
|
+
'f.customSize': 'Custom Image Size',
|
|
953
|
+
'f.customSizeHint': 'Fixed dimension sent to custom endpoint (e.g. 1024x1024).',
|
|
954
|
+
'p.customSize': '1024x1024',
|
|
955
|
+
|
|
956
|
+
'f.replicateModel': 'Replicate Model ID',
|
|
957
|
+
'f.replicateModelHint': 'Target model identifier on Replicate.',
|
|
958
|
+
'p.replicateModel': 'black-forest-labs/flux-schnell',
|
|
959
|
+
'f.replicateKeyEnv': 'Replicate Key Reference',
|
|
960
|
+
'f.replicateKeyEnvHint': 'Credential reference holding the Replicate API token.',
|
|
961
|
+
'p.replicateKeyEnv': 'REPLICATE_API_TOKEN',
|
|
962
|
+
|
|
963
|
+
'f.seedreamModel': 'SeaDream Model ID',
|
|
964
|
+
'f.seedreamModelHint': 'ByteDance SeaDream model identifier.',
|
|
965
|
+
'p.seedreamModel': 'seedream-4.0',
|
|
966
|
+
'f.seedreamKeyEnv': 'SeaDream Key Reference',
|
|
967
|
+
'f.seedreamKeyEnvHint': 'Credential reference holding the SeaDream API key.',
|
|
968
|
+
'p.seedreamKeyEnv': 'SEEDREAM_API_KEY',
|
|
969
|
+
'f.seedreamBaseURL': 'SeaDream Base URL',
|
|
970
|
+
'f.seedreamBaseURLHint': 'SeaDream API endpoint URL (ByteDance/Volcengine Ark).',
|
|
971
|
+
'p.seedreamBaseURL': 'https://api.bytedanceapi.com/v1',
|
|
972
|
+
|
|
973
|
+
'f.geminiModel': 'Gemini Model ID',
|
|
974
|
+
'f.geminiModelHint': 'Google Gemini image generation model identifier.',
|
|
975
|
+
'p.geminiModel': 'gemini-2.0-flash-exp-image-generation',
|
|
976
|
+
'f.geminiKeyEnv': 'Gemini Key Reference',
|
|
977
|
+
'f.geminiKeyEnvHint': 'Credential reference holding Google Gemini API key.',
|
|
978
|
+
'p.geminiKeyEnv': 'GEMINI_API_KEY',
|
|
979
|
+
|
|
980
|
+
'f.localKind': 'Local Engine Architecture',
|
|
981
|
+
'f.localKindHint': 'Select between ComfyUI API or Automatic1111 WebUI backend.',
|
|
982
|
+
'f.localBaseURL': 'Local Server URL',
|
|
983
|
+
'f.localBaseURLHint': 'Server address (e.g. http://127.0.0.1:8188 for ComfyUI, http://127.0.0.1:7860 for A1111).',
|
|
984
|
+
'p.localBaseURL': 'http://127.0.0.1:8188',
|
|
985
|
+
'f.localModel': 'Local Model / Checkpoint',
|
|
986
|
+
'f.localModelHint': 'Checkpoint name or ComfyUI workflow name.',
|
|
987
|
+
'p.localModel': 'v1-5-pruned-emaonly.safetensors',
|
|
988
|
+
'f.localSteps': 'Sampling Steps',
|
|
989
|
+
'f.localStepsHint': 'Number of denoising sampling steps.',
|
|
990
|
+
'p.localSteps': '20',
|
|
991
|
+
'f.localCfg': 'CFG Scale',
|
|
992
|
+
'f.localCfgHint': 'Classifier-free guidance scale.',
|
|
993
|
+
'p.localCfg': '7',
|
|
994
|
+
'f.localComfyUrl': 'Local ComfyUI URL (Legacy)',
|
|
995
|
+
'f.localComfyUrlHint': 'Direct ComfyUI endpoint URL.',
|
|
996
|
+
'p.localComfyUrl': 'http://127.0.0.1:8188',
|
|
997
|
+
'f.localA1111Url': 'Local A1111 URL (Legacy)',
|
|
998
|
+
'f.localA1111UrlHint': 'Direct Automatic1111 endpoint URL.',
|
|
999
|
+
'p.localA1111Url': 'http://127.0.0.1:7860',
|
|
1000
|
+
|
|
1001
|
+
'f.subscriptionQuality': 'Subscription Quality',
|
|
1002
|
+
'f.subscriptionQualityHint': 'Target quality profile when using Codex or Grok subscriptions.',
|
|
1003
|
+
|
|
1004
|
+
'f.enhancePrompt': 'LLM Prompt Enhancer',
|
|
1005
|
+
'f.enhancePromptHint': 'Expand concise prompts with rich artistic details before generation.',
|
|
1006
|
+
'f.enableLlmEnhancer': 'Enable LLM Enhancer (Legacy)',
|
|
1007
|
+
'f.enableLlmEnhancerHint': 'Legacy toggle for LLM prompt expansion.',
|
|
1008
|
+
'f.enhanceModel': 'Enhancement Model',
|
|
1009
|
+
'f.enhanceModelHint': 'Dedicated LLM model used to expand prompts (empty = conversation model).',
|
|
1010
|
+
'p.enhanceModel': 'Leave empty for default chat model',
|
|
1011
|
+
'f.enhanceBelowChars': 'Enhancement Threshold',
|
|
1012
|
+
'f.enhanceBelowCharsHint': 'Prompts longer than this character count will skip expansion.',
|
|
1013
|
+
'p.enhanceBelowChars': '200',
|
|
1014
|
+
'f.stylePreset': 'Artistic Style Preset',
|
|
1015
|
+
'f.stylePresetHint': 'Automatic visual style suffix added to all generated prompts.',
|
|
1016
|
+
|
|
1017
|
+
'f.qualityGate': 'Silent Quality Gate',
|
|
1018
|
+
'f.qualityGateHint': 'Automatically inspects generated frames and silently re-rolls blank or broken outputs.',
|
|
1019
|
+
'f.dailyBudgetUsd': 'Daily Spend Budget ($ USD)',
|
|
1020
|
+
'f.dailyBudgetUsdHint': 'Hard spending cap per day across all visual generation tools (0 = no limit).',
|
|
1021
|
+
'p.dailyBudgetUsd': '0.00',
|
|
1022
|
+
'f.loopGuardLimit': 'Session Loop Guard Limit',
|
|
1023
|
+
'f.loopGuardLimitHint': 'Maximum consecutive image generations allowed before requiring user input (0 = off).',
|
|
1024
|
+
'p.loopGuardLimit': '3',
|
|
1025
|
+
|
|
1026
|
+
'f.diskCache': 'Content-Addressed Disk Cache',
|
|
1027
|
+
'f.diskCacheHint': 'Instant <50ms retrieval and zero API cost for repeated identical generations.',
|
|
1028
|
+
'f.cacheBySeed': 'Cache by Seed + Prompt',
|
|
1029
|
+
'f.cacheBySeedHint': 'Reuse existing result if the exact same seed and prompt are requested.',
|
|
1030
|
+
'f.cacheByPrompt': 'Cache by Prompt Only',
|
|
1031
|
+
'f.cacheByPromptHint': 'Reuse existing result if the prompt has already been generated.',
|
|
1032
|
+
'f.pruneDays': 'Retention Period (Days)',
|
|
1033
|
+
'f.pruneDaysHint': 'Automatically delete image files and cache older than this many days (0 = keep forever).',
|
|
1034
|
+
'p.pruneDays': '0',
|
|
1035
|
+
'f.cacheTtlDays': 'Cache Retention (Legacy)',
|
|
1036
|
+
'f.cacheTtlDaysHint': 'Legacy name for retention period in days.',
|
|
1037
|
+
'p.cacheTtlDays': '0',
|
|
1156
1038
|
}
|
|
1157
1039
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1040
|
+
const ru = {
|
|
1041
|
+
'settings.navLabel': 'Image Studio',
|
|
1042
|
+
'settings.title': 'Генерация изображений',
|
|
1043
|
+
'settings.description': 'Мультипровайдерная студия: генерация, инпейнтинг, вариации, апскейл и векторизация.',
|
|
1044
|
+
'settings.unavailable': 'Настройки плагина инициализируются. Они появятся автоматически через несколько секунд.',
|
|
1045
|
+
'settings.unsaved': 'Несохранённые изменения',
|
|
1046
|
+
'settings.saving': 'Сохранение…',
|
|
1047
|
+
'settings.save': 'Сохранить настройки',
|
|
1048
|
+
'settings.discard': 'Сбросить изменения',
|
|
1049
|
+
'settings.saveFailed': 'Не удалось сохранить настройки. Проверьте параметры и попробуйте снова.',
|
|
1050
|
+
'settings.readOnly': 'Настройки доступны только для чтения.',
|
|
1051
|
+
'settings.overridden': 'переопределено',
|
|
1052
|
+
'settings.reset': 'Сбросить к умолчанию',
|
|
1053
|
+
'settings.invalidNumber': 'Введите корректное число',
|
|
1054
|
+
'settings.collapse': 'Свернуть',
|
|
1055
|
+
'settings.expand': 'Развернуть',
|
|
1056
|
+
'settings.credentialHint': 'Ключи API хранятся в DSH Credentials (~/.dsh/.credentials.yaml) и не попадают в файлы конфигурации.',
|
|
1057
|
+
|
|
1058
|
+
'tab.general': 'Основные',
|
|
1059
|
+
'tab.provider': 'Провайдер',
|
|
1060
|
+
'tab.enhancer': 'Промпт и стили',
|
|
1061
|
+
'tab.safety': 'Безопасность и бюджет',
|
|
1062
|
+
'tab.cache': 'Кэш и хранение',
|
|
1063
|
+
|
|
1064
|
+
'stat.active_provider': 'Активный провайдер',
|
|
1065
|
+
'stat.format_size': 'Параметры по умолчанию',
|
|
1066
|
+
'stat.safety_status': 'Защита и Loop Guard',
|
|
1067
|
+
'stat.budget_cache': 'Бюджет и кэширование',
|
|
1068
|
+
|
|
1069
|
+
'card.generating': 'Генерация изображения…',
|
|
1070
|
+
'card.failed': 'Ошибка генерации',
|
|
1071
|
+
'card.image': 'Результат Image Studio',
|
|
1072
|
+
'card.unknownError': 'Неизвестная ошибка во время создания изображения.',
|
|
1073
|
+
'card.sizePrefix': 'Размер',
|
|
1074
|
+
'card.seedPrefix': 'Сид',
|
|
1075
|
+
'card.actionReroll': 'Перегенерировать картинку',
|
|
1076
|
+
'card.actionUpscale': 'Увеличить разрешение',
|
|
1077
|
+
'card.actionRemoveBg': 'Удалить фон',
|
|
1078
|
+
'card.reroll': 'Реролл',
|
|
1079
|
+
'card.upscale': 'Апскейл 2x/4x',
|
|
1080
|
+
'card.removeBg': 'Без фона',
|
|
1081
|
+
'card.copyPrompt': 'Копировать промпт',
|
|
1082
|
+
'card.origPrompt': 'Исходный промпт',
|
|
1083
|
+
|
|
1084
|
+
'f.enabled': 'Главный выключатель',
|
|
1085
|
+
'f.enabledHint': 'Включение или отключение всех инструментов генерации картинок.',
|
|
1086
|
+
'f.provider': 'Провайдер генерации',
|
|
1087
|
+
'f.providerHint': 'Сервис, выполняющий непосредственное создание изображений.',
|
|
1088
|
+
'f.inherit': 'По умолчанию / наследовать',
|
|
1089
|
+
'f.needFal': 'Требуется ключ FAL API',
|
|
1090
|
+
'f.needCustom': 'Требуется OpenAI-совместимый эндпоинт',
|
|
1091
|
+
'f.needCodex': 'Встроенная подписка Codex',
|
|
1092
|
+
'f.needGrok': 'Встроенная подписка Grok',
|
|
1093
|
+
'f.needLocal': 'Локальный ComfyUI или Automatic1111',
|
|
1094
|
+
'f.needSeedream': 'Требуется ключ SeaDream API',
|
|
1095
|
+
'f.needGemini': 'Требуется ключ Google Gemini API',
|
|
1096
|
+
'f.needReplicate': 'Требуется токен Replicate API',
|
|
1097
|
+
|
|
1098
|
+
'f.defaultSize': 'Соотношение сторон по умолчанию',
|
|
1099
|
+
'f.defaultSizeHint': 'Разрешение и пропорции кадра, когда параметр image_size не передан.',
|
|
1100
|
+
'f.defaultFormat': 'Формат файла',
|
|
1101
|
+
'f.defaultFormatHint': 'Формат сохранения результатов (PNG, JPEG или WEBP).',
|
|
1102
|
+
'f.deliverAs': 'Способ доставки',
|
|
1103
|
+
'f.deliverAsHint': 'Способ передачи: "link" (безопасно для текстовых моделей) или "image" (для vision-моделей).',
|
|
1104
|
+
'f.outputDir': 'Каталог сохранения',
|
|
1105
|
+
'f.outputDirHint': 'Путь к папке сохранения сгенерированных файлов на диске.',
|
|
1106
|
+
'p.outputDir': 'generated/images',
|
|
1107
|
+
'f.historyLimit': 'Лимит истории',
|
|
1108
|
+
'f.historyLimitHint': 'Максимальное число последних генераций, удерживаемых в памяти.',
|
|
1109
|
+
'p.historyLimit': '50',
|
|
1110
|
+
|
|
1111
|
+
'f.model': 'Модель FAL',
|
|
1112
|
+
'f.modelHint': 'Идентификатор модели на сервисе FAL (например, fal-ai/flux-2/klein/9b).',
|
|
1113
|
+
'p.model': 'fal-ai/flux-2/klein/9b',
|
|
1114
|
+
'f.apiKeyEnv': 'Ключ FAL (credential-ref)',
|
|
1115
|
+
'f.apiKeyEnvHint': 'Ссылка на ключ доступа FAL в хранилище credentials.',
|
|
1116
|
+
'p.apiKeyEnv': 'FAL_API_KEY',
|
|
1117
|
+
'f.baseURL': 'Базовый URL FAL',
|
|
1118
|
+
'f.baseURLHint': 'Адрес сервиса очередей FAL.',
|
|
1119
|
+
'p.baseURL': 'https://queue.fal.run',
|
|
1120
|
+
'f.pollIntervalMs': 'Интервал опроса (мс)',
|
|
1121
|
+
'f.pollIntervalMsHint': 'Периодичность проверки статуса при асинхронной генерации.',
|
|
1122
|
+
'p.pollIntervalMs': '2000',
|
|
1123
|
+
'f.timeoutMs': 'Таймаут (мс)',
|
|
1124
|
+
'f.timeoutMsHint': 'Предельное время ожидания генерации до возврата ошибки.',
|
|
1125
|
+
'p.timeoutMs': '180000',
|
|
1126
|
+
|
|
1127
|
+
'f.customBaseURL': 'URL стороннего API',
|
|
1128
|
+
'f.customBaseURLHint': 'Адрес OpenAI-совместимого эндпоинта (например, https://api.openai.com/v1).',
|
|
1129
|
+
'p.customBaseURL': 'https://api.openai.com/v1',
|
|
1130
|
+
'f.customModel': 'Модель стороннего API',
|
|
1131
|
+
'f.customModelHint': 'Имя модели для отправки в сторонний API.',
|
|
1132
|
+
'p.customModel': 'dall-e-3',
|
|
1133
|
+
'f.customKeyEnv': 'Ключ стороннего API',
|
|
1134
|
+
'f.customKeyEnvHint': 'Имя ссылки на ключ для стороннего API.',
|
|
1135
|
+
'p.customKeyEnv': 'OPENAI_API_KEY',
|
|
1136
|
+
'f.customSize': 'Фиксированный размер',
|
|
1137
|
+
'f.customSizeHint': 'Точный размер кадра для стороннего API (например, 1024x1024).',
|
|
1138
|
+
'p.customSize': '1024x1024',
|
|
1139
|
+
|
|
1140
|
+
'f.replicateModel': 'Модель Replicate',
|
|
1141
|
+
'f.replicateModelHint': 'Идентификатор модели на Replicate.',
|
|
1142
|
+
'p.replicateModel': 'black-forest-labs/flux-schnell',
|
|
1143
|
+
'f.replicateKeyEnv': 'Ключ Replicate',
|
|
1144
|
+
'f.replicateKeyEnvHint': 'Имя ссылки на API-токен Replicate.',
|
|
1145
|
+
'p.replicateKeyEnv': 'REPLICATE_API_TOKEN',
|
|
1146
|
+
|
|
1147
|
+
'f.seedreamModel': 'Модель SeaDream',
|
|
1148
|
+
'f.seedreamModelHint': 'Идентификатор модели ByteDance SeaDream.',
|
|
1149
|
+
'p.seedreamModel': 'seedream-4.0',
|
|
1150
|
+
'f.seedreamKeyEnv': 'Ключ SeaDream',
|
|
1151
|
+
'f.seedreamKeyEnvHint': 'Имя ссылки на ключ API SeaDream.',
|
|
1152
|
+
'p.seedreamKeyEnv': 'SEEDREAM_API_KEY',
|
|
1153
|
+
'f.seedreamBaseURL': 'Базовый URL SeaDream',
|
|
1154
|
+
'f.seedreamBaseURLHint': 'Адрес API SeaDream (ByteDance/Volcengine Ark).',
|
|
1155
|
+
'p.seedreamBaseURL': 'https://api.bytedanceapi.com/v1',
|
|
1156
|
+
|
|
1157
|
+
'f.geminiModel': 'Модель Gemini',
|
|
1158
|
+
'f.geminiModelHint': 'Идентификатор модели генерации Google Gemini.',
|
|
1159
|
+
'p.geminiModel': 'gemini-2.0-flash-exp-image-generation',
|
|
1160
|
+
'f.geminiKeyEnv': 'Ключ Gemini',
|
|
1161
|
+
'f.geminiKeyEnvHint': 'Имя ссылки на ключ Google Gemini API.',
|
|
1162
|
+
'p.geminiKeyEnv': 'GEMINI_API_KEY',
|
|
1163
|
+
|
|
1164
|
+
'f.localKind': 'Тип локального движка',
|
|
1165
|
+
'f.localKindHint': 'Выбор между ComfyUI API и Automatic1111 WebUI.',
|
|
1166
|
+
'f.localBaseURL': 'Адрес локального сервера',
|
|
1167
|
+
'f.localBaseURLHint': 'Адрес локального сервера (ComfyUI: 8188, A1111: 7860).',
|
|
1168
|
+
'p.localBaseURL': 'http://127.0.0.1:8188',
|
|
1169
|
+
'f.localModel': 'Модель / Чекпоинт',
|
|
1170
|
+
'f.localModelHint': 'Имя чекпоинта или воркфлоу ComfyUI.',
|
|
1171
|
+
'p.localModel': 'v1-5-pruned-emaonly.safetensors',
|
|
1172
|
+
'f.localSteps': 'Шаги сэмплирования',
|
|
1173
|
+
'f.localStepsHint': 'Количество шагов генерации.',
|
|
1174
|
+
'p.localSteps': '20',
|
|
1175
|
+
'f.localCfg': 'Шкала CFG',
|
|
1176
|
+
'f.localCfgHint': 'Сила следования промпту.',
|
|
1177
|
+
'p.localCfg': '7',
|
|
1178
|
+
'f.localComfyUrl': 'Локальный ComfyUI URL (Legacy)',
|
|
1179
|
+
'f.localComfyUrlHint': 'Прямой адрес ComfyUI.',
|
|
1180
|
+
'p.localComfyUrl': 'http://127.0.0.1:8188',
|
|
1181
|
+
'f.localA1111Url': 'Локальный A1111 URL (Legacy)',
|
|
1182
|
+
'f.localA1111UrlHint': 'Прямой адрес Automatic1111.',
|
|
1183
|
+
'p.localA1111Url': 'http://127.0.0.1:7860',
|
|
1184
|
+
|
|
1185
|
+
'f.subscriptionQuality': 'Качество подписки',
|
|
1186
|
+
'f.subscriptionQualityHint': 'Профиль качества для провайдеров Codex и Grok.',
|
|
1187
|
+
|
|
1188
|
+
'f.enhancePrompt': 'LLM-улучшение промпта',
|
|
1189
|
+
'f.enhancePromptHint': 'Автоматическое обогащение коротких запросов художественными деталями.',
|
|
1190
|
+
'f.enableLlmEnhancer': 'Включить LLM Enhancer (Legacy)',
|
|
1191
|
+
'f.enableLlmEnhancerHint': 'Устаревший переключатель улучшения промптов.',
|
|
1192
|
+
'f.enhanceModel': 'Модель улучшения',
|
|
1193
|
+
'f.enhanceModelHint': 'Модель для расширения промптов (пусто = основная модель чата).',
|
|
1194
|
+
'p.enhanceModel': 'Пусто для основной модели чата',
|
|
1195
|
+
'f.enhanceBelowChars': 'Порог длины промпта',
|
|
1196
|
+
'f.enhanceBelowCharsHint': 'Промпты длиннее этого количества символов не расширяются.',
|
|
1197
|
+
'p.enhanceBelowChars': '200',
|
|
1198
|
+
'f.stylePreset': 'Художественный стиль',
|
|
1199
|
+
'f.stylePresetHint': 'Автоматический суффикс стиля для всех создаваемых изображений.',
|
|
1200
|
+
|
|
1201
|
+
'f.qualityGate': 'Контроль качества (Quality Gate)',
|
|
1202
|
+
'f.qualityGateHint': 'Автоматическая проверка кадров и скрытый реролл при дефектах или пустом кадре.',
|
|
1203
|
+
'f.dailyBudgetUsd': 'Дневной бюджет ($ USD)',
|
|
1204
|
+
'f.dailyBudgetUsdHint': 'Предельный лимит затрат в сутки по всем графическим инструментам (0 = без лимита).',
|
|
1205
|
+
'p.dailyBudgetUsd': '0.00',
|
|
1206
|
+
'f.loopGuardLimit': 'Защита от циклов (Loop Guard)',
|
|
1207
|
+
'f.loopGuardLimitHint': 'Максимум последовательных генераций подряд без участия пользователя (0 = выкл).',
|
|
1208
|
+
'p.loopGuardLimit': '3',
|
|
1209
|
+
|
|
1210
|
+
'f.diskCache': 'Дисковый кэш по хэшу',
|
|
1211
|
+
'f.diskCacheHint': 'Мгновенная выдача (<50 мс) и нулевая стоимость для повторных идентичных генераций.',
|
|
1212
|
+
'f.cacheBySeed': 'Кэш по сиду и промпту',
|
|
1213
|
+
'f.cacheBySeedHint': 'Возвращать кэш при полном совпадении сида и текста промпта.',
|
|
1214
|
+
'f.cacheByPrompt': 'Кэш по тексту промпта',
|
|
1215
|
+
'f.cacheByPromptHint': 'Возвращать кэш при повторении того же текста промпта.',
|
|
1216
|
+
'f.pruneDays': 'Срок хранения файлов (дни)',
|
|
1217
|
+
'f.pruneDaysHint': 'Автоматическое удаление файлов и записей старше указанного числа дней (0 = бессрочно).',
|
|
1218
|
+
'p.pruneDays': '0',
|
|
1219
|
+
'f.cacheTtlDays': 'Срок хранения кэша (Legacy)',
|
|
1220
|
+
'f.cacheTtlDaysHint': 'Устаревшее имя параметра срока хранения.',
|
|
1221
|
+
'p.cacheTtlDays': '0',
|
|
1192
1222
|
}
|
|
1193
1223
|
|
|
1224
|
+
// -------------------------------------------------------------- Registration & Apply
|
|
1225
|
+
const inject = ['slots', 'settingsScope', 'locale', 'sessions']
|
|
1226
|
+
|
|
1194
1227
|
function apply(ctx) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
// уносил с собой весь плагин — в интерфейсе это выглядело как «Failed to
|
|
1199
|
-
// load plugins» с перечнем ни в чём не повинных соседей.
|
|
1200
|
-
//
|
|
1201
|
-
// Поэтому каждый язык объявляется отдельно и по-хорошему: заняли до нас —
|
|
1202
|
-
// уступаем, свой английский при этом всё равно встаёт на место.
|
|
1203
|
-
const addLocale = (locale, dictionary) => {
|
|
1204
|
-
try {
|
|
1205
|
-
return ctx.locale.register(NS, locale, dictionary)
|
|
1206
|
-
} catch (alreadyTaken) {
|
|
1207
|
-
return () => {}
|
|
1208
|
-
}
|
|
1228
|
+
if (ctx.locale && typeof ctx.locale.define === 'function') {
|
|
1229
|
+
ctx.locale.define('en', NS, en)
|
|
1230
|
+
ctx.locale.define('ru', NS, ru)
|
|
1209
1231
|
}
|
|
1210
|
-
|
|
1211
|
-
const undo = [addLocale('en', en), addLocale('ru', ru)]
|
|
1212
|
-
return () => { for (const off of undo) off() }
|
|
1213
|
-
}, 'dsh-image-gen: dictionaries')
|
|
1214
|
-
// Подпись раздела рисует не наш компонент, а боковой список, поэтому
|
|
1215
|
-
// props.t сюда не доедет: берём переводчик, привязанный к namespace.
|
|
1216
|
-
const t = ctx.locale.bind(NS)
|
|
1217
|
-
ctx.effect(
|
|
1218
|
-
() => refreshMirrorUntilVisible(ctx),
|
|
1219
|
-
'dsh-image-gen: re-read the settings mirror until our namespace appears',
|
|
1220
|
-
)
|
|
1221
|
-
// На странице, открытой не с localhost, ядро отключает настройки целиком:
|
|
1222
|
-
// общий вид документа не читается, каждый раздел получает "unavailable",
|
|
1223
|
-
// запись выбрасывается. Сервер это ограничение не разделяет, и плагин
|
|
1224
|
-
// dsh-lanmode поднимает ту же механику поверх тех же вызовов. Если он
|
|
1225
|
-
// установлен — берём его службу, иначе ядровую: на loopback-странице она
|
|
1226
|
-
// работает штатно.
|
|
1232
|
+
|
|
1227
1233
|
let card
|
|
1228
1234
|
const cardOnce = () => {
|
|
1229
1235
|
if (card === undefined) {
|
|
1230
|
-
|
|
1236
|
+
const scope = ((ctx.get && ctx.get('lanSettings')) || ctx.settingsScope).bind({ namespace: SETTINGS_NS })
|
|
1237
|
+
card = new FalSettingsCardController(scope)
|
|
1231
1238
|
}
|
|
1232
1239
|
return card
|
|
1233
1240
|
}
|
|
1234
1241
|
|
|
1235
|
-
|
|
1236
|
-
ctx.slots
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1242
|
+
function registerSlotWhenReady(slotName, registerFn) {
|
|
1243
|
+
if (!ctx.slots) return
|
|
1244
|
+
if (typeof ctx.slots.inject === 'function') {
|
|
1245
|
+
try {
|
|
1246
|
+
ctx.slots.inject(slotName, () => {
|
|
1247
|
+
try {
|
|
1248
|
+
return registerFn()
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
console.warn('[dsh-image-gen] Error registering slot ' + slotName + ':', err)
|
|
1251
|
+
}
|
|
1252
|
+
})
|
|
1253
|
+
return
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
console.warn('[dsh-image-gen] Failed to inject slot ' + slotName + ':', err)
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (typeof ctx.slots.register === 'function') {
|
|
1259
|
+
try {
|
|
1260
|
+
registerFn()
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
console.warn('[dsh-image-gen] Failed direct registration for ' + slotName + ':', err)
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1247
1266
|
|
|
1248
|
-
|
|
1267
|
+
// Register toolviews for all 8 visual tools
|
|
1268
|
+
const toolviewTools = [
|
|
1269
|
+
'generate_image',
|
|
1270
|
+
'edit_image',
|
|
1271
|
+
'vary_image',
|
|
1272
|
+
'blend_images',
|
|
1273
|
+
'generate_image_pack',
|
|
1274
|
+
'remove_background',
|
|
1275
|
+
'upscale_image',
|
|
1276
|
+
'vectorize_image',
|
|
1277
|
+
]
|
|
1278
|
+
|
|
1279
|
+
for (const toolName of toolviewTools) {
|
|
1280
|
+
registerSlotWhenReady('tool.call.toolview', () =>
|
|
1281
|
+
ctx.slots.register(
|
|
1282
|
+
{
|
|
1283
|
+
name: 'tool.call.toolview',
|
|
1284
|
+
key: toolName,
|
|
1285
|
+
locale: NS,
|
|
1286
|
+
inject: () => ({ sessions: ctx.sessions }),
|
|
1287
|
+
},
|
|
1288
|
+
(props) => react.createElement(ErrorBoundary, null, react.createElement(FalImageCard, props))
|
|
1289
|
+
)
|
|
1290
|
+
)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Settings card item
|
|
1294
|
+
registerSlotWhenReady('settings.plugin.item', () =>
|
|
1249
1295
|
ctx.slots.register(
|
|
1250
|
-
{
|
|
1251
|
-
|
|
1252
|
-
|
|
1296
|
+
{
|
|
1297
|
+
name: 'settings.plugin.item',
|
|
1298
|
+
key: SETTINGS_NS,
|
|
1299
|
+
locale: NS,
|
|
1300
|
+
inject: () => cardOnce().inject(),
|
|
1301
|
+
},
|
|
1302
|
+
(props) => react.createElement(ErrorBoundary, null, react.createElement(FalSettingsCard, props))
|
|
1303
|
+
)
|
|
1253
1304
|
)
|
|
1254
|
-
// Карточка во вкладке «Настройки плагинов» вместо строки в боковом
|
|
1255
|
-
// списке: список плоский и перестал помещаться, а карточки вкладки
|
|
1256
|
-
// сворачиваются. Ключ слота обязан равняться пространству настроек:
|
|
1257
|
-
// вкладка перебирает именно их, с чужим ключом карточка исчезла бы молча.
|
|
1258
|
-
// Заголовок, пояснение и сворачивание рисует сама карточка — ядро даёт
|
|
1259
|
-
// только рамку списка.
|
|
1260
|
-
//
|
|
1261
|
-
// Запасной путь: слот объявлен пакетом настроек ядра. Если в сборке его
|
|
1262
|
-
// нет, регистрация бросает — тогда остаётся прежний раздел в боковом
|
|
1263
|
-
// списке, чтобы настройки не пропали.
|
|
1264
|
-
function registerFirst(ctx, candidates, component) {
|
|
1265
|
-
if (!ctx || !ctx.slots) return undefined
|
|
1266
|
-
for (const entry of candidates) {
|
|
1267
|
-
try {
|
|
1268
|
-
if (typeof ctx.slots?.has === 'function' && !ctx.slots.has(entry.name)) {
|
|
1269
|
-
continue
|
|
1270
|
-
}
|
|
1271
|
-
let ok = false
|
|
1272
|
-
ctx.slots.inject(entry.name, () => {
|
|
1273
|
-
ctx.slots.register(entry, component)
|
|
1274
|
-
ok = true
|
|
1275
|
-
})
|
|
1276
|
-
if (ok) return entry.name
|
|
1277
|
-
} catch {}
|
|
1278
|
-
}
|
|
1279
|
-
return undefined
|
|
1280
1305
|
}
|
|
1281
1306
|
|
|
1282
|
-
registerFirst(ctx, [
|
|
1283
|
-
{
|
|
1284
|
-
name: 'settings.plugin.item',
|
|
1285
|
-
key: SETTINGS_NS,
|
|
1286
|
-
locale: NS,
|
|
1287
|
-
inject: () => cardOnce().inject(),
|
|
1288
|
-
},
|
|
1289
|
-
{
|
|
1290
|
-
name: 'settings.section',
|
|
1291
|
-
id: '@goodandready/dsh-image-gen',
|
|
1292
|
-
order: 34,
|
|
1293
|
-
locale: NS,
|
|
1294
|
-
label: () => t('settings.navLabel'),
|
|
1295
|
-
inject: () => cardOnce().inject(),
|
|
1296
|
-
},
|
|
1297
|
-
], FalSettingsCard)
|
|
1298
|
-
}
|
|
1299
1307
|
exports.apply = apply
|
|
1300
1308
|
exports.inject = inject
|
|
1301
1309
|
return module.exports
|