@sequent-org/moodboard 1.4.47 → 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/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/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/styles/chat.css +62 -0
|
@@ -2,6 +2,9 @@ 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
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';
|
|
5
8
|
import { Events } from '../../core/events/Events.js';
|
|
6
9
|
|
|
7
10
|
import { buildChatDom } from './ChatWindowRenderer.js';
|
|
@@ -9,6 +12,8 @@ import { formatChatErrorForDisplay } from './formatChatError.js';
|
|
|
9
12
|
import { ChatMessageList } from './ChatMessageList.js';
|
|
10
13
|
import { ChatComposer } from './ChatComposer.js';
|
|
11
14
|
import { ChatPillMenu } from './ChatPillMenu.js';
|
|
15
|
+
import { ChatSettingsPopup } from './ChatSettingsPopup.js';
|
|
16
|
+
import { ChatVideoToolbarPills } from './ChatVideoToolbarPills.js';
|
|
12
17
|
import { ChatExtendedPromptModal } from './ChatExtendedPromptModal.js';
|
|
13
18
|
import { Model3dProgressOverlay } from './Model3dProgressOverlay.js';
|
|
14
19
|
import { Model3dBoardSkeleton } from './Model3dBoardSkeleton.js';
|
|
@@ -19,19 +24,19 @@ const CONTENT_TYPE_OPTIONS = [
|
|
|
19
24
|
id: 'image',
|
|
20
25
|
label: 'Изображение',
|
|
21
26
|
icon: ICONS.image,
|
|
22
|
-
description: '
|
|
27
|
+
description: 'По описанию или ссылке на референс'
|
|
23
28
|
},
|
|
24
29
|
{
|
|
25
30
|
id: 'video',
|
|
26
31
|
label: 'Видео',
|
|
27
32
|
icon: ICONS.video,
|
|
28
|
-
description: '
|
|
33
|
+
description: 'По описанию или по первому и последнему кадру'
|
|
29
34
|
},
|
|
30
35
|
{
|
|
31
36
|
id: '3d',
|
|
32
37
|
label: '3D-модель',
|
|
33
38
|
icon: ICONS.cube,
|
|
34
|
-
description: '
|
|
39
|
+
description: 'По выбранному изображению'
|
|
35
40
|
}
|
|
36
41
|
];
|
|
37
42
|
|
|
@@ -93,21 +98,27 @@ const FORMAT_OPTIONS = [
|
|
|
93
98
|
{ id: '2:3', label: '2:3', icon: RATIO_ICONS['2:3'] },
|
|
94
99
|
{ id: '9:16', label: '9:16', icon: RATIO_ICONS['9:16'] },
|
|
95
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'] },
|
|
96
103
|
{ id: '5:4', label: '5:4', icon: RATIO_ICONS['5:4'] },
|
|
97
104
|
{ id: '4:3', label: '4:3', icon: RATIO_ICONS['4:3'] },
|
|
98
105
|
{ id: '14:10', label: '14:10', icon: RATIO_ICONS['14:10'] },
|
|
99
106
|
{ id: '3:2', label: '3:2', icon: RATIO_ICONS['3:2'] },
|
|
100
107
|
{ id: '16:9', label: '16:9', icon: RATIO_ICONS['16:9'] },
|
|
101
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'] },
|
|
102
112
|
{ id: 'auto', label: 'Auto', icon: RATIO_ICONS['auto'] },
|
|
103
113
|
];
|
|
104
114
|
|
|
105
115
|
const COUNT_OPTIONS = [
|
|
106
|
-
{ id: 'auto', label: 'Авто', icon: COUNT_ICONS.auto },
|
|
107
116
|
{ id: '1', label: '1 Изображение', icon: COUNT_ICONS[1] },
|
|
108
117
|
{ id: '2', label: '2 Изображения', icon: COUNT_ICONS[2] },
|
|
109
118
|
{ id: '3', label: '3 Изображения', icon: COUNT_ICONS[3] },
|
|
110
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] },
|
|
111
122
|
];
|
|
112
123
|
|
|
113
124
|
const BOARD_IMAGE_WIDTH = 300;
|
|
@@ -124,38 +135,28 @@ const BOARD_IMAGE_LANE_REFERENCE_RATIO = [2, 3];
|
|
|
124
135
|
const BOARD_IMAGE_LANE_CENTER_OFFSET = 250;
|
|
125
136
|
const BOARD_IMAGE_LANE_UI_GAP = 16;
|
|
126
137
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
label: 'Алиса',
|
|
137
|
-
icon: ICONS.modelAlice,
|
|
138
|
-
description: 'YandexGPT'
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
id: 'gpt',
|
|
142
|
-
label: 'GPT',
|
|
143
|
-
icon: ICONS.modelGpt,
|
|
144
|
-
description: 'OpenAI'
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
id: 'google',
|
|
148
|
-
label: 'Google',
|
|
149
|
-
icon: ICONS.modelGoogle,
|
|
150
|
-
description: 'Gemini'
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
id: 'qwen',
|
|
154
|
-
label: 'Qwen',
|
|
155
|
-
icon: '<img src="/icons/qwen.svg" alt="" aria-hidden="true">',
|
|
156
|
-
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;
|
|
157
147
|
}
|
|
158
|
-
|
|
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
|
+
}
|
|
159
160
|
|
|
160
161
|
/**
|
|
161
162
|
* Корневой контейнер чата ИИ-ассистента.
|
|
@@ -193,13 +194,25 @@ export class ChatWindow {
|
|
|
193
194
|
this._extendedPromptModal = null;
|
|
194
195
|
this._contentTypeId = 'image';
|
|
195
196
|
this._contentTypeMenu = null;
|
|
196
|
-
this._modelId = '
|
|
197
|
+
this._modelId = 'nano-banana-pro';
|
|
197
198
|
this._modelMenu = null;
|
|
198
199
|
this._formatId = 'auto';
|
|
199
200
|
this._formatMenu = null;
|
|
200
201
|
this._providers = [];
|
|
201
|
-
this._countId = '
|
|
202
|
+
this._countId = '1';
|
|
202
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;
|
|
203
216
|
this._unsubscribe = null;
|
|
204
217
|
this._attached = false;
|
|
205
218
|
this._3dFaceCountId = '1m';
|
|
@@ -241,6 +254,23 @@ export class ChatWindow {
|
|
|
241
254
|
// приходит на каждый mousemove и не должен пушить превью в чат —
|
|
242
255
|
// финальный набор картинок мы получим из BoxSelectCommit по strict-contains.
|
|
243
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;
|
|
244
274
|
}
|
|
245
275
|
|
|
246
276
|
attach() {
|
|
@@ -264,10 +294,19 @@ export class ChatWindow {
|
|
|
264
294
|
if (this._contentTypeId === '3d') {
|
|
265
295
|
return this._submit3d(text, attachments);
|
|
266
296
|
}
|
|
297
|
+
if (this._contentTypeId === 'video') {
|
|
298
|
+
return this._submitVideo(text, attachments);
|
|
299
|
+
}
|
|
267
300
|
this._clearBoardSelection();
|
|
268
301
|
return this._session.send(text, { ...this._getImageRequestOptions(), referenceImages: attachments });
|
|
269
302
|
},
|
|
270
|
-
onAbort: () =>
|
|
303
|
+
onAbort: () => {
|
|
304
|
+
if (this._contentTypeId === 'video') {
|
|
305
|
+
this._videoSession?.abort();
|
|
306
|
+
} else {
|
|
307
|
+
this._session.abort();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
271
310
|
}
|
|
272
311
|
);
|
|
273
312
|
this._composer.attach();
|
|
@@ -298,6 +337,10 @@ export class ChatWindow {
|
|
|
298
337
|
this._contentTypeMenu.refresh();
|
|
299
338
|
this._update3dPillVisibility();
|
|
300
339
|
this._update3dSendGate();
|
|
340
|
+
this._modelMenu?.refresh();
|
|
341
|
+
this._formatMenu?.refresh();
|
|
342
|
+
this._updateResolutionPillVisibility();
|
|
343
|
+
this._settingsPopupCtrl?.refresh();
|
|
301
344
|
}
|
|
302
345
|
}
|
|
303
346
|
);
|
|
@@ -306,12 +349,24 @@ export class ChatWindow {
|
|
|
306
349
|
this._modelMenu = new ChatPillMenu(
|
|
307
350
|
{ trigger: this._refs.modelPill, menu: this._refs.modelMenu, label: this._refs.modelLabel, icon: this._refs.modelIcon },
|
|
308
351
|
{
|
|
309
|
-
getOptions: () =>
|
|
310
|
-
|
|
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,
|
|
311
359
|
onSelect: (id) => {
|
|
312
|
-
this.
|
|
360
|
+
if (this._contentTypeId === 'video') {
|
|
361
|
+
this._videoModelId = id;
|
|
362
|
+
this._clampVideoSettingsToModel();
|
|
363
|
+
} else {
|
|
364
|
+
this._modelId = id;
|
|
365
|
+
this._clampSettingsToModel();
|
|
366
|
+
}
|
|
313
367
|
this._modelMenu.refresh();
|
|
314
|
-
this.
|
|
368
|
+
this._updateResolutionPillVisibility();
|
|
369
|
+
this._settingsPopupCtrl?.refresh();
|
|
315
370
|
}
|
|
316
371
|
}
|
|
317
372
|
);
|
|
@@ -320,22 +375,52 @@ export class ChatWindow {
|
|
|
320
375
|
this._formatMenu = new ChatPillMenu(
|
|
321
376
|
{ trigger: this._refs.formatPill, menu: this._refs.formatMenu, label: this._refs.formatLabel },
|
|
322
377
|
{
|
|
323
|
-
getOptions: () => this._getSupportedFormatOptions(),
|
|
324
|
-
getActiveId: () => this._formatId,
|
|
378
|
+
getOptions: () => this._contentTypeId === 'video' ? this._getSupportedVideoFormatOptions() : this._getSupportedFormatOptions(),
|
|
379
|
+
getActiveId: () => this._contentTypeId === 'video' ? this._videoRatioId : this._formatId,
|
|
325
380
|
onSelect: (id) => {
|
|
326
|
-
this.
|
|
381
|
+
if (this._contentTypeId === 'video') {
|
|
382
|
+
this._videoRatioId = id;
|
|
383
|
+
} else {
|
|
384
|
+
this._formatId = id;
|
|
385
|
+
this._updateFormatPillIcon();
|
|
386
|
+
this._updateFormatPillLabel();
|
|
387
|
+
}
|
|
327
388
|
this._formatMenu.refresh();
|
|
328
|
-
this._updateFormatPillIcon();
|
|
329
|
-
this._updateFormatPillLabel();
|
|
330
389
|
}
|
|
331
390
|
}
|
|
332
391
|
);
|
|
333
392
|
this._formatMenu.attach();
|
|
334
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
|
+
|
|
335
420
|
this._countMenu = new ChatPillMenu(
|
|
336
421
|
{ trigger: this._refs.countPill, menu: this._refs.countMenu, label: this._refs.countLabel, icon: this._refs.countIcon },
|
|
337
422
|
{
|
|
338
|
-
getOptions: () =>
|
|
423
|
+
getOptions: () => this._getSupportedCountOptions(),
|
|
339
424
|
getActiveId: () => this._countId,
|
|
340
425
|
onSelect: (id) => {
|
|
341
426
|
this._countId = id;
|
|
@@ -346,7 +431,57 @@ export class ChatWindow {
|
|
|
346
431
|
);
|
|
347
432
|
this._countMenu.attach();
|
|
348
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
|
+
|
|
349
482
|
this._attach3dPills();
|
|
483
|
+
this._attachVideoPills();
|
|
484
|
+
this._attachVideoToolbarPills();
|
|
350
485
|
this._update3dPillVisibility();
|
|
351
486
|
|
|
352
487
|
const initialState = this._session.getState();
|
|
@@ -400,12 +535,25 @@ export class ChatWindow {
|
|
|
400
535
|
this._3dResultFormatMenu = null;
|
|
401
536
|
this._3dResultFormatWrapper?.remove();
|
|
402
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;
|
|
403
547
|
this._composer?.destroy();
|
|
404
548
|
this._extendedPromptModal?.destroy();
|
|
405
549
|
this._contentTypeMenu?.destroy();
|
|
406
550
|
this._modelMenu?.destroy();
|
|
407
551
|
this._formatMenu?.destroy();
|
|
552
|
+
this._resolutionMenu?.destroy();
|
|
553
|
+
this._resolutionMenu = null;
|
|
408
554
|
this._countMenu?.destroy();
|
|
555
|
+
this._settingsPopupCtrl?.destroy();
|
|
556
|
+
this._settingsPopupCtrl = null;
|
|
409
557
|
if (this._refs?.root && this._refs.root.parentNode === this._container) {
|
|
410
558
|
this._container.removeChild(this._refs.root);
|
|
411
559
|
}
|
|
@@ -520,12 +668,20 @@ export class ChatWindow {
|
|
|
520
668
|
|
|
521
669
|
_update3dPillVisibility() {
|
|
522
670
|
const is3d = this._contentTypeId === '3d';
|
|
671
|
+
const isVideo = this._contentTypeId === 'video';
|
|
523
672
|
const formatWrapper = this._refs.formatPill?.closest('.moodboard-chat__pill-wrapper');
|
|
524
673
|
const countWrapper = this._refs.countPill?.closest('.moodboard-chat__pill-wrapper');
|
|
525
674
|
const modelWrapper = this._refs.modelPill?.closest('.moodboard-chat__pill-wrapper');
|
|
526
675
|
if (formatWrapper) formatWrapper.style.display = is3d ? 'none' : '';
|
|
527
|
-
|
|
676
|
+
// В видео-режиме счётчик не нужен: у всех видео-моделей maxCount=1
|
|
677
|
+
if (countWrapper) countWrapper.style.display = (is3d || isVideo) ? 'none' : '';
|
|
528
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
|
+
}
|
|
529
685
|
if (this._3dFaceCountWrapper) this._3dFaceCountWrapper.style.display = is3d ? '' : 'none';
|
|
530
686
|
if (this._3dTypeWrapper) this._3dTypeWrapper.style.display = is3d ? '' : 'none';
|
|
531
687
|
if (this._3dModeWrapper) this._3dModeWrapper.style.display = is3d ? '' : 'none';
|
|
@@ -534,6 +690,10 @@ export class ChatWindow {
|
|
|
534
690
|
if (this._3dTextureStyleWrapper) {
|
|
535
691
|
this._3dTextureStyleWrapper.style.display = (is3d && this._3dMode === 'text') ? '' : 'none';
|
|
536
692
|
}
|
|
693
|
+
// Длительность — только в видео-режиме
|
|
694
|
+
if (this._videoDurationWrapper) this._videoDurationWrapper.style.display = isVideo ? '' : 'none';
|
|
695
|
+
// Видео-настройки (Звук/Водяной знак/Люди/Seed/CFG/Негатив) — пилюли тулбара
|
|
696
|
+
this._videoToolbarPills?.refresh();
|
|
537
697
|
}
|
|
538
698
|
|
|
539
699
|
_update3dSendGate() {
|
|
@@ -780,7 +940,9 @@ export class ChatWindow {
|
|
|
780
940
|
const list = await this._aiClient.listProviders();
|
|
781
941
|
this._providers = Array.isArray(list) ? list : [];
|
|
782
942
|
this._session.setAvailableProviders(list);
|
|
783
|
-
|
|
943
|
+
// Серверные supportedRatios могут дополнительно сузить список —
|
|
944
|
+
// после загрузки ещё раз клампим настройки на случай несоответствия.
|
|
945
|
+
this._clampSettingsToModel();
|
|
784
946
|
} catch (err) {
|
|
785
947
|
console.warn('[ChatWindow] cannot load providers:', err.message);
|
|
786
948
|
this._providers = [];
|
|
@@ -789,52 +951,251 @@ export class ChatWindow {
|
|
|
789
951
|
}
|
|
790
952
|
|
|
791
953
|
/**
|
|
792
|
-
* Возвращает
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
* @param {string} modelId
|
|
796
|
-
* @returns {string}
|
|
797
|
-
*/
|
|
798
|
-
_imageProviderForModel(modelId) {
|
|
799
|
-
return modelId === 'gpt' ? 'openai-image' : 'yandex-art';
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* Возвращает подмножество FORMAT_OPTIONS, поддерживаемое текущим провайдером.
|
|
804
|
-
* Если провайдер не ограничивает форматы (supportedRatios === null) — возвращает все.
|
|
954
|
+
* Возвращает подмножество FORMAT_OPTIONS, поддерживаемое текущей моделью.
|
|
955
|
+
* Источник истины — capability.ratios из imageModelCapabilities.
|
|
805
956
|
* Формат 'auto' присутствует всегда.
|
|
806
957
|
*
|
|
807
958
|
* @returns {Array}
|
|
808
959
|
*/
|
|
809
960
|
_getSupportedFormatOptions() {
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
const ratios = provider?.supportedRatios;
|
|
813
|
-
|
|
814
|
-
if (!Array.isArray(ratios) || ratios.length === 0) {
|
|
961
|
+
const cap = getImageModelCapability(this._modelId);
|
|
962
|
+
if (!cap || !Array.isArray(cap.ratios) || cap.ratios.length === 0) {
|
|
815
963
|
return FORMAT_OPTIONS;
|
|
816
964
|
}
|
|
965
|
+
return FORMAT_OPTIONS.filter((opt) => opt.id === 'auto' || cap.ratios.includes(opt.id));
|
|
966
|
+
}
|
|
817
967
|
|
|
818
|
-
|
|
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
|
+
});
|
|
819
981
|
}
|
|
820
982
|
|
|
821
983
|
/**
|
|
822
|
-
* При смене модели
|
|
823
|
-
*
|
|
984
|
+
* При смене модели приводит текущий формат, разрешение и количество
|
|
985
|
+
* к допустимым значениям. Невалидное значение — на дефолт.
|
|
824
986
|
*/
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
987
|
+
_clampSettingsToModel() {
|
|
988
|
+
const cap = getImageModelCapability(this._modelId);
|
|
989
|
+
|
|
990
|
+
// Формат
|
|
991
|
+
const supportedFormats = this._getSupportedFormatOptions();
|
|
992
|
+
if (!supportedFormats.some((opt) => opt.id === this._formatId)) {
|
|
829
993
|
this._formatId = 'auto';
|
|
830
994
|
}
|
|
831
|
-
// refresh обновляет лейбл из option.label ('Auto'), поэтому _updateFormatPillLabel
|
|
832
|
-
// вызывается после — она выставляет человекочитаемый текст 'Соотношение сторон'.
|
|
833
995
|
this._formatMenu?.refresh();
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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];
|
|
837
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;
|
|
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;
|
|
838
1199
|
}
|
|
839
1200
|
|
|
840
1201
|
_render(state) {
|
|
@@ -850,6 +1211,8 @@ export class ChatWindow {
|
|
|
850
1211
|
this._formatMenu.refresh();
|
|
851
1212
|
this._updateFormatPillIcon();
|
|
852
1213
|
this._updateFormatPillLabel();
|
|
1214
|
+
this._resolutionMenu?.refresh();
|
|
1215
|
+
this._updateResolutionPillLabel();
|
|
853
1216
|
this._countMenu.refresh();
|
|
854
1217
|
this._updateCountPillIcon();
|
|
855
1218
|
this._composer.setStreaming(state.status === 'streaming');
|
|
@@ -1191,14 +1554,31 @@ export class ChatWindow {
|
|
|
1191
1554
|
|
|
1192
1555
|
_getImageRequestOptions() {
|
|
1193
1556
|
const [widthRatio, heightRatio] = parseFormatRatio(this._formatId);
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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;
|
|
1202
1582
|
}
|
|
1203
1583
|
|
|
1204
1584
|
_computePendingOverlayScreenLayout(messages, messageId, scale = 1) {
|
|
@@ -2138,7 +2518,7 @@ function parseImageCount(countId) {
|
|
|
2138
2518
|
return 1;
|
|
2139
2519
|
}
|
|
2140
2520
|
|
|
2141
|
-
return Math.min(Math.max(count, 1),
|
|
2521
|
+
return Math.min(Math.max(count, 1), 6);
|
|
2142
2522
|
}
|
|
2143
2523
|
|
|
2144
2524
|
function findImageGenerationBatch(messages, messageId) {
|