@sequent-org/moodboard 1.4.45 → 1.4.48
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 +1 -0
- package/src/core/flows/ClipboardFlow.js +29 -1
- package/src/core/flows/Model3dFlow.js +15 -0
- package/src/core/index.js +4 -2
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +2 -0
- package/src/objects/Model3dScreenshotImageObject.js +8 -0
- package/src/objects/ObjectFactory.js +2 -0
- package/src/services/ai/AiClient.js +181 -2
- package/src/services/ai/Model3dSessionController.js +238 -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 +8 -0
- package/src/ui/chat/ChatComposer.js +50 -3
- package/src/ui/chat/ChatSettingsPopup.js +210 -44
- package/src/ui/chat/ChatVideoToolbarPills.js +284 -0
- package/src/ui/chat/ChatWindow.js +919 -92
- package/src/ui/chat/ChatWindowRenderer.js +29 -1
- package/src/ui/chat/Model3dBoardSkeleton.js +93 -0
- package/src/ui/chat/Model3dProgressOverlay.js +114 -0
- package/src/ui/chat/icons.js +27 -6
- package/src/ui/handles/HandlesDomRenderer.js +30 -0
- package/src/ui/styles/chat.css +235 -0
- package/src/ui/styles/panels.css +264 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { AiClient } from '../../services/ai/AiClient.js';
|
|
2
2
|
import { ChatHistoryStore } from '../../services/ai/ChatHistoryStore.js';
|
|
3
3
|
import { ChatSessionController } from '../../services/ai/ChatSessionController.js';
|
|
4
|
+
import { Model3dSessionController } from '../../services/ai/Model3dSessionController.js';
|
|
5
|
+
import { IMAGE_MODELS, getImageModelCapability } from '../../services/ai/imageModelCapabilities.js';
|
|
6
|
+
import { VIDEO_MODELS, getVideoModelCapability } from '../../services/ai/videoModelCapabilities.js';
|
|
7
|
+
import { VideoSessionController } from '../../services/ai/VideoSessionController.js';
|
|
4
8
|
import { Events } from '../../core/events/Events.js';
|
|
5
9
|
|
|
6
10
|
import { buildChatDom } from './ChatWindowRenderer.js';
|
|
@@ -8,7 +12,11 @@ import { formatChatErrorForDisplay } from './formatChatError.js';
|
|
|
8
12
|
import { ChatMessageList } from './ChatMessageList.js';
|
|
9
13
|
import { ChatComposer } from './ChatComposer.js';
|
|
10
14
|
import { ChatPillMenu } from './ChatPillMenu.js';
|
|
15
|
+
import { ChatSettingsPopup } from './ChatSettingsPopup.js';
|
|
16
|
+
import { ChatVideoToolbarPills } from './ChatVideoToolbarPills.js';
|
|
11
17
|
import { ChatExtendedPromptModal } from './ChatExtendedPromptModal.js';
|
|
18
|
+
import { Model3dProgressOverlay } from './Model3dProgressOverlay.js';
|
|
19
|
+
import { Model3dBoardSkeleton } from './Model3dBoardSkeleton.js';
|
|
12
20
|
import { ICONS, RATIO_ICONS, COUNT_ICONS } from './icons.js';
|
|
13
21
|
|
|
14
22
|
const CONTENT_TYPE_OPTIONS = [
|
|
@@ -16,16 +24,71 @@ const CONTENT_TYPE_OPTIONS = [
|
|
|
16
24
|
id: 'image',
|
|
17
25
|
label: 'Изображение',
|
|
18
26
|
icon: ICONS.image,
|
|
19
|
-
description: '
|
|
27
|
+
description: 'По описанию или ссылке на референс'
|
|
20
28
|
},
|
|
21
29
|
{
|
|
22
30
|
id: 'video',
|
|
23
31
|
label: 'Видео',
|
|
24
32
|
icon: ICONS.video,
|
|
25
|
-
description: '
|
|
33
|
+
description: 'По описанию или по первому и последнему кадру'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: '3d',
|
|
37
|
+
label: '3D-модель',
|
|
38
|
+
icon: ICONS.cube,
|
|
39
|
+
description: 'По выбранному изображению'
|
|
26
40
|
}
|
|
27
41
|
];
|
|
28
42
|
|
|
43
|
+
const FACE_COUNT_OPTIONS_3D = [
|
|
44
|
+
{ id: '1.5m', label: '1.5M' },
|
|
45
|
+
{ id: '1m', label: '1M' },
|
|
46
|
+
{ id: '500k', label: '500k' },
|
|
47
|
+
{ id: '50k', label: '50k' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const FACE_COUNT_MAP_3D = { '1.5m': 1500000, '1m': 1000000, '500k': 500000, '50k': 50000 };
|
|
51
|
+
|
|
52
|
+
const TYPE_3D_OPTIONS = [
|
|
53
|
+
{ id: 'geo_tex', label: 'Геометрия + Текстура' },
|
|
54
|
+
{ id: 'geo', label: 'Только геометрия' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const MODE_3D_OPTIONS = [
|
|
58
|
+
{ id: 'image', label: 'Изображение' },
|
|
59
|
+
{ id: 'multi', label: 'Несколько изображений' },
|
|
60
|
+
{ id: 'text', label: 'Текст' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const TEXTURE_STYLE_OPTIONS_3D = [
|
|
64
|
+
{ id: 'general', label: 'Общий' },
|
|
65
|
+
{ id: 'stone_carving', label: 'Резьба по камню' },
|
|
66
|
+
{ id: 'blue_white_porcelain', label: 'Сине-белый фарфор' },
|
|
67
|
+
{ id: 'chinese_style', label: 'Китайский стиль' },
|
|
68
|
+
{ id: 'cartoon', label: 'Мультфильм' },
|
|
69
|
+
{ id: 'cyberpunk', label: 'Киберпанк' },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const TEXTURE_STYLE_SUFFIX_3D = {
|
|
73
|
+
general: '',
|
|
74
|
+
stone_carving: ', stone carving style',
|
|
75
|
+
blue_white_porcelain: ', blue and white porcelain style',
|
|
76
|
+
chinese_style: ', Chinese style',
|
|
77
|
+
cartoon: ', cartoon style',
|
|
78
|
+
cyberpunk: ', cyberpunk style',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const RESULT_FORMAT_OPTIONS_3D = [
|
|
82
|
+
{ id: 'glb', label: 'GLB' },
|
|
83
|
+
{ id: 'obj', label: 'OBJ' },
|
|
84
|
+
{ id: 'fbx', label: 'FBX' },
|
|
85
|
+
{ id: 'stl', label: 'STL' },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// ViewType для мультивью: 2-е..8-е вложение (1-е = фронт, без ViewType)
|
|
89
|
+
const VIEW_ORDER_3D = ['left', 'right', 'back', 'top', 'bottom', 'left_front', 'right_front'];
|
|
90
|
+
const VIEW_LABELS_3D = ['Лево', 'Право', 'Зад', 'Верх', 'Низ', 'Лево-перед', 'Право-перед'];
|
|
91
|
+
|
|
29
92
|
/** Порядок: портрет (строка 1), альбом (строка 2), авто — крайнее правое */
|
|
30
93
|
const FORMAT_OPTIONS = [
|
|
31
94
|
{ id: '1:1', label: '1:1', icon: RATIO_ICONS['1:1'] },
|
|
@@ -35,21 +98,27 @@ const FORMAT_OPTIONS = [
|
|
|
35
98
|
{ id: '2:3', label: '2:3', icon: RATIO_ICONS['2:3'] },
|
|
36
99
|
{ id: '9:16', label: '9:16', icon: RATIO_ICONS['9:16'] },
|
|
37
100
|
{ id: '1:2', label: '1:2', icon: RATIO_ICONS['1:2'] },
|
|
101
|
+
{ id: '1:4', label: '1:4', icon: RATIO_ICONS['1:4'] },
|
|
102
|
+
{ id: '1:8', label: '1:8', icon: RATIO_ICONS['1:8'] },
|
|
38
103
|
{ id: '5:4', label: '5:4', icon: RATIO_ICONS['5:4'] },
|
|
39
104
|
{ id: '4:3', label: '4:3', icon: RATIO_ICONS['4:3'] },
|
|
40
105
|
{ id: '14:10', label: '14:10', icon: RATIO_ICONS['14:10'] },
|
|
41
106
|
{ id: '3:2', label: '3:2', icon: RATIO_ICONS['3:2'] },
|
|
42
107
|
{ id: '16:9', label: '16:9', icon: RATIO_ICONS['16:9'] },
|
|
43
108
|
{ id: '2:1', label: '2:1', icon: RATIO_ICONS['2:1'] },
|
|
109
|
+
{ id: '21:9', label: '21:9', icon: RATIO_ICONS['21:9'] },
|
|
110
|
+
{ id: '4:1', label: '4:1', icon: RATIO_ICONS['4:1'] },
|
|
111
|
+
{ id: '8:1', label: '8:1', icon: RATIO_ICONS['8:1'] },
|
|
44
112
|
{ id: 'auto', label: 'Auto', icon: RATIO_ICONS['auto'] },
|
|
45
113
|
];
|
|
46
114
|
|
|
47
115
|
const COUNT_OPTIONS = [
|
|
48
|
-
{ id: 'auto', label: 'Авто', icon: COUNT_ICONS.auto },
|
|
49
116
|
{ id: '1', label: '1 Изображение', icon: COUNT_ICONS[1] },
|
|
50
117
|
{ id: '2', label: '2 Изображения', icon: COUNT_ICONS[2] },
|
|
51
118
|
{ id: '3', label: '3 Изображения', icon: COUNT_ICONS[3] },
|
|
52
119
|
{ id: '4', label: '4 Изображения', icon: COUNT_ICONS[4] },
|
|
120
|
+
{ id: '5', label: '5 Изображений', icon: COUNT_ICONS[4] },
|
|
121
|
+
{ id: '6', label: '6 Изображений', icon: COUNT_ICONS[4] },
|
|
53
122
|
];
|
|
54
123
|
|
|
55
124
|
const BOARD_IMAGE_WIDTH = 300;
|
|
@@ -66,38 +135,28 @@ const BOARD_IMAGE_LANE_REFERENCE_RATIO = [2, 3];
|
|
|
66
135
|
const BOARD_IMAGE_LANE_CENTER_OFFSET = 250;
|
|
67
136
|
const BOARD_IMAGE_LANE_UI_GAP = 16;
|
|
68
137
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
label: 'Алиса',
|
|
79
|
-
icon: ICONS.modelAlice,
|
|
80
|
-
description: 'YandexGPT'
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
id: 'gpt',
|
|
84
|
-
label: 'GPT',
|
|
85
|
-
icon: ICONS.modelGpt,
|
|
86
|
-
description: 'OpenAI'
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
id: 'google',
|
|
90
|
-
label: 'Google',
|
|
91
|
-
icon: ICONS.modelGoogle,
|
|
92
|
-
description: 'Gemini'
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: 'qwen',
|
|
96
|
-
label: 'Qwen',
|
|
97
|
-
icon: '<img src="/icons/qwen.svg" alt="" aria-hidden="true">',
|
|
98
|
-
description: 'Alibaba'
|
|
138
|
+
/** Маппинг провайдера видео → иконка пилла модели. */
|
|
139
|
+
function _iconForVideoProvider(provider) {
|
|
140
|
+
switch (provider) {
|
|
141
|
+
case 'gemini-video': return ICONS.modelGoogle;
|
|
142
|
+
case 'veo': return ICONS.modelGoogle;
|
|
143
|
+
case 'openai-video': return ICONS.modelGpt;
|
|
144
|
+
case 'kling': return ICONS.modelKling;
|
|
145
|
+
case 'seedance': return ICONS.modelSeedance;
|
|
146
|
+
default: return ICONS.model;
|
|
99
147
|
}
|
|
100
|
-
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Маппинг провайдера → иконка пилла модели. */
|
|
151
|
+
function _iconForProvider(provider) {
|
|
152
|
+
switch (provider) {
|
|
153
|
+
case 'yandex-art': return ICONS.modelAlice;
|
|
154
|
+
case 'gemini-image': return ICONS.modelGoogle;
|
|
155
|
+
case 'openai-image': return ICONS.modelGpt;
|
|
156
|
+
case 'qwen-image': return ICONS.modelQwen;
|
|
157
|
+
default: return ICONS.model;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
101
160
|
|
|
102
161
|
/**
|
|
103
162
|
* Корневой контейнер чата ИИ-ассистента.
|
|
@@ -135,15 +194,49 @@ export class ChatWindow {
|
|
|
135
194
|
this._extendedPromptModal = null;
|
|
136
195
|
this._contentTypeId = 'image';
|
|
137
196
|
this._contentTypeMenu = null;
|
|
138
|
-
this._modelId = '
|
|
197
|
+
this._modelId = 'nano-banana-pro';
|
|
139
198
|
this._modelMenu = null;
|
|
140
199
|
this._formatId = 'auto';
|
|
141
200
|
this._formatMenu = null;
|
|
142
201
|
this._providers = [];
|
|
143
|
-
this._countId = '
|
|
202
|
+
this._countId = '1';
|
|
144
203
|
this._countMenu = null;
|
|
204
|
+
// Разрешение — пилл, видим только когда модель имеет resolutions.length > 0
|
|
205
|
+
this._resolutionId = getImageModelCapability('nano-banana-pro')?.defaultResolution ?? null;
|
|
206
|
+
this._resolutionMenu = null;
|
|
207
|
+
// Попап настроек
|
|
208
|
+
this._settingsPopupCtrl = null;
|
|
209
|
+
// Доп-параметры генерации изображения (хранятся здесь, передаются в payload)
|
|
210
|
+
this._imageSeed = null;
|
|
211
|
+
this._imageNegativePrompt = '';
|
|
212
|
+
this._imageBackground = null;
|
|
213
|
+
this._imageOutputFormat = null;
|
|
214
|
+
this._imagePromptExtend = false;
|
|
215
|
+
this._imageWatermark = false;
|
|
145
216
|
this._unsubscribe = null;
|
|
146
217
|
this._attached = false;
|
|
218
|
+
this._3dFaceCountId = '1m';
|
|
219
|
+
this._3dTypeId = 'geo_tex';
|
|
220
|
+
this._3dFaceCountMenu = null;
|
|
221
|
+
this._3dTypeMenu = null;
|
|
222
|
+
this._3dFaceCountWrapper = null;
|
|
223
|
+
this._3dTypeWrapper = null;
|
|
224
|
+
this._3dMode = 'image';
|
|
225
|
+
this._3dTextureStyleId = 'general';
|
|
226
|
+
this._3dResultFormatId = 'glb';
|
|
227
|
+
this._3dModeMenu = null;
|
|
228
|
+
this._3dModeWrapper = null;
|
|
229
|
+
this._3dTextureStyleMenu = null;
|
|
230
|
+
this._3dTextureStyleWrapper = null;
|
|
231
|
+
this._3dResultFormatMenu = null;
|
|
232
|
+
this._3dResultFormatWrapper = null;
|
|
233
|
+
this._model3dSession = null;
|
|
234
|
+
this._model3dUnsubscribe = null;
|
|
235
|
+
this._model3dProgressOverlay = null;
|
|
236
|
+
this._model3dSkeleton = null;
|
|
237
|
+
// Мировые координаты центра скелетона — модель приземлится ровно сюда,
|
|
238
|
+
// независимо от пана/зума во время генерации.
|
|
239
|
+
this._model3dSkeletonWorld = null;
|
|
147
240
|
this._boardImageMessageIds = new Set();
|
|
148
241
|
this._shiftedForImageBatchKeys = new Set();
|
|
149
242
|
this._boardImageShiftHistory = new Map();
|
|
@@ -161,6 +254,23 @@ export class ChatWindow {
|
|
|
161
254
|
// приходит на каждый mousemove и не должен пушить превью в чат —
|
|
162
255
|
// финальный набор картинок мы получим из BoxSelectCommit по strict-contains.
|
|
163
256
|
this._boxSelectActive = false;
|
|
257
|
+
|
|
258
|
+
// Видео-генерация
|
|
259
|
+
this._videoModelId = VIDEO_MODELS[0].id;
|
|
260
|
+
this._videoRatioId = VIDEO_MODELS[0].ratios[0] ?? '16:9';
|
|
261
|
+
this._videoResolutionId = null;
|
|
262
|
+
this._videoDurationId = VIDEO_MODELS[0].defaultDuration ?? VIDEO_MODELS[0].durations[0] ?? 5;
|
|
263
|
+
this._videoSeed = null;
|
|
264
|
+
this._videoNegativePrompt = '';
|
|
265
|
+
this._videoAudio = false;
|
|
266
|
+
this._videoWatermark = false;
|
|
267
|
+
this._videoCfgScale = null;
|
|
268
|
+
this._videoPersonGeneration = 'allow_all';
|
|
269
|
+
this._videoDurationWrapper = null;
|
|
270
|
+
this._videoDurationMenu = null;
|
|
271
|
+
this._videoToolbarPills = null;
|
|
272
|
+
this._videoSession = null;
|
|
273
|
+
this._videoUnsubscribe = null;
|
|
164
274
|
}
|
|
165
275
|
|
|
166
276
|
attach() {
|
|
@@ -181,10 +291,22 @@ export class ChatWindow {
|
|
|
181
291
|
},
|
|
182
292
|
{
|
|
183
293
|
onSubmit: (text, attachments) => {
|
|
294
|
+
if (this._contentTypeId === '3d') {
|
|
295
|
+
return this._submit3d(text, attachments);
|
|
296
|
+
}
|
|
297
|
+
if (this._contentTypeId === 'video') {
|
|
298
|
+
return this._submitVideo(text, attachments);
|
|
299
|
+
}
|
|
184
300
|
this._clearBoardSelection();
|
|
185
301
|
return this._session.send(text, { ...this._getImageRequestOptions(), referenceImages: attachments });
|
|
186
302
|
},
|
|
187
|
-
onAbort: () =>
|
|
303
|
+
onAbort: () => {
|
|
304
|
+
if (this._contentTypeId === 'video') {
|
|
305
|
+
this._videoSession?.abort();
|
|
306
|
+
} else {
|
|
307
|
+
this._session.abort();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
188
310
|
}
|
|
189
311
|
);
|
|
190
312
|
this._composer.attach();
|
|
@@ -213,6 +335,12 @@ export class ChatWindow {
|
|
|
213
335
|
onSelect: (id) => {
|
|
214
336
|
this._contentTypeId = id;
|
|
215
337
|
this._contentTypeMenu.refresh();
|
|
338
|
+
this._update3dPillVisibility();
|
|
339
|
+
this._update3dSendGate();
|
|
340
|
+
this._modelMenu?.refresh();
|
|
341
|
+
this._formatMenu?.refresh();
|
|
342
|
+
this._updateResolutionPillVisibility();
|
|
343
|
+
this._settingsPopupCtrl?.refresh();
|
|
216
344
|
}
|
|
217
345
|
}
|
|
218
346
|
);
|
|
@@ -221,12 +349,24 @@ export class ChatWindow {
|
|
|
221
349
|
this._modelMenu = new ChatPillMenu(
|
|
222
350
|
{ trigger: this._refs.modelPill, menu: this._refs.modelMenu, label: this._refs.modelLabel, icon: this._refs.modelIcon },
|
|
223
351
|
{
|
|
224
|
-
getOptions: () =>
|
|
225
|
-
|
|
352
|
+
getOptions: () => {
|
|
353
|
+
if (this._contentTypeId === 'video') {
|
|
354
|
+
return VIDEO_MODELS.map((m) => ({ id: m.id, label: m.label, description: m.description, icon: _iconForVideoProvider(m.provider) }));
|
|
355
|
+
}
|
|
356
|
+
return IMAGE_MODELS.map((m) => ({ id: m.id, label: m.label, description: m.description, icon: _iconForProvider(m.provider) }));
|
|
357
|
+
},
|
|
358
|
+
getActiveId: () => this._contentTypeId === 'video' ? this._videoModelId : this._modelId,
|
|
226
359
|
onSelect: (id) => {
|
|
227
|
-
this.
|
|
360
|
+
if (this._contentTypeId === 'video') {
|
|
361
|
+
this._videoModelId = id;
|
|
362
|
+
this._clampVideoSettingsToModel();
|
|
363
|
+
} else {
|
|
364
|
+
this._modelId = id;
|
|
365
|
+
this._clampSettingsToModel();
|
|
366
|
+
}
|
|
228
367
|
this._modelMenu.refresh();
|
|
229
|
-
this.
|
|
368
|
+
this._updateResolutionPillVisibility();
|
|
369
|
+
this._settingsPopupCtrl?.refresh();
|
|
230
370
|
}
|
|
231
371
|
}
|
|
232
372
|
);
|
|
@@ -235,22 +375,52 @@ export class ChatWindow {
|
|
|
235
375
|
this._formatMenu = new ChatPillMenu(
|
|
236
376
|
{ trigger: this._refs.formatPill, menu: this._refs.formatMenu, label: this._refs.formatLabel },
|
|
237
377
|
{
|
|
238
|
-
getOptions: () => this._getSupportedFormatOptions(),
|
|
239
|
-
getActiveId: () => this._formatId,
|
|
378
|
+
getOptions: () => this._contentTypeId === 'video' ? this._getSupportedVideoFormatOptions() : this._getSupportedFormatOptions(),
|
|
379
|
+
getActiveId: () => this._contentTypeId === 'video' ? this._videoRatioId : this._formatId,
|
|
240
380
|
onSelect: (id) => {
|
|
241
|
-
this.
|
|
381
|
+
if (this._contentTypeId === 'video') {
|
|
382
|
+
this._videoRatioId = id;
|
|
383
|
+
} else {
|
|
384
|
+
this._formatId = id;
|
|
385
|
+
this._updateFormatPillIcon();
|
|
386
|
+
this._updateFormatPillLabel();
|
|
387
|
+
}
|
|
242
388
|
this._formatMenu.refresh();
|
|
243
|
-
this._updateFormatPillIcon();
|
|
244
|
-
this._updateFormatPillLabel();
|
|
245
389
|
}
|
|
246
390
|
}
|
|
247
391
|
);
|
|
248
392
|
this._formatMenu.attach();
|
|
249
393
|
|
|
394
|
+
this._resolutionMenu = new ChatPillMenu(
|
|
395
|
+
{ trigger: this._refs.resolutionPill, menu: this._refs.resolutionMenu, label: this._refs.resolutionLabel },
|
|
396
|
+
{
|
|
397
|
+
getOptions: () => {
|
|
398
|
+
if (this._contentTypeId === 'video') {
|
|
399
|
+
const cap = getVideoModelCapability(this._videoModelId);
|
|
400
|
+
return (cap?.resolutions ?? []).map((r) => ({ id: r, label: r }));
|
|
401
|
+
}
|
|
402
|
+
const cap = getImageModelCapability(this._modelId);
|
|
403
|
+
return (cap?.resolutions ?? []).map((r) => ({ id: r, label: r }));
|
|
404
|
+
},
|
|
405
|
+
getActiveId: () => this._contentTypeId === 'video' ? this._videoResolutionId : this._resolutionId,
|
|
406
|
+
onSelect: (id) => {
|
|
407
|
+
if (this._contentTypeId === 'video') {
|
|
408
|
+
this._videoResolutionId = id;
|
|
409
|
+
} else {
|
|
410
|
+
this._resolutionId = id;
|
|
411
|
+
this._updateResolutionPillLabel();
|
|
412
|
+
}
|
|
413
|
+
this._resolutionMenu.refresh();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
this._resolutionMenu.attach();
|
|
418
|
+
this._updateResolutionPillVisibility();
|
|
419
|
+
|
|
250
420
|
this._countMenu = new ChatPillMenu(
|
|
251
421
|
{ trigger: this._refs.countPill, menu: this._refs.countMenu, label: this._refs.countLabel, icon: this._refs.countIcon },
|
|
252
422
|
{
|
|
253
|
-
getOptions: () =>
|
|
423
|
+
getOptions: () => this._getSupportedCountOptions(),
|
|
254
424
|
getActiveId: () => this._countId,
|
|
255
425
|
onSelect: (id) => {
|
|
256
426
|
this._countId = id;
|
|
@@ -261,6 +431,59 @@ export class ChatWindow {
|
|
|
261
431
|
);
|
|
262
432
|
this._countMenu.attach();
|
|
263
433
|
|
|
434
|
+
this._settingsPopupCtrl = new ChatSettingsPopup(
|
|
435
|
+
{ trigger: this._refs.settingsTrigger, popup: this._refs.settingsPopup },
|
|
436
|
+
{
|
|
437
|
+
getSettings: () => ({
|
|
438
|
+
systemPrompt: this._session.getState().settings?.systemPrompt ?? '',
|
|
439
|
+
temperature: this._session.getState().settings?.temperature ?? 0.7,
|
|
440
|
+
maxTokens: this._session.getState().settings?.maxTokens ?? 2000,
|
|
441
|
+
seed: this._imageSeed,
|
|
442
|
+
negativePrompt: this._imageNegativePrompt,
|
|
443
|
+
background: this._imageBackground,
|
|
444
|
+
outputFormat: this._imageOutputFormat,
|
|
445
|
+
promptExtend: this._imagePromptExtend,
|
|
446
|
+
watermark: this._imageWatermark,
|
|
447
|
+
videoSeed: this._videoSeed,
|
|
448
|
+
videoNegativePrompt: this._videoNegativePrompt,
|
|
449
|
+
videoAudio: this._videoAudio,
|
|
450
|
+
videoWatermark: this._videoWatermark,
|
|
451
|
+
videoCfgScale: this._videoCfgScale,
|
|
452
|
+
videoPersonGeneration: this._videoPersonGeneration,
|
|
453
|
+
}),
|
|
454
|
+
getCapability: () => getImageModelCapability(this._modelId),
|
|
455
|
+
getVideoCapability: () => getVideoModelCapability(this._videoModelId),
|
|
456
|
+
getContentType: () => this._contentTypeId,
|
|
457
|
+
onChange: (patch) => {
|
|
458
|
+
const sessionPatch = {};
|
|
459
|
+
if ('systemPrompt' in patch) sessionPatch.systemPrompt = patch.systemPrompt;
|
|
460
|
+
if ('temperature' in patch) sessionPatch.temperature = patch.temperature;
|
|
461
|
+
if ('maxTokens' in patch) sessionPatch.maxTokens = patch.maxTokens;
|
|
462
|
+
if (Object.keys(sessionPatch).length > 0) this._session.updateSettings(sessionPatch);
|
|
463
|
+
if ('seed' in patch) this._imageSeed = patch.seed;
|
|
464
|
+
if ('negativePrompt' in patch) this._imageNegativePrompt = patch.negativePrompt;
|
|
465
|
+
if ('background' in patch) this._imageBackground = patch.background;
|
|
466
|
+
if ('outputFormat' in patch) this._imageOutputFormat = patch.outputFormat;
|
|
467
|
+
if ('promptExtend' in patch) this._imagePromptExtend = patch.promptExtend;
|
|
468
|
+
if ('watermark' in patch) this._imageWatermark = patch.watermark;
|
|
469
|
+
if ('videoSeed' in patch) this._videoSeed = patch.videoSeed;
|
|
470
|
+
if ('videoNegativePrompt' in patch) this._videoNegativePrompt = patch.videoNegativePrompt;
|
|
471
|
+
if ('videoAudio' in patch) this._videoAudio = patch.videoAudio;
|
|
472
|
+
if ('videoWatermark' in patch) this._videoWatermark = patch.videoWatermark;
|
|
473
|
+
if ('videoCfgScale' in patch) this._videoCfgScale = patch.videoCfgScale;
|
|
474
|
+
if ('videoPersonGeneration' in patch) this._videoPersonGeneration = patch.videoPersonGeneration;
|
|
475
|
+
},
|
|
476
|
+
onClearHistory: () => this._session.clearHistory()
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
this._settingsPopupCtrl.attach();
|
|
480
|
+
this._settingsPopupCtrl.refresh();
|
|
481
|
+
|
|
482
|
+
this._attach3dPills();
|
|
483
|
+
this._attachVideoPills();
|
|
484
|
+
this._attachVideoToolbarPills();
|
|
485
|
+
this._update3dPillVisibility();
|
|
486
|
+
|
|
264
487
|
const initialState = this._session.getState();
|
|
265
488
|
this._markExistingBoardImages(initialState.messages);
|
|
266
489
|
this._reserveCurrentAiImageLaneSlots();
|
|
@@ -286,12 +509,51 @@ export class ChatWindow {
|
|
|
286
509
|
this._pendingBatchOffsets.clear();
|
|
287
510
|
this._aiImageLaneSlots.clear();
|
|
288
511
|
this._draggingAiImageIds.clear();
|
|
512
|
+
this._model3dUnsubscribe?.();
|
|
513
|
+
this._model3dUnsubscribe = null;
|
|
514
|
+
this._model3dSession?.abort?.();
|
|
515
|
+
this._model3dProgressOverlay?.destroy();
|
|
516
|
+
this._model3dProgressOverlay = null;
|
|
517
|
+
this._clear3dSkeleton();
|
|
518
|
+
this._3dFaceCountMenu?.destroy();
|
|
519
|
+
this._3dFaceCountMenu = null;
|
|
520
|
+
this._3dTypeMenu?.destroy();
|
|
521
|
+
this._3dTypeMenu = null;
|
|
522
|
+
this._3dFaceCountWrapper?.remove();
|
|
523
|
+
this._3dFaceCountWrapper = null;
|
|
524
|
+
this._3dTypeWrapper?.remove();
|
|
525
|
+
this._3dTypeWrapper = null;
|
|
526
|
+
this._3dModeMenu?.destroy();
|
|
527
|
+
this._3dModeMenu = null;
|
|
528
|
+
this._3dModeWrapper?.remove();
|
|
529
|
+
this._3dModeWrapper = null;
|
|
530
|
+
this._3dTextureStyleMenu?.destroy();
|
|
531
|
+
this._3dTextureStyleMenu = null;
|
|
532
|
+
this._3dTextureStyleWrapper?.remove();
|
|
533
|
+
this._3dTextureStyleWrapper = null;
|
|
534
|
+
this._3dResultFormatMenu?.destroy();
|
|
535
|
+
this._3dResultFormatMenu = null;
|
|
536
|
+
this._3dResultFormatWrapper?.remove();
|
|
537
|
+
this._3dResultFormatWrapper = null;
|
|
538
|
+
if (this._videoUnsubscribe) { this._videoUnsubscribe(); this._videoUnsubscribe = null; }
|
|
539
|
+
this._videoSession?.abort?.();
|
|
540
|
+
this._videoSession = null;
|
|
541
|
+
this._videoDurationMenu?.destroy();
|
|
542
|
+
this._videoDurationMenu = null;
|
|
543
|
+
this._videoDurationWrapper?.remove();
|
|
544
|
+
this._videoDurationWrapper = null;
|
|
545
|
+
this._videoToolbarPills?.destroy();
|
|
546
|
+
this._videoToolbarPills = null;
|
|
289
547
|
this._composer?.destroy();
|
|
290
548
|
this._extendedPromptModal?.destroy();
|
|
291
549
|
this._contentTypeMenu?.destroy();
|
|
292
550
|
this._modelMenu?.destroy();
|
|
293
551
|
this._formatMenu?.destroy();
|
|
552
|
+
this._resolutionMenu?.destroy();
|
|
553
|
+
this._resolutionMenu = null;
|
|
294
554
|
this._countMenu?.destroy();
|
|
555
|
+
this._settingsPopupCtrl?.destroy();
|
|
556
|
+
this._settingsPopupCtrl = null;
|
|
295
557
|
if (this._refs?.root && this._refs.root.parentNode === this._container) {
|
|
296
558
|
this._container.removeChild(this._refs.root);
|
|
297
559
|
}
|
|
@@ -310,6 +572,341 @@ export class ChatWindow {
|
|
|
310
572
|
this.detach();
|
|
311
573
|
}
|
|
312
574
|
|
|
575
|
+
_attach3dPills() {
|
|
576
|
+
const pillsContainer = this._refs.formatPill?.closest('.moodboard-chat__pills');
|
|
577
|
+
if (!pillsContainer) return;
|
|
578
|
+
|
|
579
|
+
const mk3dPill = (label, icon) => {
|
|
580
|
+
const wrapper = document.createElement('div');
|
|
581
|
+
wrapper.className = 'moodboard-chat__pill-wrapper';
|
|
582
|
+
const pill = document.createElement('button');
|
|
583
|
+
pill.type = 'button';
|
|
584
|
+
pill.className = 'moodboard-chat__pill';
|
|
585
|
+
pill.setAttribute('aria-haspopup', 'menu');
|
|
586
|
+
pill.setAttribute('aria-expanded', 'false');
|
|
587
|
+
const iconSpan = document.createElement('span');
|
|
588
|
+
iconSpan.className = 'moodboard-chat__pill-icon-wrap';
|
|
589
|
+
iconSpan.innerHTML = icon;
|
|
590
|
+
const labelEl = document.createElement('span');
|
|
591
|
+
labelEl.className = 'moodboard-chat__pill-label';
|
|
592
|
+
labelEl.textContent = label;
|
|
593
|
+
pill.appendChild(iconSpan);
|
|
594
|
+
pill.appendChild(labelEl);
|
|
595
|
+
const menu = document.createElement('div');
|
|
596
|
+
menu.className = 'moodboard-chat__menu';
|
|
597
|
+
menu.setAttribute('role', 'menu');
|
|
598
|
+
wrapper.appendChild(pill);
|
|
599
|
+
wrapper.appendChild(menu);
|
|
600
|
+
return { wrapper, pill, menu, labelEl, iconEl: iconSpan };
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const fc = mk3dPill('1M', ICONS.sliders);
|
|
604
|
+
pillsContainer.appendChild(fc.wrapper);
|
|
605
|
+
this._3dFaceCountWrapper = fc.wrapper;
|
|
606
|
+
this._3dFaceCountMenu = new ChatPillMenu(
|
|
607
|
+
{ trigger: fc.pill, menu: fc.menu, label: fc.labelEl, icon: fc.iconEl },
|
|
608
|
+
{
|
|
609
|
+
getOptions: () => FACE_COUNT_OPTIONS_3D,
|
|
610
|
+
getActiveId: () => this._3dFaceCountId,
|
|
611
|
+
onSelect: (id) => { this._3dFaceCountId = id; this._3dFaceCountMenu.refresh(); }
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
this._3dFaceCountMenu.attach();
|
|
615
|
+
|
|
616
|
+
const tp = mk3dPill('Геометрия + Текстура', ICONS.cube);
|
|
617
|
+
pillsContainer.appendChild(tp.wrapper);
|
|
618
|
+
this._3dTypeWrapper = tp.wrapper;
|
|
619
|
+
this._3dTypeMenu = new ChatPillMenu(
|
|
620
|
+
{ trigger: tp.pill, menu: tp.menu, label: tp.labelEl, icon: tp.iconEl },
|
|
621
|
+
{
|
|
622
|
+
getOptions: () => TYPE_3D_OPTIONS,
|
|
623
|
+
getActiveId: () => this._3dTypeId,
|
|
624
|
+
onSelect: (id) => { this._3dTypeId = id; this._3dTypeMenu.refresh(); }
|
|
625
|
+
}
|
|
626
|
+
);
|
|
627
|
+
this._3dTypeMenu.attach();
|
|
628
|
+
|
|
629
|
+
const md = mk3dPill('Изображение', ICONS.image);
|
|
630
|
+
pillsContainer.appendChild(md.wrapper);
|
|
631
|
+
this._3dModeWrapper = md.wrapper;
|
|
632
|
+
this._3dModeMenu = new ChatPillMenu(
|
|
633
|
+
{ trigger: md.pill, menu: md.menu, label: md.labelEl, icon: md.iconEl },
|
|
634
|
+
{
|
|
635
|
+
getOptions: () => MODE_3D_OPTIONS,
|
|
636
|
+
getActiveId: () => this._3dMode,
|
|
637
|
+
onSelect: (id) => { this._3dModeMenu.refresh(); this._update3dMode(id); }
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
this._3dModeMenu.attach();
|
|
641
|
+
|
|
642
|
+
const ts = mk3dPill('Общий', ICONS.palette);
|
|
643
|
+
pillsContainer.appendChild(ts.wrapper);
|
|
644
|
+
this._3dTextureStyleWrapper = ts.wrapper;
|
|
645
|
+
this._3dTextureStyleMenu = new ChatPillMenu(
|
|
646
|
+
{ trigger: ts.pill, menu: ts.menu, label: ts.labelEl, icon: ts.iconEl },
|
|
647
|
+
{
|
|
648
|
+
getOptions: () => TEXTURE_STYLE_OPTIONS_3D,
|
|
649
|
+
getActiveId: () => this._3dTextureStyleId,
|
|
650
|
+
onSelect: (id) => { this._3dTextureStyleId = id; this._3dTextureStyleMenu.refresh(); }
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
this._3dTextureStyleMenu.attach();
|
|
654
|
+
|
|
655
|
+
const rf = mk3dPill('GLB', ICONS.sliders);
|
|
656
|
+
pillsContainer.appendChild(rf.wrapper);
|
|
657
|
+
this._3dResultFormatWrapper = rf.wrapper;
|
|
658
|
+
this._3dResultFormatMenu = new ChatPillMenu(
|
|
659
|
+
{ trigger: rf.pill, menu: rf.menu, label: rf.labelEl, icon: rf.iconEl },
|
|
660
|
+
{
|
|
661
|
+
getOptions: () => RESULT_FORMAT_OPTIONS_3D,
|
|
662
|
+
getActiveId: () => this._3dResultFormatId,
|
|
663
|
+
onSelect: (id) => { this._3dResultFormatId = id; this._3dResultFormatMenu.refresh(); }
|
|
664
|
+
}
|
|
665
|
+
);
|
|
666
|
+
this._3dResultFormatMenu.attach();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
_update3dPillVisibility() {
|
|
670
|
+
const is3d = this._contentTypeId === '3d';
|
|
671
|
+
const isVideo = this._contentTypeId === 'video';
|
|
672
|
+
const formatWrapper = this._refs.formatPill?.closest('.moodboard-chat__pill-wrapper');
|
|
673
|
+
const countWrapper = this._refs.countPill?.closest('.moodboard-chat__pill-wrapper');
|
|
674
|
+
const modelWrapper = this._refs.modelPill?.closest('.moodboard-chat__pill-wrapper');
|
|
675
|
+
if (formatWrapper) formatWrapper.style.display = is3d ? 'none' : '';
|
|
676
|
+
// В видео-режиме счётчик не нужен: у всех видео-моделей maxCount=1
|
|
677
|
+
if (countWrapper) countWrapper.style.display = (is3d || isVideo) ? 'none' : '';
|
|
678
|
+
if (modelWrapper) modelWrapper.style.display = is3d ? 'none' : '';
|
|
679
|
+
// Разрешение — скрываем в 3D-режиме; при возврате — восстанавливаем по capability
|
|
680
|
+
if (is3d) {
|
|
681
|
+
if (this._refs?.resolutionWrapper) this._refs.resolutionWrapper.style.display = 'none';
|
|
682
|
+
} else {
|
|
683
|
+
this._updateResolutionPillVisibility();
|
|
684
|
+
}
|
|
685
|
+
if (this._3dFaceCountWrapper) this._3dFaceCountWrapper.style.display = is3d ? '' : 'none';
|
|
686
|
+
if (this._3dTypeWrapper) this._3dTypeWrapper.style.display = is3d ? '' : 'none';
|
|
687
|
+
if (this._3dModeWrapper) this._3dModeWrapper.style.display = is3d ? '' : 'none';
|
|
688
|
+
if (this._3dResultFormatWrapper) this._3dResultFormatWrapper.style.display = is3d ? '' : 'none';
|
|
689
|
+
// Стиль текстуры — только в text-режиме
|
|
690
|
+
if (this._3dTextureStyleWrapper) {
|
|
691
|
+
this._3dTextureStyleWrapper.style.display = (is3d && this._3dMode === 'text') ? '' : 'none';
|
|
692
|
+
}
|
|
693
|
+
// Длительность — только в видео-режиме
|
|
694
|
+
if (this._videoDurationWrapper) this._videoDurationWrapper.style.display = isVideo ? '' : 'none';
|
|
695
|
+
// Видео-настройки (Звук/Водяной знак/Люди/Seed/CFG/Негатив) — пилюли тулбара
|
|
696
|
+
this._videoToolbarPills?.refresh();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
_update3dSendGate() {
|
|
700
|
+
if (this._contentTypeId !== '3d') {
|
|
701
|
+
this._composer?.setInputEnabled(true);
|
|
702
|
+
this._composer?.setPlaceholderOverride(null);
|
|
703
|
+
if (this._refs?.textarea) {
|
|
704
|
+
this._refs.textarea.placeholder = 'Опишите то, что хотите сгенерировать';
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const inputEnabled = this._3dMode === 'text';
|
|
709
|
+
this._composer?.setInputEnabled(inputEnabled);
|
|
710
|
+
if (this._3dMode === 'image') {
|
|
711
|
+
this._composer?.setPlaceholderOverride('Описание не нужно — нажмите «Отправить»');
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
this._composer?.setPlaceholderOverride(null);
|
|
715
|
+
if (this._refs?.textarea) {
|
|
716
|
+
if (this._3dMode === 'text') {
|
|
717
|
+
this._refs.textarea.placeholder = 'Опишите то, что хотите создать';
|
|
718
|
+
} else {
|
|
719
|
+
this._refs.textarea.placeholder = 'Выберите 2–8 изображений на доске';
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
_update3dMode(id) {
|
|
725
|
+
this._3dMode = id;
|
|
726
|
+
this._update3dPillVisibility();
|
|
727
|
+
this._update3dSendGate();
|
|
728
|
+
if (id === 'multi') {
|
|
729
|
+
this._composer?.setAttachmentLabelProvider((i) =>
|
|
730
|
+
i === 0 ? 'Фронт' : (VIEW_LABELS_3D[i - 1] ?? String(i + 1))
|
|
731
|
+
);
|
|
732
|
+
} else {
|
|
733
|
+
this._composer?.setAttachmentLabelProvider(null);
|
|
734
|
+
}
|
|
735
|
+
if (id === 'text') {
|
|
736
|
+
// В text-режиме вложения с доски недоступны — очищаем
|
|
737
|
+
this._composer?.removeAllBoardAttachments?.();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async _submit3d(text, attachments) {
|
|
742
|
+
const mode = this._3dMode;
|
|
743
|
+
|
|
744
|
+
if (mode === 'text') {
|
|
745
|
+
if (!text) return;
|
|
746
|
+
} else if (mode === 'image') {
|
|
747
|
+
if (!Array.isArray(attachments) || attachments.length !== 1) return;
|
|
748
|
+
} else if (mode === 'multi') {
|
|
749
|
+
if (!Array.isArray(attachments) || attachments.length < 2 || attachments.length > 8) return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (!this._model3dSession) {
|
|
753
|
+
this._model3dSession = new Model3dSessionController({ aiClient: this._aiClient });
|
|
754
|
+
}
|
|
755
|
+
this._model3dUnsubscribe?.();
|
|
756
|
+
|
|
757
|
+
if (!this._model3dProgressOverlay) {
|
|
758
|
+
this._model3dProgressOverlay = new Model3dProgressOverlay();
|
|
759
|
+
}
|
|
760
|
+
this._model3dProgressOverlay.attach(this._refs.composer);
|
|
761
|
+
|
|
762
|
+
this._model3dUnsubscribe = this._model3dSession.subscribe((state) => {
|
|
763
|
+
this._onModel3dState(state);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
this._show3dSkeleton();
|
|
768
|
+
} catch (e) {
|
|
769
|
+
console.warn('[ChatWindow] _show3dSkeleton failed:', e);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const suffix = TEXTURE_STYLE_SUFFIX_3D[this._3dTextureStyleId] ?? '';
|
|
773
|
+
const prompt = mode === 'text' ? (text + suffix) : undefined;
|
|
774
|
+
|
|
775
|
+
void this._model3dSession.start({
|
|
776
|
+
mode,
|
|
777
|
+
prompt,
|
|
778
|
+
image: mode !== 'text' ? attachments[0] : undefined,
|
|
779
|
+
multiViewImages: mode === 'multi'
|
|
780
|
+
? attachments.slice(1).map((file, i) => ({ file, viewType: VIEW_ORDER_3D[i] }))
|
|
781
|
+
: undefined,
|
|
782
|
+
model: '3.1',
|
|
783
|
+
generateType: this._3dTypeId === 'geo' ? 'Geometry' : 'Normal',
|
|
784
|
+
faceCount: FACE_COUNT_MAP_3D[this._3dFaceCountId] ?? 1000000,
|
|
785
|
+
pbr: false,
|
|
786
|
+
downloadFormat: this._3dResultFormatId,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
_onModel3dState(state) {
|
|
791
|
+
this._model3dProgressOverlay?.update(state);
|
|
792
|
+
|
|
793
|
+
if (state.status === 'done' && state.result) {
|
|
794
|
+
this._place3dModelOnBoard(state.result);
|
|
795
|
+
this._clear3dSkeleton();
|
|
796
|
+
// Оверлей убираем чуть позже скелетона, чтобы прогресс не «прыгал» —
|
|
797
|
+
// достаточно синхронного detach после размещения.
|
|
798
|
+
this._model3dProgressOverlay?.detach();
|
|
799
|
+
} else if (state.status === 'error') {
|
|
800
|
+
this._model3dProgressOverlay?.detach();
|
|
801
|
+
this._clear3dSkeleton();
|
|
802
|
+
if (this._refs?.errorBlock) {
|
|
803
|
+
const raw = state.error || 'Ошибка генерации 3D-модели';
|
|
804
|
+
// Убираем технический префикс вида "AiClient.submit3dModel (503): ".
|
|
805
|
+
const clean = raw.replace(/^AiClient\.\w+\s*\(\d+\):\s*/, '');
|
|
806
|
+
this._refs.errorBlock.textContent = clean || 'Ошибка генерации 3D-модели';
|
|
807
|
+
this._refs.errorBlock.classList.add('is-visible');
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Экранный прямоугольник скелетона/модели в текущем вьюпорте из мировых координат центра.
|
|
814
|
+
* Размер фиксирован шириной BOARD_IMAGE_WIDTH в world-единицах (квадрат), как у заглушки.
|
|
815
|
+
* @param {{x:number,y:number}} worldCenter
|
|
816
|
+
*/
|
|
817
|
+
_compute3dSkeletonRect(worldCenter) {
|
|
818
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
819
|
+
const s = world?.scale?.x || 1;
|
|
820
|
+
const size = Math.round(BOARD_IMAGE_WIDTH * s);
|
|
821
|
+
const screenX = Math.round(worldCenter.x * s + (world?.x || 0));
|
|
822
|
+
const screenY = Math.round(worldCenter.y * s + (world?.y || 0));
|
|
823
|
+
return {
|
|
824
|
+
left: Math.round(screenX - size / 2),
|
|
825
|
+
top: Math.round(screenY - size / 2),
|
|
826
|
+
width: size,
|
|
827
|
+
height: size,
|
|
828
|
+
radius: Math.max(2, Math.round(12 * s))
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
_show3dSkeleton() {
|
|
833
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
834
|
+
if (!world) return;
|
|
835
|
+
const s = world?.scale?.x || 1;
|
|
836
|
+
|
|
837
|
+
// Центр будущей модели в экранных координатах: над композером (как при размещении).
|
|
838
|
+
const composerRect = this._refs?.composer?.getBoundingClientRect?.();
|
|
839
|
+
const screenCenterX = composerRect
|
|
840
|
+
? Math.round(composerRect.left + composerRect.width / 2)
|
|
841
|
+
: 400;
|
|
842
|
+
const screenCenterY = composerRect
|
|
843
|
+
? Math.round(composerRect.top - 200)
|
|
844
|
+
: 200;
|
|
845
|
+
|
|
846
|
+
// _clear3dSkeleton() обнуляет _model3dSkeletonWorld — присваиваем ПОСЛЕ него.
|
|
847
|
+
this._clear3dSkeleton();
|
|
848
|
+
|
|
849
|
+
this._model3dSkeletonWorld = {
|
|
850
|
+
x: (screenCenterX - (world?.x || 0)) / s,
|
|
851
|
+
y: (screenCenterY - (world?.y || 0)) / s
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
this._model3dSkeleton = new Model3dBoardSkeleton({ iconSvg: ICONS.cube });
|
|
855
|
+
this._model3dSkeleton.attach(this._container ?? document.body);
|
|
856
|
+
this._model3dSkeleton.setRect(this._compute3dSkeletonRect(this._model3dSkeletonWorld), { animate: false });
|
|
857
|
+
this._scheduleAnimationFrame(() => this._model3dSkeleton?.enter());
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
_sync3dSkeletonToViewport({ disableTransition = false } = {}) {
|
|
861
|
+
if (!this._model3dSkeleton?.isAttached() || !this._model3dSkeletonWorld) return;
|
|
862
|
+
this._model3dSkeleton.setRect(
|
|
863
|
+
this._compute3dSkeletonRect(this._model3dSkeletonWorld),
|
|
864
|
+
{ animate: !disableTransition }
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
_clear3dSkeleton() {
|
|
869
|
+
this._model3dSkeleton?.destroy();
|
|
870
|
+
this._model3dSkeleton = null;
|
|
871
|
+
this._model3dSkeletonWorld = null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
_place3dModelOnBoard(result) {
|
|
875
|
+
if (!this._boardCore?.eventBus) return;
|
|
876
|
+
const { previewBase64, mimeType, modelUrl, format } = result;
|
|
877
|
+
const src = previewBase64
|
|
878
|
+
? `data:${mimeType || 'image/jpeg'};base64,${previewBase64}`
|
|
879
|
+
: modelUrl;
|
|
880
|
+
if (!src) return;
|
|
881
|
+
|
|
882
|
+
const world = this._boardCore.pixi?.worldLayer || this._boardCore.pixi?.app?.stage;
|
|
883
|
+
const s = world?.scale?.x || 1;
|
|
884
|
+
|
|
885
|
+
// Приоритет — мировая точка скелетона: модель приземляется ровно туда,
|
|
886
|
+
// где пользователь видел заглушку, даже если он панорамировал/зумил во время генерации.
|
|
887
|
+
let x;
|
|
888
|
+
let y;
|
|
889
|
+
if (this._model3dSkeletonWorld) {
|
|
890
|
+
x = Math.round(this._model3dSkeletonWorld.x * s + (world?.x || 0));
|
|
891
|
+
y = Math.round(this._model3dSkeletonWorld.y * s + (world?.y || 0));
|
|
892
|
+
} else {
|
|
893
|
+
const composerRect = this._refs?.composer?.getBoundingClientRect?.();
|
|
894
|
+
x = composerRect ? Math.round(composerRect.left + composerRect.width / 2) : 400;
|
|
895
|
+
y = composerRect ? Math.round(composerRect.top - 200) : 200;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
this._boardCore.eventBus.emit(Events.UI.PasteImageAt, {
|
|
899
|
+
x,
|
|
900
|
+
y,
|
|
901
|
+
src,
|
|
902
|
+
name: 'ai-3d-model',
|
|
903
|
+
skipUpload: true,
|
|
904
|
+
objectType: 'model3d-screenshot-img',
|
|
905
|
+
modelUrl,
|
|
906
|
+
format
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
313
910
|
_clearBoardSelection() {
|
|
314
911
|
if (typeof this._boardCore?.selectTool?.clearSelection === 'function') {
|
|
315
912
|
this._boardCore.selectTool.clearSelection();
|
|
@@ -343,7 +940,9 @@ export class ChatWindow {
|
|
|
343
940
|
const list = await this._aiClient.listProviders();
|
|
344
941
|
this._providers = Array.isArray(list) ? list : [];
|
|
345
942
|
this._session.setAvailableProviders(list);
|
|
346
|
-
|
|
943
|
+
// Серверные supportedRatios могут дополнительно сузить список —
|
|
944
|
+
// после загрузки ещё раз клампим настройки на случай несоответствия.
|
|
945
|
+
this._clampSettingsToModel();
|
|
347
946
|
} catch (err) {
|
|
348
947
|
console.warn('[ChatWindow] cannot load providers:', err.message);
|
|
349
948
|
this._providers = [];
|
|
@@ -352,52 +951,251 @@ export class ChatWindow {
|
|
|
352
951
|
}
|
|
353
952
|
|
|
354
953
|
/**
|
|
355
|
-
* Возвращает
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
* @param {string} modelId
|
|
359
|
-
* @returns {string}
|
|
360
|
-
*/
|
|
361
|
-
_imageProviderForModel(modelId) {
|
|
362
|
-
return modelId === 'gpt' ? 'openai-image' : 'yandex-art';
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Возвращает подмножество FORMAT_OPTIONS, поддерживаемое текущим провайдером.
|
|
367
|
-
* Если провайдер не ограничивает форматы (supportedRatios === null) — возвращает все.
|
|
954
|
+
* Возвращает подмножество FORMAT_OPTIONS, поддерживаемое текущей моделью.
|
|
955
|
+
* Источник истины — capability.ratios из imageModelCapabilities.
|
|
368
956
|
* Формат 'auto' присутствует всегда.
|
|
369
957
|
*
|
|
370
958
|
* @returns {Array}
|
|
371
959
|
*/
|
|
372
960
|
_getSupportedFormatOptions() {
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
const ratios = provider?.supportedRatios;
|
|
376
|
-
|
|
377
|
-
if (!Array.isArray(ratios) || ratios.length === 0) {
|
|
961
|
+
const cap = getImageModelCapability(this._modelId);
|
|
962
|
+
if (!cap || !Array.isArray(cap.ratios) || cap.ratios.length === 0) {
|
|
378
963
|
return FORMAT_OPTIONS;
|
|
379
964
|
}
|
|
965
|
+
return FORMAT_OPTIONS.filter((opt) => opt.id === 'auto' || cap.ratios.includes(opt.id));
|
|
966
|
+
}
|
|
380
967
|
|
|
381
|
-
|
|
968
|
+
/**
|
|
969
|
+
* Возвращает подмножество COUNT_OPTIONS, поддерживаемое текущей моделью.
|
|
970
|
+
* Опции выше capability.maxCount скрываются.
|
|
971
|
+
*
|
|
972
|
+
* @returns {Array}
|
|
973
|
+
*/
|
|
974
|
+
_getSupportedCountOptions() {
|
|
975
|
+
const cap = getImageModelCapability(this._modelId);
|
|
976
|
+
const maxCount = cap?.maxCount ?? 4;
|
|
977
|
+
return COUNT_OPTIONS.filter((opt) => {
|
|
978
|
+
const n = Number.parseInt(opt.id, 10);
|
|
979
|
+
return !Number.isFinite(n) || n <= maxCount;
|
|
980
|
+
});
|
|
382
981
|
}
|
|
383
982
|
|
|
384
983
|
/**
|
|
385
|
-
* При смене модели
|
|
386
|
-
*
|
|
984
|
+
* При смене модели приводит текущий формат, разрешение и количество
|
|
985
|
+
* к допустимым значениям. Невалидное значение — на дефолт.
|
|
387
986
|
*/
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
987
|
+
_clampSettingsToModel() {
|
|
988
|
+
const cap = getImageModelCapability(this._modelId);
|
|
989
|
+
|
|
990
|
+
// Формат
|
|
991
|
+
const supportedFormats = this._getSupportedFormatOptions();
|
|
992
|
+
if (!supportedFormats.some((opt) => opt.id === this._formatId)) {
|
|
392
993
|
this._formatId = 'auto';
|
|
393
994
|
}
|
|
394
|
-
// refresh обновляет лейбл из option.label ('Auto'), поэтому _updateFormatPillLabel
|
|
395
|
-
// вызывается после — она выставляет человекочитаемый текст 'Соотношение сторон'.
|
|
396
995
|
this._formatMenu?.refresh();
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
996
|
+
this._updateFormatPillIcon();
|
|
997
|
+
this._updateFormatPillLabel();
|
|
998
|
+
|
|
999
|
+
// Разрешение
|
|
1000
|
+
const resolutions = cap?.resolutions ?? [];
|
|
1001
|
+
if (resolutions.length === 0) {
|
|
1002
|
+
this._resolutionId = null;
|
|
1003
|
+
} else if (!resolutions.includes(this._resolutionId)) {
|
|
1004
|
+
this._resolutionId = cap.defaultResolution ?? resolutions[0] ?? null;
|
|
1005
|
+
}
|
|
1006
|
+
this._resolutionMenu?.refresh();
|
|
1007
|
+
this._updateResolutionPillLabel();
|
|
1008
|
+
|
|
1009
|
+
// Количество
|
|
1010
|
+
const maxCount = cap?.maxCount ?? 4;
|
|
1011
|
+
const n = Number.parseInt(this._countId, 10);
|
|
1012
|
+
if (Number.isFinite(n) && n > maxCount) {
|
|
1013
|
+
this._countId = '1';
|
|
1014
|
+
}
|
|
1015
|
+
this._countMenu?.refresh();
|
|
1016
|
+
this._updateCountPillIcon();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
_updateResolutionPillVisibility() {
|
|
1020
|
+
const isVideo = this._contentTypeId === 'video';
|
|
1021
|
+
const cap = isVideo ? getVideoModelCapability(this._videoModelId) : getImageModelCapability(this._modelId);
|
|
1022
|
+
const hasResolutions = Array.isArray(cap?.resolutions) && cap.resolutions.length > 0;
|
|
1023
|
+
const wrapper = this._refs?.resolutionWrapper;
|
|
1024
|
+
if (wrapper) wrapper.style.display = hasResolutions ? '' : 'none';
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
_updateResolutionPillLabel() {
|
|
1028
|
+
const labelEl = this._refs?.resolutionLabel;
|
|
1029
|
+
if (!labelEl) return;
|
|
1030
|
+
labelEl.textContent = this._resolutionId ?? 'Разрешение';
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
_getSupportedVideoFormatOptions() {
|
|
1034
|
+
const cap = getVideoModelCapability(this._videoModelId);
|
|
1035
|
+
return (cap?.ratios ?? ['16:9']).map((r) => ({ id: r, label: r }));
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
_clampVideoSettingsToModel() {
|
|
1039
|
+
const cap = getVideoModelCapability(this._videoModelId);
|
|
1040
|
+
|
|
1041
|
+
const ratios = cap?.ratios ?? [];
|
|
1042
|
+
if (ratios.length > 0 && !ratios.includes(this._videoRatioId)) {
|
|
1043
|
+
this._videoRatioId = ratios[0];
|
|
1044
|
+
}
|
|
1045
|
+
this._formatMenu?.refresh();
|
|
1046
|
+
|
|
1047
|
+
const resolutions = cap?.resolutions ?? [];
|
|
1048
|
+
if (resolutions.length === 0) {
|
|
1049
|
+
this._videoResolutionId = null;
|
|
1050
|
+
} else if (!resolutions.includes(this._videoResolutionId)) {
|
|
1051
|
+
this._videoResolutionId = resolutions[0];
|
|
1052
|
+
}
|
|
1053
|
+
this._resolutionMenu?.refresh();
|
|
1054
|
+
this._updateResolutionPillVisibility();
|
|
1055
|
+
|
|
1056
|
+
const durations = cap?.durations ?? [];
|
|
1057
|
+
if (!durations.includes(this._videoDurationId)) {
|
|
1058
|
+
this._videoDurationId = cap?.defaultDuration ?? durations[0] ?? 5;
|
|
400
1059
|
}
|
|
1060
|
+
this._videoDurationMenu?.refresh();
|
|
1061
|
+
this._settingsPopupCtrl?.refresh();
|
|
1062
|
+
this._videoToolbarPills?.refresh();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
_attachVideoPills() {
|
|
1066
|
+
const pillsContainer = this._refs.formatPill?.closest('.moodboard-chat__pills');
|
|
1067
|
+
if (!pillsContainer) return;
|
|
1068
|
+
|
|
1069
|
+
const wrapper = document.createElement('div');
|
|
1070
|
+
wrapper.className = 'moodboard-chat__pill-wrapper';
|
|
1071
|
+
const pill = document.createElement('button');
|
|
1072
|
+
pill.type = 'button';
|
|
1073
|
+
pill.className = 'moodboard-chat__pill';
|
|
1074
|
+
pill.setAttribute('aria-haspopup', 'menu');
|
|
1075
|
+
pill.setAttribute('aria-expanded', 'false');
|
|
1076
|
+
const iconSpan = document.createElement('span');
|
|
1077
|
+
iconSpan.className = 'moodboard-chat__pill-icon-wrap';
|
|
1078
|
+
iconSpan.innerHTML = ICONS.video;
|
|
1079
|
+
const labelEl = document.createElement('span');
|
|
1080
|
+
labelEl.className = 'moodboard-chat__pill-label';
|
|
1081
|
+
labelEl.textContent = `${this._videoDurationId} сек`;
|
|
1082
|
+
pill.appendChild(iconSpan);
|
|
1083
|
+
pill.appendChild(labelEl);
|
|
1084
|
+
const menu = document.createElement('div');
|
|
1085
|
+
menu.className = 'moodboard-chat__menu';
|
|
1086
|
+
menu.setAttribute('role', 'menu');
|
|
1087
|
+
wrapper.appendChild(pill);
|
|
1088
|
+
wrapper.appendChild(menu);
|
|
1089
|
+
|
|
1090
|
+
pillsContainer.appendChild(wrapper);
|
|
1091
|
+
this._videoDurationWrapper = wrapper;
|
|
1092
|
+
this._videoDurationMenu = new ChatPillMenu(
|
|
1093
|
+
{ trigger: pill, menu, label: labelEl, icon: iconSpan },
|
|
1094
|
+
{
|
|
1095
|
+
getOptions: () => {
|
|
1096
|
+
const cap = getVideoModelCapability(this._videoModelId);
|
|
1097
|
+
return (cap?.durations ?? []).map((d) => ({ id: String(d), label: `${d} сек` }));
|
|
1098
|
+
},
|
|
1099
|
+
getActiveId: () => String(this._videoDurationId),
|
|
1100
|
+
onSelect: (id) => {
|
|
1101
|
+
this._videoDurationId = Number(id);
|
|
1102
|
+
this._videoDurationMenu.refresh();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
);
|
|
1106
|
+
this._videoDurationMenu.attach();
|
|
1107
|
+
this._videoDurationMenu.refresh();
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
_attachVideoToolbarPills() {
|
|
1111
|
+
const pillsContainer = this._refs.formatPill?.closest('.moodboard-chat__pills');
|
|
1112
|
+
if (!pillsContainer) return;
|
|
1113
|
+
this._videoToolbarPills = new ChatVideoToolbarPills(pillsContainer, {
|
|
1114
|
+
getActive: () => this._contentTypeId === 'video',
|
|
1115
|
+
getModelId: () => this._videoModelId,
|
|
1116
|
+
getState: () => ({
|
|
1117
|
+
seed: this._videoSeed,
|
|
1118
|
+
negativePrompt: this._videoNegativePrompt,
|
|
1119
|
+
audio: this._videoAudio,
|
|
1120
|
+
watermark: this._videoWatermark,
|
|
1121
|
+
cfgScale: this._videoCfgScale,
|
|
1122
|
+
personGeneration: this._videoPersonGeneration,
|
|
1123
|
+
}),
|
|
1124
|
+
onChange: (patch) => {
|
|
1125
|
+
if ('seed' in patch) this._videoSeed = patch.seed;
|
|
1126
|
+
if ('negativePrompt' in patch) this._videoNegativePrompt = patch.negativePrompt;
|
|
1127
|
+
if ('audio' in patch) this._videoAudio = patch.audio;
|
|
1128
|
+
if ('watermark' in patch) this._videoWatermark = patch.watermark;
|
|
1129
|
+
if ('cfgScale' in patch) this._videoCfgScale = patch.cfgScale;
|
|
1130
|
+
if ('personGeneration' in patch) this._videoPersonGeneration = patch.personGeneration;
|
|
1131
|
+
this._videoToolbarPills?.refresh();
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
this._videoToolbarPills.attach();
|
|
1135
|
+
this._videoToolbarPills.refresh();
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async _submitVideo(text, attachments) {
|
|
1139
|
+
if (!text) return;
|
|
1140
|
+
if (!this._videoSession) {
|
|
1141
|
+
this._videoSession = new VideoSessionController({ aiClient: this._aiClient });
|
|
1142
|
+
}
|
|
1143
|
+
if (this._videoUnsubscribe) {
|
|
1144
|
+
this._videoUnsubscribe();
|
|
1145
|
+
this._videoUnsubscribe = null;
|
|
1146
|
+
}
|
|
1147
|
+
this._videoUnsubscribe = this._videoSession.subscribe((state) => this._onVideoState(state));
|
|
1148
|
+
const opts = this._getVideoRequestOptions();
|
|
1149
|
+
void this._videoSession.start({ ...opts, prompt: text, referenceImages: attachments?.length ? attachments : undefined });
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
_onVideoState(state) {
|
|
1153
|
+
if (state.status === 'submitting' || state.status === 'polling') {
|
|
1154
|
+
this._composer?.setStreaming(true);
|
|
1155
|
+
}
|
|
1156
|
+
if (state.status === 'done') {
|
|
1157
|
+
this._composer?.setStreaming(false);
|
|
1158
|
+
// Тип объекта «видео» на доске не существует. Событие Events.UI.PasteImageAt
|
|
1159
|
+
// ожидает data URL картинки — видео-URL несовместим. Размещение на доске
|
|
1160
|
+
// выходит за рамки текущей фазы (нет объекта типа video).
|
|
1161
|
+
console.info('[ChatWindow] Видео готово. Размещение на доске не поддерживается: нет объекта типа video.', state.result?.videoUrl);
|
|
1162
|
+
if (this._refs?.errorBlock) {
|
|
1163
|
+
this._refs.errorBlock.textContent = 'Видео сгенерировано. Размещение на доске появится в следующей фазе.';
|
|
1164
|
+
this._refs.errorBlock.classList.add('is-visible');
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (state.status === 'error') {
|
|
1168
|
+
this._composer?.setStreaming(false);
|
|
1169
|
+
if (this._refs?.errorBlock) {
|
|
1170
|
+
const clean = (state.error || 'Ошибка генерации видео').replace(/^AiClient\.\w+\s*\(\d+\):\s*/, '');
|
|
1171
|
+
this._refs.errorBlock.textContent = clean;
|
|
1172
|
+
this._refs.errorBlock.classList.add('is-visible');
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (state.status === 'idle') {
|
|
1176
|
+
this._composer?.setStreaming(false);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
_getVideoRequestOptions() {
|
|
1181
|
+
const cap = getVideoModelCapability(this._videoModelId);
|
|
1182
|
+
const opts = {
|
|
1183
|
+
provider: cap?.provider ?? 'gemini-video',
|
|
1184
|
+
model: cap?.model,
|
|
1185
|
+
ratio: this._videoRatioId,
|
|
1186
|
+
duration: this._videoDurationId,
|
|
1187
|
+
};
|
|
1188
|
+
if (cap?.resolutions?.length > 0 && this._videoResolutionId) {
|
|
1189
|
+
opts.resolution = this._videoResolutionId;
|
|
1190
|
+
}
|
|
1191
|
+
const sup = cap?.supports ?? {};
|
|
1192
|
+
if (sup.seed && this._videoSeed != null) opts.seed = this._videoSeed;
|
|
1193
|
+
if (sup.negativePrompt && this._videoNegativePrompt) opts.negativePrompt = this._videoNegativePrompt;
|
|
1194
|
+
if (sup.audio) opts.audio = this._videoAudio;
|
|
1195
|
+
if (sup.watermark) opts.watermark = this._videoWatermark;
|
|
1196
|
+
if (sup.cfgScale && this._videoCfgScale != null) opts.cfgScale = this._videoCfgScale;
|
|
1197
|
+
if (sup.personGeneration) opts.personGeneration = this._videoPersonGeneration;
|
|
1198
|
+
return opts;
|
|
401
1199
|
}
|
|
402
1200
|
|
|
403
1201
|
_render(state) {
|
|
@@ -413,6 +1211,8 @@ export class ChatWindow {
|
|
|
413
1211
|
this._formatMenu.refresh();
|
|
414
1212
|
this._updateFormatPillIcon();
|
|
415
1213
|
this._updateFormatPillLabel();
|
|
1214
|
+
this._resolutionMenu?.refresh();
|
|
1215
|
+
this._updateResolutionPillLabel();
|
|
416
1216
|
this._countMenu.refresh();
|
|
417
1217
|
this._updateCountPillIcon();
|
|
418
1218
|
this._composer.setStreaming(state.status === 'streaming');
|
|
@@ -754,14 +1554,31 @@ export class ChatWindow {
|
|
|
754
1554
|
|
|
755
1555
|
_getImageRequestOptions() {
|
|
756
1556
|
const [widthRatio, heightRatio] = parseFormatRatio(this._formatId);
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1557
|
+
const cap = getImageModelCapability(this._modelId);
|
|
1558
|
+
const provider = cap?.provider ?? 'yandex-art';
|
|
1559
|
+
const model = cap?.model;
|
|
1560
|
+
const imageCount = parseImageCount(this._countId);
|
|
1561
|
+
|
|
1562
|
+
const opts = { provider, widthRatio, heightRatio, model, imageCount };
|
|
1563
|
+
|
|
1564
|
+
// Качество (фиксировано для gpt-image-2-*)
|
|
1565
|
+
if (cap?.quality != null) opts.quality = cap.quality;
|
|
1566
|
+
|
|
1567
|
+
// Разрешение (только если модель поддерживает выбор)
|
|
1568
|
+
if (cap?.resolutions?.length > 0 && this._resolutionId) {
|
|
1569
|
+
opts.resolution = this._resolutionId;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// Доп-параметры — только поддерживаемые моделью
|
|
1573
|
+
const sup = cap?.supports ?? {};
|
|
1574
|
+
if (sup.seed && this._imageSeed != null) opts.seed = this._imageSeed;
|
|
1575
|
+
if (sup.negativePrompt && this._imageNegativePrompt) opts.negativePrompt = this._imageNegativePrompt;
|
|
1576
|
+
if (sup.background && this._imageBackground) opts.background = this._imageBackground;
|
|
1577
|
+
if (sup.outputFormat && this._imageOutputFormat) opts.outputFormat = this._imageOutputFormat;
|
|
1578
|
+
if (sup.promptExtend) opts.promptExtend = this._imagePromptExtend;
|
|
1579
|
+
if (sup.watermark) opts.watermark = this._imageWatermark;
|
|
1580
|
+
|
|
1581
|
+
return opts;
|
|
765
1582
|
}
|
|
766
1583
|
|
|
767
1584
|
_computePendingOverlayScreenLayout(messages, messageId, scale = 1) {
|
|
@@ -862,9 +1679,11 @@ export class ChatWindow {
|
|
|
862
1679
|
|
|
863
1680
|
const onPanUpdate = () => {
|
|
864
1681
|
this._syncPendingOverlaysToViewport({ disableTransition: true });
|
|
1682
|
+
this._sync3dSkeletonToViewport({ disableTransition: true });
|
|
865
1683
|
};
|
|
866
1684
|
const onViewportChange = () => {
|
|
867
1685
|
this._syncPendingOverlaysToViewport({ disableTransition: true, recomputeWorld: false });
|
|
1686
|
+
this._sync3dSkeletonToViewport({ disableTransition: true });
|
|
868
1687
|
// Зум меняет проекцию композера в мир: после него существующее изображение может
|
|
869
1688
|
// оказаться под заглушкой. Перепроверяем инвариант сразу, не дожидаясь рендера сессии.
|
|
870
1689
|
this._ensureExistingImagesClearOfPending();
|
|
@@ -1006,9 +1825,11 @@ export class ChatWindow {
|
|
|
1006
1825
|
const onSelectionRemove = (data) => {
|
|
1007
1826
|
const objectId = data?.object;
|
|
1008
1827
|
if (objectId) this._composer?.removeAttachmentForObject?.(objectId);
|
|
1828
|
+
this._update3dSendGate();
|
|
1009
1829
|
};
|
|
1010
1830
|
const onSelectionClear = () => {
|
|
1011
1831
|
this._composer?.removeAllBoardAttachments?.();
|
|
1832
|
+
this._update3dSendGate();
|
|
1012
1833
|
};
|
|
1013
1834
|
const onBoxSelectStart = () => {
|
|
1014
1835
|
this._boxSelectActive = true;
|
|
@@ -1139,8 +1960,8 @@ export class ChatWindow {
|
|
|
1139
1960
|
return null;
|
|
1140
1961
|
}
|
|
1141
1962
|
|
|
1142
|
-
_clampImageGroupAnchorY(y, referenceHeight) {
|
|
1143
|
-
const clearance = Math.round(referenceHeight / 2) + BOARD_IMAGE_LANE_UI_GAP;
|
|
1963
|
+
_clampImageGroupAnchorY(y, referenceHeight, reserveY = 0) {
|
|
1964
|
+
const clearance = Math.round(referenceHeight / 2) + BOARD_IMAGE_LANE_UI_GAP + reserveY;
|
|
1144
1965
|
|
|
1145
1966
|
const errorBlock = this._refs?.errorBlock;
|
|
1146
1967
|
if (errorBlock && errorBlock.classList.contains('is-visible')) {
|
|
@@ -1174,10 +1995,16 @@ export class ChatWindow {
|
|
|
1174
1995
|
const existingCenterY = this._getAiImageLaneCenterScreenY();
|
|
1175
1996
|
const [wr, hr] = parseFormatRatio(this._formatId);
|
|
1176
1997
|
const actualHeight = Math.round(BOARD_IMAGE_WIDTH / (wr / hr));
|
|
1177
|
-
|
|
1998
|
+
|
|
1999
|
+
let reserveY = 0;
|
|
2000
|
+
if (this._refs?.attachmentsPreview && !this._refs.attachmentsPreview.classList.contains('is-visible')) {
|
|
2001
|
+
reserveY = 74; // Резервируем место под ряд вложений (60px + padding)
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
let y = existingCenterY ?? (composerRect.top - reserveY - Math.round(actualHeight / 2) - BOARD_IMAGE_LANE_UI_GAP);
|
|
1178
2005
|
|
|
1179
2006
|
if (existingCenterY == null) {
|
|
1180
|
-
y = this._clampImageGroupAnchorY(y, actualHeight);
|
|
2007
|
+
y = this._clampImageGroupAnchorY(y, actualHeight, reserveY);
|
|
1181
2008
|
}
|
|
1182
2009
|
|
|
1183
2010
|
return {
|
|
@@ -1691,7 +2518,7 @@ function parseImageCount(countId) {
|
|
|
1691
2518
|
return 1;
|
|
1692
2519
|
}
|
|
1693
2520
|
|
|
1694
|
-
return Math.min(Math.max(count, 1),
|
|
2521
|
+
return Math.min(Math.max(count, 1), 6);
|
|
1695
2522
|
}
|
|
1696
2523
|
|
|
1697
2524
|
function findImageGenerationBatch(messages, messageId) {
|
|
@@ -1762,7 +2589,7 @@ function rectsOverlap(a, b) {
|
|
|
1762
2589
|
|
|
1763
2590
|
function isReferenceImageObject(object) {
|
|
1764
2591
|
return Boolean(object?.id)
|
|
1765
|
-
&& (object.type === 'image' || object.type === 'revit-screenshot-img')
|
|
2592
|
+
&& (object.type === 'image' || object.type === 'revit-screenshot-img' || object.type === 'model3d-screenshot-img')
|
|
1766
2593
|
&& typeof getImageObjectSource(object) === 'string';
|
|
1767
2594
|
}
|
|
1768
2595
|
|