@sequent-org/moodboard 1.4.41 → 1.4.43
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 +4 -1
- package/src/ui/chat/ChatWindow.js +131 -17
- package/src/ui/chat/icons.js +5 -0
package/package.json
CHANGED
|
@@ -28,7 +28,10 @@ export class AiClient {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Список доступных провайдеров.
|
|
31
|
-
*
|
|
31
|
+
*
|
|
32
|
+
* @returns {Promise<Array<{id: string, label: string, enabled: boolean, supportedRatios: string[]|null}>>}
|
|
33
|
+
* supportedRatios — массив id форматов из FORMAT_OPTIONS (например ['1:1','3:2','2:3']),
|
|
34
|
+
* либо null, если провайдер не ограничивает доступные соотношения сторон.
|
|
32
35
|
*/
|
|
33
36
|
async listProviders() {
|
|
34
37
|
const res = await this._fetch(`${this._baseUrl}/providers`, {
|
|
@@ -26,7 +26,7 @@ const CONTENT_TYPE_OPTIONS = [
|
|
|
26
26
|
}
|
|
27
27
|
];
|
|
28
28
|
|
|
29
|
-
/** Порядок: портрет (строка 1),
|
|
29
|
+
/** Порядок: портрет (строка 1), альбом (строка 2), авто — крайнее правое */
|
|
30
30
|
const FORMAT_OPTIONS = [
|
|
31
31
|
{ id: '1:1', label: '1:1', icon: RATIO_ICONS['1:1'] },
|
|
32
32
|
{ id: '4:5', label: '4:5', icon: RATIO_ICONS['4:5'] },
|
|
@@ -35,13 +35,13 @@ const FORMAT_OPTIONS = [
|
|
|
35
35
|
{ id: '2:3', label: '2:3', icon: RATIO_ICONS['2:3'] },
|
|
36
36
|
{ id: '9:16', label: '9:16', icon: RATIO_ICONS['9:16'] },
|
|
37
37
|
{ id: '1:2', label: '1:2', icon: RATIO_ICONS['1:2'] },
|
|
38
|
-
{ id: 'auto', label: 'Auto', icon: RATIO_ICONS['auto'] },
|
|
39
38
|
{ id: '5:4', label: '5:4', icon: RATIO_ICONS['5:4'] },
|
|
40
39
|
{ id: '4:3', label: '4:3', icon: RATIO_ICONS['4:3'] },
|
|
41
40
|
{ id: '14:10', label: '14:10', icon: RATIO_ICONS['14:10'] },
|
|
42
41
|
{ id: '3:2', label: '3:2', icon: RATIO_ICONS['3:2'] },
|
|
43
42
|
{ id: '16:9', label: '16:9', icon: RATIO_ICONS['16:9'] },
|
|
44
43
|
{ id: '2:1', label: '2:1', icon: RATIO_ICONS['2:1'] },
|
|
44
|
+
{ id: 'auto', label: 'Auto', icon: RATIO_ICONS['auto'] },
|
|
45
45
|
];
|
|
46
46
|
|
|
47
47
|
const COUNT_OPTIONS = [
|
|
@@ -61,12 +61,16 @@ const BOARD_IMAGE_REARRANGE_MS = 520;
|
|
|
61
61
|
const BOARD_IMAGE_PENDING_ENTER_FACTOR = 1.6;
|
|
62
62
|
// Каскад между блоками одного батча (мс): пользователь видит, что они приезжают друг за другом.
|
|
63
63
|
const BOARD_IMAGE_PENDING_STAGGER_MS = 90;
|
|
64
|
+
// Референсная высота ряда AI-картинок: центры выравниваются по одной оси независимо от формата.
|
|
65
|
+
const BOARD_IMAGE_LANE_REFERENCE_RATIO = [2, 3];
|
|
66
|
+
const BOARD_IMAGE_LANE_CENTER_OFFSET = 250;
|
|
67
|
+
const BOARD_IMAGE_LANE_UI_GAP = 16;
|
|
64
68
|
|
|
65
69
|
const MODEL_OPTIONS = [
|
|
66
70
|
{
|
|
67
71
|
id: 'auto',
|
|
68
72
|
label: 'Автоматический режим',
|
|
69
|
-
icon: ICONS.
|
|
73
|
+
icon: ICONS.modelBot,
|
|
70
74
|
description: 'Мы подберем модель для ваших задач.'
|
|
71
75
|
},
|
|
72
76
|
{
|
|
@@ -135,6 +139,7 @@ export class ChatWindow {
|
|
|
135
139
|
this._modelMenu = null;
|
|
136
140
|
this._formatId = 'auto';
|
|
137
141
|
this._formatMenu = null;
|
|
142
|
+
this._providers = [];
|
|
138
143
|
this._countId = 'auto';
|
|
139
144
|
this._countMenu = null;
|
|
140
145
|
this._unsubscribe = null;
|
|
@@ -219,6 +224,7 @@ export class ChatWindow {
|
|
|
219
224
|
onSelect: (id) => {
|
|
220
225
|
this._modelId = id;
|
|
221
226
|
this._modelMenu.refresh();
|
|
227
|
+
this._clampFormatToModel();
|
|
222
228
|
}
|
|
223
229
|
}
|
|
224
230
|
);
|
|
@@ -227,7 +233,7 @@ export class ChatWindow {
|
|
|
227
233
|
this._formatMenu = new ChatPillMenu(
|
|
228
234
|
{ trigger: this._refs.formatPill, menu: this._refs.formatMenu, label: this._refs.formatLabel },
|
|
229
235
|
{
|
|
230
|
-
getOptions: () =>
|
|
236
|
+
getOptions: () => this._getSupportedFormatOptions(),
|
|
231
237
|
getActiveId: () => this._formatId,
|
|
232
238
|
onSelect: (id) => {
|
|
233
239
|
this._formatId = id;
|
|
@@ -332,13 +338,65 @@ export class ChatWindow {
|
|
|
332
338
|
async _loadProviders() {
|
|
333
339
|
try {
|
|
334
340
|
const list = await this._aiClient.listProviders();
|
|
341
|
+
this._providers = Array.isArray(list) ? list : [];
|
|
335
342
|
this._session.setAvailableProviders(list);
|
|
343
|
+
this._clampFormatToModel();
|
|
336
344
|
} catch (err) {
|
|
337
345
|
console.warn('[ChatWindow] cannot load providers:', err.message);
|
|
346
|
+
this._providers = [];
|
|
338
347
|
this._session.setAvailableProviders([]);
|
|
339
348
|
}
|
|
340
349
|
}
|
|
341
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Возвращает id провайдера изображений для текущей модели.
|
|
353
|
+
* Дублирует логику из _getImageRequestOptions, чтобы не привязываться к payload-контексту.
|
|
354
|
+
*
|
|
355
|
+
* @param {string} modelId
|
|
356
|
+
* @returns {string}
|
|
357
|
+
*/
|
|
358
|
+
_imageProviderForModel(modelId) {
|
|
359
|
+
return modelId === 'gpt' ? 'openai-image' : 'yandex-art';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Возвращает подмножество FORMAT_OPTIONS, поддерживаемое текущим провайдером.
|
|
364
|
+
* Если провайдер не ограничивает форматы (supportedRatios === null) — возвращает все.
|
|
365
|
+
* Формат 'auto' присутствует всегда.
|
|
366
|
+
*
|
|
367
|
+
* @returns {Array}
|
|
368
|
+
*/
|
|
369
|
+
_getSupportedFormatOptions() {
|
|
370
|
+
const providerId = this._imageProviderForModel(this._modelId);
|
|
371
|
+
const provider = this._providers.find((p) => p.id === providerId);
|
|
372
|
+
const ratios = provider?.supportedRatios;
|
|
373
|
+
|
|
374
|
+
if (!Array.isArray(ratios) || ratios.length === 0) {
|
|
375
|
+
return FORMAT_OPTIONS;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return FORMAT_OPTIONS.filter((opt) => opt.id === 'auto' || ratios.includes(opt.id));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* При смене модели проверяет, что текущий формат входит в список поддерживаемых.
|
|
383
|
+
* Если нет — сбрасывает на 'auto'.
|
|
384
|
+
*/
|
|
385
|
+
_clampFormatToModel() {
|
|
386
|
+
const supported = this._getSupportedFormatOptions();
|
|
387
|
+
const stillValid = supported.some((opt) => opt.id === this._formatId);
|
|
388
|
+
if (!stillValid) {
|
|
389
|
+
this._formatId = 'auto';
|
|
390
|
+
}
|
|
391
|
+
// refresh обновляет лейбл из option.label ('Auto'), поэтому _updateFormatPillLabel
|
|
392
|
+
// вызывается после — она выставляет человекочитаемый текст 'Соотношение сторон'.
|
|
393
|
+
this._formatMenu?.refresh();
|
|
394
|
+
if (!stillValid) {
|
|
395
|
+
this._updateFormatPillIcon();
|
|
396
|
+
this._updateFormatPillLabel();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
342
400
|
_render(state) {
|
|
343
401
|
if (!this._attached && !this._refs) return;
|
|
344
402
|
this._syncGeneratedImagesToBoard(state.messages);
|
|
@@ -894,23 +952,74 @@ export class ChatWindow {
|
|
|
894
952
|
};
|
|
895
953
|
}
|
|
896
954
|
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
955
|
+
_getAiImageLaneCenterScreenY(scale = 1) {
|
|
956
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
957
|
+
const s = scale || world?.scale?.x || 1;
|
|
958
|
+
const worldOffsetY = world?.y || 0;
|
|
959
|
+
|
|
960
|
+
for (const record of this._pendingOverlays.values()) {
|
|
961
|
+
if (Number.isFinite(record.worldY)) {
|
|
962
|
+
return Math.round(record.worldY * s + worldOffsetY);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
for (const object of this._getBoardAiImageObjects()) {
|
|
967
|
+
if (!object?.position) continue;
|
|
968
|
+
|
|
969
|
+
const height = getBoardObjectHeight(object);
|
|
970
|
+
return Math.round((object.position.y + height / 2) * s + worldOffsetY);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
for (const slot of this._aiImageLaneSlots.values()) {
|
|
974
|
+
if (slot && Number.isFinite(slot.y) && Number.isFinite(slot.height)) {
|
|
975
|
+
return Math.round((slot.y + slot.height / 2) * s + worldOffsetY);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
_clampImageGroupAnchorY(y, referenceHeight) {
|
|
983
|
+
const clearance = Math.round(referenceHeight / 2) + BOARD_IMAGE_LANE_UI_GAP;
|
|
984
|
+
|
|
985
|
+
const errorBlock = this._refs?.errorBlock;
|
|
986
|
+
if (errorBlock && errorBlock.classList.contains('is-visible')) {
|
|
987
|
+
const errorRect = errorBlock.getBoundingClientRect();
|
|
988
|
+
if (errorRect.height > 0) {
|
|
989
|
+
const minY = errorRect.top - clearance;
|
|
990
|
+
if (y > minY) {
|
|
991
|
+
y = minY;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const statusBar = this._refs?.statusBar;
|
|
997
|
+
if (statusBar && statusBar.classList.contains('is-visible')) {
|
|
998
|
+
const statusRect = statusBar.getBoundingClientRect();
|
|
999
|
+
// jsdom отдаёт нулевой rect — без реальной геометрии не сдвигаем ряд.
|
|
1000
|
+
if (statusRect.height > 0 && statusRect.top > 0) {
|
|
1001
|
+
const minY = statusRect.top - clearance;
|
|
909
1002
|
if (y > minY) {
|
|
910
1003
|
y = minY;
|
|
911
1004
|
}
|
|
912
1005
|
}
|
|
913
|
-
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return y;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
_getImageGroupAnchor() {
|
|
1012
|
+
const composerRect = this._refs?.composer?.getBoundingClientRect?.();
|
|
1013
|
+
if (composerRect) {
|
|
1014
|
+
const existingCenterY = this._getAiImageLaneCenterScreenY();
|
|
1015
|
+
const [wr, hr] = parseFormatRatio(this._formatId);
|
|
1016
|
+
const actualHeight = Math.round(BOARD_IMAGE_WIDTH / (wr / hr));
|
|
1017
|
+
let y = existingCenterY ?? (composerRect.top - Math.round(actualHeight / 2) - BOARD_IMAGE_LANE_UI_GAP);
|
|
1018
|
+
|
|
1019
|
+
if (existingCenterY == null) {
|
|
1020
|
+
y = this._clampImageGroupAnchorY(y, actualHeight);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
914
1023
|
return {
|
|
915
1024
|
x: Math.round(composerRect.left + composerRect.width / 2),
|
|
916
1025
|
y: Math.round(y)
|
|
@@ -1386,6 +1495,11 @@ export class ChatWindow {
|
|
|
1386
1495
|
}
|
|
1387
1496
|
}
|
|
1388
1497
|
|
|
1498
|
+
function getBoardImageReferenceHeight() {
|
|
1499
|
+
const [widthRatio, heightRatio] = BOARD_IMAGE_LANE_REFERENCE_RATIO;
|
|
1500
|
+
return Math.round(BOARD_IMAGE_WIDTH / (widthRatio / heightRatio));
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1389
1503
|
function parseFormatRatio(formatId) {
|
|
1390
1504
|
if (!formatId || formatId === 'auto') {
|
|
1391
1505
|
return [1, 1];
|
package/src/ui/chat/icons.js
CHANGED
|
@@ -54,7 +54,12 @@ const MODEL_QWEN_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" heig
|
|
|
54
54
|
/** Placeholder Yandex Alice — буква А в круге, 36×36 */
|
|
55
55
|
const MODEL_ALICE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true"><circle cx="18" cy="18" r="16" fill="#fc3f1d"/><text x="18" y="23" text-anchor="middle" font-size="16" font-family="Arial,sans-serif" fill="#fff" font-weight="bold">А</text></svg>`;
|
|
56
56
|
|
|
57
|
+
/** Иконка «Автоматический режим» — робот, 36×36, outline */
|
|
58
|
+
const MODEL_BOT_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>`;
|
|
59
|
+
|
|
57
60
|
export const ICONS = {
|
|
61
|
+
bot: svg('<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>'),
|
|
62
|
+
modelBot: MODEL_BOT_ICON,
|
|
58
63
|
image: IMAGE_ICON,
|
|
59
64
|
video: VIDEO_ICON,
|
|
60
65
|
bolt: svg('<path d="M13 3L4 14h6l-1 7 9-11h-6l1-7z"/>'),
|