@sequent-org/moodboard 1.4.47 → 1.4.49
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/package.json +1 -1
- package/src/core/events/Events.js +2 -0
- package/src/core/flows/ClipboardFlow.js +13 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +1 -1
- package/src/objects/ImageObject.js +5 -0
- package/src/services/ai/AiClient.js +51 -0
- package/src/services/ai/VideoSessionController.js +175 -0
- package/src/services/ai/imageModelCapabilities.js +130 -0
- package/src/services/ai/videoModelCapabilities.js +120 -0
- package/src/ui/ImagePropertiesPanel.js +1473 -4
- package/src/ui/chat/ChatSettingsPopup.js +210 -44
- package/src/ui/chat/ChatVideoToolbarPills.js +284 -0
- package/src/ui/chat/ChatWindow.js +469 -89
- package/src/ui/chat/ChatWindowRenderer.js +29 -1
- package/src/ui/chat/icons.js +26 -6
- package/src/ui/comments/CommentThreadPopover.js +68 -0
- package/src/ui/connectors/ConnectionAnchorsLayer.js +8 -1
- package/src/ui/styles/chat.css +62 -0
- package/src/ui/styles/panels.css +388 -1
- package/src/utils/applyRoundedMask.js +39 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Попап
|
|
2
|
+
* Попап доп-настроек генерации изображения + «Очистить историю».
|
|
3
|
+
*
|
|
4
|
+
* Секция изображения (seed, негативный промпт, фон, формат файла,
|
|
5
|
+
* авто-расширение, водяной знак) — поля отображаются только когда текущая
|
|
6
|
+
* модель изображения поддерживает соответствующую настройку.
|
|
7
|
+
*
|
|
8
|
+
* Видео-настройки в этой панели НЕ живут — они вынесены в тулбар
|
|
9
|
+
* (см. ChatVideoToolbarPills). Поэтому кнопка-триггер скрывается целиком,
|
|
10
|
+
* когда у текущей модели изображения нет ни одной поддерживаемой настройки.
|
|
3
11
|
*
|
|
4
12
|
* Одна ответственность: построить контент попапа, прокинуть значения
|
|
5
13
|
* наружу при изменении, открывать/закрывать по триггеру.
|
|
@@ -9,7 +17,13 @@ export class ChatSettingsPopup {
|
|
|
9
17
|
/**
|
|
10
18
|
* @param {{ trigger: HTMLElement, popup: HTMLElement }} refs
|
|
11
19
|
* @param {{
|
|
12
|
-
* getSettings: () => {
|
|
20
|
+
* getSettings: () => {
|
|
21
|
+
* systemPrompt: string, temperature: number, maxTokens: number,
|
|
22
|
+
* seed: number|null, negativePrompt: string,
|
|
23
|
+
* background: string|null, outputFormat: string|null,
|
|
24
|
+
* promptExtend: boolean, watermark: boolean
|
|
25
|
+
* },
|
|
26
|
+
* getCapability?: () => import('../../services/ai/imageModelCapabilities.js').ImageModelCapability|null,
|
|
13
27
|
* onChange: (patch: object) => void,
|
|
14
28
|
* onClearHistory: () => void
|
|
15
29
|
* }} handlers
|
|
@@ -22,6 +36,8 @@ export class ChatSettingsPopup {
|
|
|
22
36
|
this._docClickHandler = null;
|
|
23
37
|
this._isOpen = false;
|
|
24
38
|
this._fields = null;
|
|
39
|
+
// Обёртка секции доп-настроек — скрывается целиком, когда модель ничего не поддерживает
|
|
40
|
+
this._imageGenSection = null;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
attach() {
|
|
@@ -35,15 +51,19 @@ export class ChatSettingsPopup {
|
|
|
35
51
|
refresh() {
|
|
36
52
|
if (!this._fields) return;
|
|
37
53
|
const s = this._handlers.getSettings?.() || {};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
|
|
55
|
+
const imageVisible = this._refreshImageGenSection(s);
|
|
56
|
+
|
|
57
|
+
// Кнопка-триггер видна только когда для текущей модели есть хотя бы
|
|
58
|
+
// одна поддерживаемая настройка генерации изображения. Видео-настройки
|
|
59
|
+
// живут в тулбаре (ChatVideoToolbarPills), не в этой панели.
|
|
60
|
+
this._setTriggerVisible(imageVisible);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_setTriggerVisible(visible) {
|
|
64
|
+
const host = this._trigger?.parentNode || this._trigger;
|
|
65
|
+
if (host) host.style.display = visible ? 'inline-flex' : 'none';
|
|
66
|
+
if (!visible && this._isOpen) this.close();
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
toggle() {
|
|
@@ -78,58 +98,144 @@ export class ChatSettingsPopup {
|
|
|
78
98
|
for (const off of this._listeners) off();
|
|
79
99
|
this._listeners = [];
|
|
80
100
|
this._fields = null;
|
|
101
|
+
this._imageGenSection = null;
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
_buildContent() {
|
|
84
105
|
const fields = {};
|
|
85
106
|
const fragment = document.createDocumentFragment();
|
|
86
107
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
}));
|
|
108
|
+
// Секция доп-настроек генерации изображения
|
|
109
|
+
const imageGenSection = document.createElement('div');
|
|
110
|
+
imageGenSection.className = 'moodboard-chat__settings-image-section';
|
|
111
|
+
imageGenSection.style.display = 'none';
|
|
112
|
+
|
|
113
|
+
const sectionDivider = document.createElement('div');
|
|
114
|
+
sectionDivider.className = 'moodboard-chat__settings-divider';
|
|
115
|
+
sectionDivider.style.cssText = 'border-top:1px solid #E5E7EB;margin:2px 0;';
|
|
116
|
+
imageGenSection.appendChild(sectionDivider);
|
|
98
117
|
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
const sectionTitle = document.createElement('div');
|
|
119
|
+
sectionTitle.className = 'moodboard-chat__settings-label';
|
|
120
|
+
sectionTitle.textContent = 'Параметры генерации';
|
|
121
|
+
sectionTitle.style.marginTop = '4px';
|
|
122
|
+
imageGenSection.appendChild(sectionTitle);
|
|
123
|
+
|
|
124
|
+
// Seed
|
|
125
|
+
const seedField = this._buildField({
|
|
126
|
+
label: 'Seed (число)',
|
|
101
127
|
input: () => {
|
|
102
128
|
const inp = document.createElement('input');
|
|
103
129
|
inp.type = 'number';
|
|
104
|
-
inp.step = '0.1';
|
|
105
130
|
inp.min = '0';
|
|
106
|
-
inp.
|
|
131
|
+
inp.step = '1';
|
|
132
|
+
inp.placeholder = 'Случайный';
|
|
107
133
|
inp.className = 'moodboard-chat__settings-input';
|
|
108
|
-
fields.
|
|
134
|
+
fields.seed = inp;
|
|
109
135
|
this._on(inp, 'change', () => {
|
|
110
|
-
const
|
|
111
|
-
|
|
136
|
+
const raw = inp.value.trim();
|
|
137
|
+
const v = raw === '' ? null : Number.parseInt(raw, 10);
|
|
138
|
+
this._handlers.onChange?.({ seed: (v !== null && !Number.isNaN(v)) ? v : null });
|
|
112
139
|
});
|
|
113
140
|
return inp;
|
|
114
141
|
}
|
|
115
|
-
})
|
|
142
|
+
});
|
|
143
|
+
seedField.dataset.imageGenField = 'seed';
|
|
144
|
+
imageGenSection.appendChild(seedField);
|
|
116
145
|
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
// Негативный промпт
|
|
147
|
+
const negField = this._buildField({
|
|
148
|
+
label: 'Негативный промпт',
|
|
119
149
|
input: () => {
|
|
120
150
|
const inp = document.createElement('input');
|
|
121
|
-
inp.type = '
|
|
122
|
-
inp.
|
|
123
|
-
inp.min = '1';
|
|
151
|
+
inp.type = 'text';
|
|
152
|
+
inp.placeholder = 'Что исключить из изображения';
|
|
124
153
|
inp.className = 'moodboard-chat__settings-input';
|
|
125
|
-
fields.
|
|
154
|
+
fields.negativePrompt = inp;
|
|
126
155
|
this._on(inp, 'change', () => {
|
|
127
|
-
|
|
128
|
-
if (!Number.isNaN(v) && v > 0) this._handlers.onChange?.({ maxTokens: v });
|
|
156
|
+
this._handlers.onChange?.({ negativePrompt: inp.value });
|
|
129
157
|
});
|
|
130
158
|
return inp;
|
|
131
159
|
}
|
|
132
|
-
})
|
|
160
|
+
});
|
|
161
|
+
negField.dataset.imageGenField = 'negativePrompt';
|
|
162
|
+
imageGenSection.appendChild(negField);
|
|
163
|
+
|
|
164
|
+
// Фон (background)
|
|
165
|
+
const bgField = this._buildField({
|
|
166
|
+
label: 'Фон',
|
|
167
|
+
input: () => {
|
|
168
|
+
const sel = document.createElement('select');
|
|
169
|
+
sel.className = 'moodboard-chat__settings-input';
|
|
170
|
+
[
|
|
171
|
+
{ value: '', label: 'По умолчанию' },
|
|
172
|
+
{ value: 'opaque', label: 'Непрозрачный' },
|
|
173
|
+
{ value: 'auto', label: 'Авто' }
|
|
174
|
+
].forEach(({ value, label }) => {
|
|
175
|
+
const opt = document.createElement('option');
|
|
176
|
+
opt.value = value;
|
|
177
|
+
opt.textContent = label;
|
|
178
|
+
sel.appendChild(opt);
|
|
179
|
+
});
|
|
180
|
+
fields.background = sel;
|
|
181
|
+
this._on(sel, 'change', () => {
|
|
182
|
+
this._handlers.onChange?.({ background: sel.value || null });
|
|
183
|
+
});
|
|
184
|
+
return sel;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
bgField.dataset.imageGenField = 'background';
|
|
188
|
+
imageGenSection.appendChild(bgField);
|
|
189
|
+
|
|
190
|
+
// Формат файла (outputFormat)
|
|
191
|
+
const fmtField = this._buildField({
|
|
192
|
+
label: 'Формат файла',
|
|
193
|
+
input: () => {
|
|
194
|
+
const sel = document.createElement('select');
|
|
195
|
+
sel.className = 'moodboard-chat__settings-input';
|
|
196
|
+
[
|
|
197
|
+
{ value: '', label: 'По умолчанию' },
|
|
198
|
+
{ value: 'png', label: 'PNG' },
|
|
199
|
+
{ value: 'jpeg', label: 'JPEG' },
|
|
200
|
+
{ value: 'webp', label: 'WebP' }
|
|
201
|
+
].forEach(({ value, label }) => {
|
|
202
|
+
const opt = document.createElement('option');
|
|
203
|
+
opt.value = value;
|
|
204
|
+
opt.textContent = label;
|
|
205
|
+
sel.appendChild(opt);
|
|
206
|
+
});
|
|
207
|
+
fields.outputFormat = sel;
|
|
208
|
+
this._on(sel, 'change', () => {
|
|
209
|
+
this._handlers.onChange?.({ outputFormat: sel.value || null });
|
|
210
|
+
});
|
|
211
|
+
return sel;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
fmtField.dataset.imageGenField = 'outputFormat';
|
|
215
|
+
imageGenSection.appendChild(fmtField);
|
|
216
|
+
|
|
217
|
+
// Авто-расширение промпта
|
|
218
|
+
const extField = this._buildCheckboxField({
|
|
219
|
+
label: 'Авто-расширение промпта',
|
|
220
|
+
key: 'promptExtend',
|
|
221
|
+
fields,
|
|
222
|
+
onChange: (v) => this._handlers.onChange?.({ promptExtend: v })
|
|
223
|
+
});
|
|
224
|
+
extField.dataset.imageGenField = 'promptExtend';
|
|
225
|
+
imageGenSection.appendChild(extField);
|
|
226
|
+
|
|
227
|
+
// Водяной знак
|
|
228
|
+
const wmField = this._buildCheckboxField({
|
|
229
|
+
label: 'Водяной знак',
|
|
230
|
+
key: 'watermark',
|
|
231
|
+
fields,
|
|
232
|
+
onChange: (v) => this._handlers.onChange?.({ watermark: v })
|
|
233
|
+
});
|
|
234
|
+
wmField.dataset.imageGenField = 'watermark';
|
|
235
|
+
imageGenSection.appendChild(wmField);
|
|
236
|
+
|
|
237
|
+
fragment.appendChild(imageGenSection);
|
|
238
|
+
this._imageGenSection = imageGenSection;
|
|
133
239
|
|
|
134
240
|
const row = document.createElement('div');
|
|
135
241
|
row.className = 'moodboard-chat__settings-popup-row';
|
|
@@ -148,6 +254,47 @@ export class ChatSettingsPopup {
|
|
|
148
254
|
this._fields = fields;
|
|
149
255
|
}
|
|
150
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Показывает/скрывает поля секции генерации и синхронизирует значения
|
|
259
|
+
* в зависимости от текущих возможностей модели.
|
|
260
|
+
*/
|
|
261
|
+
_refreshImageGenSection(s) {
|
|
262
|
+
if (!this._imageGenSection || !this._fields) return false;
|
|
263
|
+
const isImage = this._handlers.getContentType?.() === 'image';
|
|
264
|
+
const cap = isImage ? (this._handlers.getCapability?.() ?? null) : null;
|
|
265
|
+
const sup = cap?.supports ?? {};
|
|
266
|
+
|
|
267
|
+
const anyVisible = !!(sup.seed || sup.negativePrompt || sup.background || sup.outputFormat || sup.promptExtend || sup.watermark);
|
|
268
|
+
this._imageGenSection.style.display = anyVisible ? '' : 'none';
|
|
269
|
+
|
|
270
|
+
// Показываем/скрываем отдельные поля по флагам
|
|
271
|
+
for (const el of this._imageGenSection.querySelectorAll('[data-image-gen-field]')) {
|
|
272
|
+
const flag = el.dataset.imageGenField;
|
|
273
|
+
el.style.display = sup[flag] ? '' : 'none';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Синхронизируем значения только если поле не активно
|
|
277
|
+
if (sup.seed && document.activeElement !== this._fields.seed) {
|
|
278
|
+
this._fields.seed.value = s.seed != null ? String(s.seed) : '';
|
|
279
|
+
}
|
|
280
|
+
if (sup.negativePrompt && document.activeElement !== this._fields.negativePrompt) {
|
|
281
|
+
this._fields.negativePrompt.value = s.negativePrompt || '';
|
|
282
|
+
}
|
|
283
|
+
if (sup.background) {
|
|
284
|
+
this._fields.background.value = s.background || '';
|
|
285
|
+
}
|
|
286
|
+
if (sup.outputFormat) {
|
|
287
|
+
this._fields.outputFormat.value = s.outputFormat || '';
|
|
288
|
+
}
|
|
289
|
+
if (sup.promptExtend && this._fields.promptExtend) {
|
|
290
|
+
this._fields.promptExtend.checked = Boolean(s.promptExtend);
|
|
291
|
+
}
|
|
292
|
+
if (sup.watermark && this._fields.watermark) {
|
|
293
|
+
this._fields.watermark.checked = Boolean(s.watermark);
|
|
294
|
+
}
|
|
295
|
+
return anyVisible;
|
|
296
|
+
}
|
|
297
|
+
|
|
151
298
|
_buildField({ label, input }) {
|
|
152
299
|
const wrap = document.createElement('div');
|
|
153
300
|
wrap.className = 'moodboard-chat__settings-field';
|
|
@@ -159,13 +306,32 @@ export class ChatSettingsPopup {
|
|
|
159
306
|
return wrap;
|
|
160
307
|
}
|
|
161
308
|
|
|
309
|
+
_buildCheckboxField({ label, key, fields, onChange }) {
|
|
310
|
+
const wrap = document.createElement('div');
|
|
311
|
+
wrap.className = 'moodboard-chat__settings-field';
|
|
312
|
+
wrap.style.flexDirection = 'row';
|
|
313
|
+
wrap.style.alignItems = 'center';
|
|
314
|
+
wrap.style.gap = '8px';
|
|
315
|
+
|
|
316
|
+
const cb = document.createElement('input');
|
|
317
|
+
cb.type = 'checkbox';
|
|
318
|
+
cb.className = 'moodboard-chat__settings-checkbox';
|
|
319
|
+
fields[key] = cb;
|
|
320
|
+
this._on(cb, 'change', () => onChange(cb.checked));
|
|
321
|
+
|
|
322
|
+
const labelEl = document.createElement('label');
|
|
323
|
+
labelEl.className = 'moodboard-chat__settings-label';
|
|
324
|
+
labelEl.style.textTransform = 'none';
|
|
325
|
+
labelEl.style.cursor = 'pointer';
|
|
326
|
+
labelEl.textContent = label;
|
|
327
|
+
|
|
328
|
+
wrap.appendChild(cb);
|
|
329
|
+
wrap.appendChild(labelEl);
|
|
330
|
+
return wrap;
|
|
331
|
+
}
|
|
332
|
+
|
|
162
333
|
_on(el, type, handler) {
|
|
163
334
|
el.addEventListener(type, handler);
|
|
164
335
|
this._listeners.push(() => el.removeEventListener(type, handler));
|
|
165
336
|
}
|
|
166
337
|
}
|
|
167
|
-
|
|
168
|
-
function formatNumber(n) {
|
|
169
|
-
if (typeof n !== 'number' || Number.isNaN(n)) return '';
|
|
170
|
-
return String(Math.round(n * 100) / 100);
|
|
171
|
-
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { ICONS } from './icons.js';
|
|
2
|
+
import { ChatPillMenu } from './ChatPillMenu.js';
|
|
3
|
+
import { getVideoModelCapability } from '../../services/ai/videoModelCapabilities.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Видео-настройки как пилюли тулбара (вместо отдельной всплывающей панели).
|
|
7
|
+
*
|
|
8
|
+
* Каждая пилюля видна только когда выбран тип «видео» и текущая модель
|
|
9
|
+
* поддерживает соответствующий параметр (по capability.supports).
|
|
10
|
+
*
|
|
11
|
+
* Типы контролов:
|
|
12
|
+
* - тумблеры (Звук, Водяной знак) — клик переключает значение;
|
|
13
|
+
* - выбор (Люди) — пилюля-меню как формат/модель;
|
|
14
|
+
* - свободный ввод (Seed, CFG, Негатив) — пилюля с мини-поповером,
|
|
15
|
+
* привязанным к ней (тот же механизм, что у меню формата).
|
|
16
|
+
*
|
|
17
|
+
* Одна ответственность: построить/обновлять пилюли видео-настроек и
|
|
18
|
+
* прокидывать изменения наружу через onChange.
|
|
19
|
+
*/
|
|
20
|
+
export class ChatVideoToolbarPills {
|
|
21
|
+
/**
|
|
22
|
+
* @param {HTMLElement} pillsContainer
|
|
23
|
+
* @param {{
|
|
24
|
+
* getActive: () => boolean,
|
|
25
|
+
* getModelId: () => string,
|
|
26
|
+
* getState: () => { seed:number|null, negativePrompt:string, audio:boolean, watermark:boolean, cfgScale:number|null, personGeneration:string },
|
|
27
|
+
* onChange: (patch: object) => void
|
|
28
|
+
* }} handlers
|
|
29
|
+
*/
|
|
30
|
+
constructor(pillsContainer, handlers) {
|
|
31
|
+
this._container = pillsContainer;
|
|
32
|
+
this._h = handlers;
|
|
33
|
+
this._listeners = [];
|
|
34
|
+
this._entries = []; // { flag, wrapper }
|
|
35
|
+
this._pills = {}; // flag -> pill button
|
|
36
|
+
this._toggleIcons = {}; // flag -> { span, on, off } для смены иконки тумблера
|
|
37
|
+
this._labels = {}; // flag -> label span (для value-пилюль)
|
|
38
|
+
this._inputs = {}; // flag -> input element
|
|
39
|
+
this._personMenu = null;
|
|
40
|
+
this._popover = null; // { menu, pill }
|
|
41
|
+
this._docHandler = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
attach() {
|
|
45
|
+
this._buildToggle('audio', 'Звук', ICONS.audio, {
|
|
46
|
+
iconOff: ICONS.audioOff,
|
|
47
|
+
title: 'Звук в видео: вкл / выкл',
|
|
48
|
+
});
|
|
49
|
+
this._buildToggle('watermark', 'Водяной знак', ICONS.watermark, {
|
|
50
|
+
title: 'Водяной знак на видео: вкл / выкл',
|
|
51
|
+
});
|
|
52
|
+
this._buildPersonPill();
|
|
53
|
+
this._buildSeedPill();
|
|
54
|
+
this._buildCfgPill();
|
|
55
|
+
this._buildNegativePill();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
refresh() {
|
|
59
|
+
const active = !!this._h.getActive?.();
|
|
60
|
+
const cap = active ? getVideoModelCapability(this._h.getModelId?.()) : null;
|
|
61
|
+
const sup = cap?.supports ?? {};
|
|
62
|
+
const st = this._h.getState?.() ?? {};
|
|
63
|
+
|
|
64
|
+
for (const { flag, wrapper } of this._entries) {
|
|
65
|
+
wrapper.style.display = (active && sup[flag]) ? '' : 'none';
|
|
66
|
+
}
|
|
67
|
+
if (!active) {
|
|
68
|
+
this._closePopover();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this._applyToggleState('audio', !!st.audio);
|
|
73
|
+
this._applyToggleState('watermark', !!st.watermark);
|
|
74
|
+
|
|
75
|
+
this._personMenu?.refresh();
|
|
76
|
+
|
|
77
|
+
if (this._labels.seed) this._labels.seed.textContent = st.seed != null ? `Seed · ${st.seed}` : 'Seed';
|
|
78
|
+
if (this._inputs.seed && document.activeElement !== this._inputs.seed) {
|
|
79
|
+
this._inputs.seed.value = st.seed != null ? String(st.seed) : '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (this._labels.cfgScale) {
|
|
83
|
+
this._labels.cfgScale.textContent = st.cfgScale != null ? `CFG · ${Number(st.cfgScale).toFixed(2)}` : 'CFG';
|
|
84
|
+
}
|
|
85
|
+
if (this._inputs.cfgScale) {
|
|
86
|
+
this._inputs.cfgScale.value = st.cfgScale != null ? String(st.cfgScale) : '0.5';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const hasNeg = !!(st.negativePrompt && st.negativePrompt.trim());
|
|
90
|
+
if (this._pills.negativePrompt) this._pills.negativePrompt.dataset.active = hasNeg ? 'true' : 'false';
|
|
91
|
+
if (this._inputs.negativePrompt && document.activeElement !== this._inputs.negativePrompt) {
|
|
92
|
+
this._inputs.negativePrompt.value = st.negativePrompt || '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
destroy() {
|
|
97
|
+
this._closePopover();
|
|
98
|
+
for (const off of this._listeners) off();
|
|
99
|
+
this._listeners = [];
|
|
100
|
+
this._personMenu?.destroy();
|
|
101
|
+
this._personMenu = null;
|
|
102
|
+
for (const { wrapper } of this._entries) wrapper.remove();
|
|
103
|
+
this._entries = [];
|
|
104
|
+
this._pills = {};
|
|
105
|
+
this._toggleIcons = {};
|
|
106
|
+
this._labels = {};
|
|
107
|
+
this._inputs = {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_registerWrapper(flag, wrapper) {
|
|
111
|
+
wrapper.style.display = 'none';
|
|
112
|
+
this._container.appendChild(wrapper);
|
|
113
|
+
this._entries.push({ flag, wrapper });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_makePill(icon, labelText) {
|
|
117
|
+
const wrapper = document.createElement('div');
|
|
118
|
+
wrapper.className = 'moodboard-chat__pill-wrapper';
|
|
119
|
+
const pill = document.createElement('button');
|
|
120
|
+
pill.type = 'button';
|
|
121
|
+
pill.className = 'moodboard-chat__pill';
|
|
122
|
+
const iconSpan = document.createElement('span');
|
|
123
|
+
iconSpan.className = 'moodboard-chat__pill-icon-wrap';
|
|
124
|
+
iconSpan.innerHTML = icon;
|
|
125
|
+
const labelEl = document.createElement('span');
|
|
126
|
+
labelEl.className = 'moodboard-chat__pill-label';
|
|
127
|
+
labelEl.textContent = labelText;
|
|
128
|
+
pill.appendChild(iconSpan);
|
|
129
|
+
pill.appendChild(labelEl);
|
|
130
|
+
wrapper.appendChild(pill);
|
|
131
|
+
return { wrapper, pill, labelEl, iconSpan };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_buildToggle(flag, labelText, icon, opts = {}) {
|
|
135
|
+
const { wrapper, pill, iconSpan } = this._makePill(icon, labelText);
|
|
136
|
+
pill.classList.add('moodboard-chat__pill--toggle');
|
|
137
|
+
pill.setAttribute('aria-pressed', 'false');
|
|
138
|
+
if (opts.title) pill.title = opts.title;
|
|
139
|
+
this._pills[flag] = pill;
|
|
140
|
+
this._toggleIcons[flag] = { span: iconSpan, on: icon, off: opts.iconOff ?? icon };
|
|
141
|
+
this._on(pill, 'click', () => {
|
|
142
|
+
const st = this._h.getState?.() ?? {};
|
|
143
|
+
this._h.onChange?.({ [flag]: !st[flag] });
|
|
144
|
+
});
|
|
145
|
+
this._registerWrapper(flag, wrapper);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_applyToggleState(flag, on) {
|
|
149
|
+
const pill = this._pills[flag];
|
|
150
|
+
if (!pill) return;
|
|
151
|
+
pill.dataset.active = on ? 'true' : 'false';
|
|
152
|
+
pill.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
153
|
+
const ic = this._toggleIcons[flag];
|
|
154
|
+
if (ic) ic.span.innerHTML = on ? ic.on : ic.off;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_buildPersonPill() {
|
|
158
|
+
const { wrapper, pill, labelEl } = this._makePill(ICONS.users, 'Разрешить');
|
|
159
|
+
pill.setAttribute('aria-haspopup', 'menu');
|
|
160
|
+
pill.setAttribute('aria-expanded', 'false');
|
|
161
|
+
const menu = document.createElement('div');
|
|
162
|
+
menu.className = 'moodboard-chat__menu';
|
|
163
|
+
menu.setAttribute('role', 'menu');
|
|
164
|
+
wrapper.appendChild(menu);
|
|
165
|
+
this._pills.personGeneration = pill;
|
|
166
|
+
this._personMenu = new ChatPillMenu(
|
|
167
|
+
{ trigger: pill, menu, label: labelEl },
|
|
168
|
+
{
|
|
169
|
+
getOptions: () => [
|
|
170
|
+
{ id: 'allow_all', label: 'Разрешить' },
|
|
171
|
+
{ id: 'disallow', label: 'Запретить' },
|
|
172
|
+
],
|
|
173
|
+
getActiveId: () => this._h.getState?.().personGeneration ?? 'allow_all',
|
|
174
|
+
onSelect: (id) => this._h.onChange?.({ personGeneration: id }),
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
this._personMenu.attach();
|
|
178
|
+
this._registerWrapper('personGeneration', wrapper);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_buildSeedPill() {
|
|
182
|
+
this._buildPopoverPill('seed', 'Seed', ICONS.seed, 'Зерно генерации', (body) => {
|
|
183
|
+
const input = document.createElement('input');
|
|
184
|
+
input.type = 'number';
|
|
185
|
+
input.min = '0';
|
|
186
|
+
input.step = '1';
|
|
187
|
+
input.placeholder = 'Случайно';
|
|
188
|
+
input.className = 'moodboard-chat__settings-input';
|
|
189
|
+
this._inputs.seed = input;
|
|
190
|
+
this._on(input, 'input', () => {
|
|
191
|
+
const raw = input.value.trim();
|
|
192
|
+
const v = raw === '' ? null : Number.parseInt(raw, 10);
|
|
193
|
+
this._h.onChange?.({ seed: (v != null && !Number.isNaN(v)) ? v : null });
|
|
194
|
+
});
|
|
195
|
+
body.appendChild(input);
|
|
196
|
+
const hint = document.createElement('div');
|
|
197
|
+
hint.className = 'moodboard-chat__settings-hint';
|
|
198
|
+
hint.textContent = 'Число, фиксирующее случайность. Одно и то же число — похожий результат. Пусто — каждый раз по-новому.';
|
|
199
|
+
body.appendChild(hint);
|
|
200
|
+
}, { title: 'Seed — число для повторяемости результата' });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_buildCfgPill() {
|
|
204
|
+
this._buildPopoverPill('cfgScale', 'CFG', ICONS.gauge, 'CFG Scale (0–1)', (body) => {
|
|
205
|
+
const input = document.createElement('input');
|
|
206
|
+
input.type = 'range';
|
|
207
|
+
input.min = '0';
|
|
208
|
+
input.max = '1';
|
|
209
|
+
input.step = '0.05';
|
|
210
|
+
input.className = 'moodboard-chat__settings-input';
|
|
211
|
+
this._inputs.cfgScale = input;
|
|
212
|
+
this._on(input, 'input', () => this._h.onChange?.({ cfgScale: Number(input.value) }));
|
|
213
|
+
body.appendChild(input);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_buildNegativePill() {
|
|
218
|
+
this._buildPopoverPill('negativePrompt', 'Негатив', ICONS.negative, 'Негативный промпт', (body) => {
|
|
219
|
+
const input = document.createElement('input');
|
|
220
|
+
input.type = 'text';
|
|
221
|
+
input.placeholder = 'Что исключить';
|
|
222
|
+
input.className = 'moodboard-chat__settings-input';
|
|
223
|
+
this._inputs.negativePrompt = input;
|
|
224
|
+
this._on(input, 'input', () => this._h.onChange?.({ negativePrompt: input.value }));
|
|
225
|
+
body.appendChild(input);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_buildPopoverPill(flag, labelText, icon, fieldLabel, buildBody, opts = {}) {
|
|
230
|
+
const { wrapper, pill, labelEl } = this._makePill(icon, labelText);
|
|
231
|
+
pill.setAttribute('aria-haspopup', 'dialog');
|
|
232
|
+
pill.setAttribute('aria-expanded', 'false');
|
|
233
|
+
if (opts.title) pill.title = opts.title;
|
|
234
|
+
|
|
235
|
+
const menu = document.createElement('div');
|
|
236
|
+
menu.className = 'moodboard-chat__menu moodboard-chat__pill-popover';
|
|
237
|
+
|
|
238
|
+
const body = document.createElement('div');
|
|
239
|
+
body.className = 'moodboard-chat__pill-popover-body';
|
|
240
|
+
const label = document.createElement('div');
|
|
241
|
+
label.className = 'moodboard-chat__settings-label';
|
|
242
|
+
label.textContent = fieldLabel;
|
|
243
|
+
body.appendChild(label);
|
|
244
|
+
buildBody(body);
|
|
245
|
+
menu.appendChild(body);
|
|
246
|
+
wrapper.appendChild(menu);
|
|
247
|
+
|
|
248
|
+
this._pills[flag] = pill;
|
|
249
|
+
this._labels[flag] = labelEl;
|
|
250
|
+
this._on(pill, 'click', (e) => {
|
|
251
|
+
e.stopPropagation();
|
|
252
|
+
if (this._popover?.menu === menu) this._closePopover();
|
|
253
|
+
else this._openPopover(menu, pill);
|
|
254
|
+
});
|
|
255
|
+
this._registerWrapper(flag, wrapper);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_openPopover(menu, pill) {
|
|
259
|
+
this._closePopover();
|
|
260
|
+
menu.classList.add('is-open');
|
|
261
|
+
pill.setAttribute('aria-expanded', 'true');
|
|
262
|
+
this._popover = { menu, pill };
|
|
263
|
+
this._docHandler = (e) => {
|
|
264
|
+
if (!menu.contains(e.target) && !pill.contains(e.target)) this._closePopover();
|
|
265
|
+
};
|
|
266
|
+
document.addEventListener('mousedown', this._docHandler);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_closePopover() {
|
|
270
|
+
if (!this._popover) return;
|
|
271
|
+
this._popover.menu.classList.remove('is-open');
|
|
272
|
+
this._popover.pill.setAttribute('aria-expanded', 'false');
|
|
273
|
+
this._popover = null;
|
|
274
|
+
if (this._docHandler) {
|
|
275
|
+
document.removeEventListener('mousedown', this._docHandler);
|
|
276
|
+
this._docHandler = null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_on(el, type, handler) {
|
|
281
|
+
el.addEventListener(type, handler);
|
|
282
|
+
this._listeners.push(() => el.removeEventListener(type, handler));
|
|
283
|
+
}
|
|
284
|
+
}
|