@sequent-org/moodboard 1.4.28 → 1.4.30
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
CHANGED
|
@@ -43,7 +43,7 @@ export class ChatSessionController {
|
|
|
43
43
|
this._abort = null;
|
|
44
44
|
|
|
45
45
|
this._state = {
|
|
46
|
-
messages: this._history.load(),
|
|
46
|
+
messages: this._history.load().map((m) => (m.pending ? { ...m, pending: false, error: m.error || 'Прервано' } : m)),
|
|
47
47
|
providerId: 'yandex-art',
|
|
48
48
|
presetId: DEFAULT_PRESET_ID,
|
|
49
49
|
settings: this._loadSettings(),
|
|
@@ -112,18 +112,23 @@ export class ChatSessionController {
|
|
|
112
112
|
/**
|
|
113
113
|
* Отправляет user-сообщение и создаёт изображение через YandexART.
|
|
114
114
|
* @param {string} text
|
|
115
|
-
* @param {{widthRatio?: number, heightRatio?: number, model?: string}} [options]
|
|
115
|
+
* @param {{widthRatio?: number, heightRatio?: number, model?: string, imageCount?: number}} [options]
|
|
116
116
|
*/
|
|
117
117
|
async send(text, options = {}) {
|
|
118
118
|
const trimmed = (text || '').trim();
|
|
119
119
|
if (!trimmed || this._state.status === 'streaming') return;
|
|
120
120
|
|
|
121
|
+
const imageCount = normalizeImageCount(options.imageCount);
|
|
121
122
|
const userMsg = makeMessage('user', trimmed);
|
|
122
|
-
const
|
|
123
|
+
const assistantMsgs = Array.from({ length: imageCount }, (_, index) => makeMessage(
|
|
124
|
+
'assistant',
|
|
125
|
+
imageCount > 1 ? `Генерируется изображение ${index + 1} из ${imageCount}…` : '',
|
|
126
|
+
{ provider: 'yandex-art', pending: true, kind: 'image' }
|
|
127
|
+
));
|
|
123
128
|
|
|
124
129
|
this._state = {
|
|
125
130
|
...this._state,
|
|
126
|
-
messages: [...this._state.messages, userMsg,
|
|
131
|
+
messages: [...this._state.messages, userMsg, ...assistantMsgs],
|
|
127
132
|
status: 'streaming',
|
|
128
133
|
error: null
|
|
129
134
|
};
|
|
@@ -132,31 +137,54 @@ export class ChatSessionController {
|
|
|
132
137
|
|
|
133
138
|
const abort = new AbortController();
|
|
134
139
|
this._abort = abort;
|
|
140
|
+
let lastError = null;
|
|
135
141
|
|
|
136
142
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
143
|
+
await Promise.all(
|
|
144
|
+
assistantMsgs.map((assistantMsg, index) => {
|
|
145
|
+
if (abort.signal.aborted) {
|
|
146
|
+
lastError = 'Отменено';
|
|
147
|
+
this._updateAssistant(assistantMsg.id, { error: lastError });
|
|
148
|
+
return Promise.resolve();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return this._client
|
|
152
|
+
.generateImage({
|
|
153
|
+
prompt: trimmed,
|
|
154
|
+
widthRatio: options.widthRatio,
|
|
155
|
+
heightRatio: options.heightRatio,
|
|
156
|
+
model: options.model,
|
|
157
|
+
signal: abort.signal
|
|
158
|
+
})
|
|
159
|
+
.then((result) => {
|
|
160
|
+
this._updateAssistant(assistantMsg.id, {
|
|
161
|
+
error: null,
|
|
162
|
+
imageBase64: result.imageBase64,
|
|
163
|
+
mimeType: result.mimeType,
|
|
164
|
+
operationId: result.operationId,
|
|
165
|
+
content: imageCount > 1 ? `Изображение ${index + 1} из ${imageCount} добавлено на доску.` : ''
|
|
166
|
+
});
|
|
167
|
+
})
|
|
168
|
+
.catch((err) => {
|
|
169
|
+
const msg = err?.name === 'AbortError' ? 'Отменено' : (err?.message || 'Ошибка запроса');
|
|
170
|
+
lastError = msg;
|
|
171
|
+
this._updateAssistant(assistantMsg.id, { error: msg });
|
|
172
|
+
});
|
|
173
|
+
})
|
|
174
|
+
);
|
|
154
175
|
} finally {
|
|
155
176
|
this._abort = null;
|
|
177
|
+
this._state = {
|
|
178
|
+
...this._state,
|
|
179
|
+
status: lastError ? 'error' : 'idle',
|
|
180
|
+
error: lastError
|
|
181
|
+
};
|
|
182
|
+
this._history.save(this._state.messages);
|
|
183
|
+
this._emit();
|
|
156
184
|
}
|
|
157
185
|
}
|
|
158
186
|
|
|
159
|
-
|
|
187
|
+
_updateAssistant(id, { error, imageBase64, mimeType, operationId, content }) {
|
|
160
188
|
const messages = this._state.messages.map((m) =>
|
|
161
189
|
m.id === id
|
|
162
190
|
? {
|
|
@@ -165,15 +193,14 @@ export class ChatSessionController {
|
|
|
165
193
|
error: error || undefined,
|
|
166
194
|
imageBase64: imageBase64 || m.imageBase64,
|
|
167
195
|
mimeType: mimeType || m.mimeType,
|
|
168
|
-
operationId: operationId || m.operationId
|
|
196
|
+
operationId: operationId || m.operationId,
|
|
197
|
+
content: content ?? m.content
|
|
169
198
|
}
|
|
170
199
|
: m
|
|
171
200
|
);
|
|
172
201
|
this._state = {
|
|
173
202
|
...this._state,
|
|
174
|
-
messages
|
|
175
|
-
status: error ? 'error' : 'idle',
|
|
176
|
-
error: error || null
|
|
203
|
+
messages
|
|
177
204
|
};
|
|
178
205
|
this._history.save(messages);
|
|
179
206
|
this._emit();
|
|
@@ -218,3 +245,12 @@ function makeMessage(role, content, extra = {}) {
|
|
|
218
245
|
...extra
|
|
219
246
|
};
|
|
220
247
|
}
|
|
248
|
+
|
|
249
|
+
function normalizeImageCount(value) {
|
|
250
|
+
const count = Number.parseInt(value, 10);
|
|
251
|
+
if (!Number.isFinite(count)) {
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return Math.min(Math.max(count, 1), 4);
|
|
256
|
+
}
|
|
@@ -77,6 +77,13 @@ export class ChatComposer {
|
|
|
77
77
|
this._textarea.focus();
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
addAttachment(file) {
|
|
81
|
+
if (!file) return;
|
|
82
|
+
this._attachments.push(file);
|
|
83
|
+
this._renderAttachmentsPreview();
|
|
84
|
+
this._refreshSendState();
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
destroy() {
|
|
81
88
|
for (const off of this._listeners) off();
|
|
82
89
|
this._listeners = [];
|
|
@@ -51,6 +51,13 @@ const COUNT_OPTIONS = [
|
|
|
51
51
|
{ id: '4', label: '4 Изображения', icon: COUNT_ICONS[4] },
|
|
52
52
|
];
|
|
53
53
|
|
|
54
|
+
const BOARD_IMAGE_WIDTH = 300;
|
|
55
|
+
const BOARD_IMAGE_STEP = 320;
|
|
56
|
+
const BOARD_IMAGE_GAP = BOARD_IMAGE_STEP - BOARD_IMAGE_WIDTH;
|
|
57
|
+
// Скорость перестановки AI-изображений на доске и въезда заглушек регулируется здесь.
|
|
58
|
+
const BOARD_IMAGE_REARRANGE_MS = 520;
|
|
59
|
+
const REFERENCE_DRAG_PREVIEW_SIZE = 96;
|
|
60
|
+
|
|
54
61
|
const MODEL_OPTIONS = [
|
|
55
62
|
{
|
|
56
63
|
id: 'auto',
|
|
@@ -129,14 +136,15 @@ export class ChatWindow {
|
|
|
129
136
|
this._unsubscribe = null;
|
|
130
137
|
this._attached = false;
|
|
131
138
|
this._boardImageMessageIds = new Set();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
this._shiftedForImageBatchKeys = new Set();
|
|
140
|
+
this._pendingOverlayEls = [];
|
|
141
|
+
this._pendingOverlayMessageIds = new Set();
|
|
142
|
+
this._boardImageShiftAnimations = new Map();
|
|
143
|
+
this._boardCursor = null;
|
|
144
|
+
this._draggedReferenceObject = null;
|
|
145
|
+
this._draggedReferenceStartPosition = null;
|
|
146
|
+
this._referenceDragPreview = null;
|
|
147
|
+
this._referenceDragHandlers = null;
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
attach() {
|
|
@@ -162,6 +170,7 @@ export class ChatWindow {
|
|
|
162
170
|
}
|
|
163
171
|
);
|
|
164
172
|
this._composer.attach();
|
|
173
|
+
this._attachReferenceDragEvents();
|
|
165
174
|
|
|
166
175
|
this._extendedPromptModal = new ChatExtendedPromptModal(
|
|
167
176
|
this._container,
|
|
@@ -230,8 +239,6 @@ export class ChatWindow {
|
|
|
230
239
|
);
|
|
231
240
|
this._countMenu.attach();
|
|
232
241
|
|
|
233
|
-
this._boardCore?.eventBus?.on?.(Events.Object.Created, this._onBoardObjectCreated);
|
|
234
|
-
|
|
235
242
|
const initialState = this._session.getState();
|
|
236
243
|
this._markExistingBoardImages(initialState.messages);
|
|
237
244
|
this._unsubscribe = this._session.subscribe((state) => this._render(state));
|
|
@@ -244,9 +251,13 @@ export class ChatWindow {
|
|
|
244
251
|
|
|
245
252
|
detach() {
|
|
246
253
|
if (!this._attached) return;
|
|
254
|
+
this._clearPendingOverlays();
|
|
255
|
+
this._cancelBoardImageShiftAnimations();
|
|
256
|
+
this._clearReferenceDragState();
|
|
247
257
|
if (this._unsubscribe) { this._unsubscribe(); this._unsubscribe = null; }
|
|
248
|
-
this.
|
|
249
|
-
this.
|
|
258
|
+
this._detachReferenceDragEvents();
|
|
259
|
+
this._shiftedForImageBatchKeys.clear();
|
|
260
|
+
this._pendingOverlayMessageIds.clear();
|
|
250
261
|
this._composer?.destroy();
|
|
251
262
|
this._extendedPromptModal?.destroy();
|
|
252
263
|
this._contentTypeMenu?.destroy();
|
|
@@ -312,6 +323,61 @@ export class ChatWindow {
|
|
|
312
323
|
this._countMenu.refresh();
|
|
313
324
|
this._updateCountPillIcon();
|
|
314
325
|
this._composer.setStreaming(state.status === 'streaming');
|
|
326
|
+
this._updatePendingImages(state.status === 'streaming' ? state.messages : []);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_updatePendingImages(messages) {
|
|
330
|
+
this._clearPendingOverlays();
|
|
331
|
+
|
|
332
|
+
const pending = (messages || []).filter((m) => m.pending && m.kind === 'image');
|
|
333
|
+
if (pending.length === 0) return;
|
|
334
|
+
|
|
335
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
336
|
+
const s = world?.scale?.x || 1;
|
|
337
|
+
|
|
338
|
+
this._shiftExistingImagesForBatch(messages, pending[0].id, s);
|
|
339
|
+
|
|
340
|
+
const [wr, hr] = parseFormatRatio(this._formatId);
|
|
341
|
+
const ratio = wr / hr;
|
|
342
|
+
const wScreen = Math.round(BOARD_IMAGE_WIDTH * s);
|
|
343
|
+
const hScreen = Math.round(wScreen / ratio);
|
|
344
|
+
|
|
345
|
+
for (const message of pending) {
|
|
346
|
+
const slot = this._getImageBatchSlot(messages, message.id, s);
|
|
347
|
+
const left = Math.round(slot.x - wScreen / 2);
|
|
348
|
+
const top = Math.round(slot.y - hScreen / 2);
|
|
349
|
+
|
|
350
|
+
const overlay = document.createElement('div');
|
|
351
|
+
overlay.className = 'moodboard-chat__pending-overlay';
|
|
352
|
+
overlay.style.cssText = `left:${left}px;top:${top}px;width:${wScreen}px;height:${hScreen}px`;
|
|
353
|
+
overlay.style.setProperty('--moodboard-chat-board-animation-ms', `${BOARD_IMAGE_REARRANGE_MS}ms`);
|
|
354
|
+
overlay.style.setProperty('--moodboard-chat-pending-enter-x', `${Math.round(BOARD_IMAGE_STEP * s)}px`);
|
|
355
|
+
|
|
356
|
+
if (!this._pendingOverlayMessageIds.has(message.id)) {
|
|
357
|
+
overlay.classList.add('moodboard-chat__pending-overlay--enter');
|
|
358
|
+
this._pendingOverlayMessageIds.add(message.id);
|
|
359
|
+
this._scheduleAnimationFrame(() => {
|
|
360
|
+
if (overlay.isConnected) {
|
|
361
|
+
overlay.classList.add('moodboard-chat__pending-overlay--entered');
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const label = document.createElement('span');
|
|
367
|
+
label.className = 'moodboard-chat__pending-image-label';
|
|
368
|
+
label.textContent = 'В процессе...';
|
|
369
|
+
overlay.appendChild(label);
|
|
370
|
+
|
|
371
|
+
document.body.appendChild(overlay);
|
|
372
|
+
this._pendingOverlayEls.push(overlay);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_clearPendingOverlays() {
|
|
377
|
+
for (const el of this._pendingOverlayEls) {
|
|
378
|
+
el.remove();
|
|
379
|
+
}
|
|
380
|
+
this._pendingOverlayEls = [];
|
|
315
381
|
}
|
|
316
382
|
|
|
317
383
|
_getImageRequestOptions() {
|
|
@@ -319,52 +385,340 @@ export class ChatWindow {
|
|
|
319
385
|
return {
|
|
320
386
|
widthRatio,
|
|
321
387
|
heightRatio,
|
|
322
|
-
model: this._modelId === 'yandex' ? 'yandex-art' : undefined
|
|
388
|
+
model: this._modelId === 'yandex' ? 'yandex-art' : undefined,
|
|
389
|
+
imageCount: parseImageCount(this._countId)
|
|
323
390
|
};
|
|
324
391
|
}
|
|
325
392
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const view = this._boardCore.pixi?.app?.view;
|
|
330
|
-
const world = this._boardCore.pixi?.worldLayer || this._boardCore.pixi?.app?.stage;
|
|
331
|
-
const s = world?.scale?.x || 1;
|
|
393
|
+
_attachReferenceDragEvents() {
|
|
394
|
+
const eventBus = this._boardCore?.eventBus;
|
|
395
|
+
if (!eventBus || typeof eventBus.on !== 'function' || this._referenceDragHandlers) return;
|
|
332
396
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const worldShift = Math.round(320 / s);
|
|
338
|
-
const objects = this._boardCore.state?.state?.objects;
|
|
339
|
-
for (const id of this._boardAiImageIds) {
|
|
340
|
-
const obj = objects?.find((o) => o.id === id);
|
|
341
|
-
if (obj?.position) {
|
|
342
|
-
this._boardCore.updateObjectPositionDirect?.(
|
|
343
|
-
id,
|
|
344
|
-
{ x: Math.round(obj.position.x - worldShift), y: obj.position.y },
|
|
345
|
-
{ snap: false }
|
|
346
|
-
);
|
|
347
|
-
}
|
|
397
|
+
const onCursorMove = ({ x, y } = {}) => {
|
|
398
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
399
|
+
this._boardCursor = { x, y };
|
|
400
|
+
this._updateReferenceDragPreview();
|
|
348
401
|
}
|
|
402
|
+
};
|
|
403
|
+
const onDragStart = (data) => {
|
|
404
|
+
this._handleReferenceDragStart(data);
|
|
405
|
+
};
|
|
406
|
+
const onDragEnd = (data) => {
|
|
407
|
+
void this._handleReferenceDragEnd(data);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
this._referenceDragHandlers = { onCursorMove, onDragStart, onDragEnd };
|
|
411
|
+
eventBus.on(Events.UI.CursorMove, onCursorMove);
|
|
412
|
+
eventBus.on(Events.Tool.DragStart, onDragStart);
|
|
413
|
+
eventBus.on(Events.Tool.DragEnd, onDragEnd);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_detachReferenceDragEvents() {
|
|
417
|
+
const eventBus = this._boardCore?.eventBus;
|
|
418
|
+
const handlers = this._referenceDragHandlers;
|
|
419
|
+
if (!eventBus || typeof eventBus.off !== 'function' || !handlers) return;
|
|
420
|
+
|
|
421
|
+
eventBus.off(Events.UI.CursorMove, handlers.onCursorMove);
|
|
422
|
+
eventBus.off(Events.Tool.DragStart, handlers.onDragStart);
|
|
423
|
+
eventBus.off(Events.Tool.DragEnd, handlers.onDragEnd);
|
|
424
|
+
this._referenceDragHandlers = null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
_handleReferenceDragStart(data = {}) {
|
|
428
|
+
const objectId = data?.object;
|
|
429
|
+
const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
|
|
430
|
+
this._draggedReferenceObject = isReferenceImageObject(object) ? object : null;
|
|
431
|
+
this._draggedReferenceStartPosition = this._draggedReferenceObject?.position
|
|
432
|
+
? { ...this._draggedReferenceObject.position }
|
|
433
|
+
: null;
|
|
434
|
+
this._updateReferenceDragPreview();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async _handleReferenceDragEnd(data = {}) {
|
|
438
|
+
const isDropTarget = this._isBoardCursorOverInput();
|
|
439
|
+
const objectId = data?.object;
|
|
440
|
+
const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
|
|
441
|
+
const startPosition = this._draggedReferenceStartPosition;
|
|
442
|
+
this._clearReferenceDragState();
|
|
443
|
+
if (!isDropTarget || !isReferenceImageObject(object)) return null;
|
|
444
|
+
|
|
445
|
+
this._restoreReferenceObjectPosition(objectId, startPosition);
|
|
446
|
+
await this._addImageObjectAsReference(object);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_restoreReferenceObjectPosition(objectId, position) {
|
|
450
|
+
if (!objectId || !position) return;
|
|
451
|
+
|
|
452
|
+
const updatePosition = this._boardCore?.updateObjectPositionDirect;
|
|
453
|
+
if (typeof updatePosition === 'function') {
|
|
454
|
+
updatePosition.call(this._boardCore, objectId, position, { snap: false });
|
|
455
|
+
return;
|
|
349
456
|
}
|
|
350
457
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
458
|
+
const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
|
|
459
|
+
if (object?.position) {
|
|
460
|
+
object.position = { ...position };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
_updateReferenceDragPreview() {
|
|
465
|
+
const object = this._draggedReferenceObject;
|
|
466
|
+
if (!object || !this._isBoardCursorOverInput()) {
|
|
467
|
+
this._hideReferenceDragPreview();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const src = getImageObjectSource(object);
|
|
472
|
+
if (!src) {
|
|
473
|
+
this._hideReferenceDragPreview();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const preview = this._ensureReferenceDragPreview(object, src);
|
|
478
|
+
const { clientX, clientY } = this._getBoardCursorClientPosition();
|
|
479
|
+
preview.style.left = `${Math.round(clientX)}px`;
|
|
480
|
+
preview.style.top = `${Math.round(clientY)}px`;
|
|
481
|
+
this._refs?.textarea?.closest?.('.moodboard-chat__input-row')?.classList.add('is-reference-drop-target');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
_ensureReferenceDragPreview(object, src) {
|
|
485
|
+
if (!this._referenceDragPreview) {
|
|
486
|
+
const preview = document.createElement('img');
|
|
487
|
+
preview.className = 'moodboard-chat__reference-drag-preview';
|
|
488
|
+
preview.alt = getImageObjectFileName(object, src);
|
|
489
|
+
preview.width = REFERENCE_DRAG_PREVIEW_SIZE;
|
|
490
|
+
preview.height = REFERENCE_DRAG_PREVIEW_SIZE;
|
|
491
|
+
document.body.appendChild(preview);
|
|
492
|
+
this._referenceDragPreview = preview;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (this._referenceDragPreview.src !== src) {
|
|
496
|
+
this._referenceDragPreview.src = src;
|
|
497
|
+
}
|
|
498
|
+
this._referenceDragPreview.alt = getImageObjectFileName(object, src);
|
|
499
|
+
|
|
500
|
+
return this._referenceDragPreview;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
_hideReferenceDragPreview() {
|
|
504
|
+
this._referenceDragPreview?.remove();
|
|
505
|
+
this._referenceDragPreview = null;
|
|
506
|
+
this._refs?.textarea?.closest?.('.moodboard-chat__input-row')?.classList.remove('is-reference-drop-target');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
_clearReferenceDragState() {
|
|
510
|
+
this._draggedReferenceObject = null;
|
|
511
|
+
this._draggedReferenceStartPosition = null;
|
|
512
|
+
this._boardCursor = null;
|
|
513
|
+
this._hideReferenceDragPreview();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
_isBoardCursorOverInput() {
|
|
517
|
+
const cursor = this._boardCursor;
|
|
518
|
+
const inputRow = this._refs?.textarea?.closest?.('.moodboard-chat__input-row');
|
|
519
|
+
if (!cursor || !inputRow) return false;
|
|
520
|
+
|
|
521
|
+
const containerRect = this._container.getBoundingClientRect?.();
|
|
522
|
+
const rect = inputRow.getBoundingClientRect();
|
|
523
|
+
const { clientX, clientY } = this._getBoardCursorClientPosition(containerRect);
|
|
524
|
+
|
|
525
|
+
return clientX >= rect.left
|
|
526
|
+
&& clientX <= rect.right
|
|
527
|
+
&& clientY >= rect.top
|
|
528
|
+
&& clientY <= rect.bottom;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
_getBoardCursorClientPosition(containerRect = null) {
|
|
532
|
+
const rect = containerRect || this._container.getBoundingClientRect?.();
|
|
533
|
+
const cursor = this._boardCursor || { x: 0, y: 0 };
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
clientX: (rect?.left || 0) + cursor.x,
|
|
537
|
+
clientY: (rect?.top || 0) + cursor.y
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async _addImageObjectAsReference(object) {
|
|
542
|
+
if (!object || !this._composer) return;
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
const file = await createFileFromImageObject(object);
|
|
546
|
+
if (!file) return;
|
|
547
|
+
this._composer.addAttachment(file);
|
|
548
|
+
this._composer.focus();
|
|
549
|
+
} catch (err) {
|
|
550
|
+
console.warn('[ChatWindow] cannot add selected image reference:', err);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
_getImageBatchSlot(messages, messageId, scale = 1) {
|
|
555
|
+
const batch = findImageGenerationBatch(messages, messageId);
|
|
556
|
+
const anchor = this._getImageGroupAnchor();
|
|
557
|
+
const step = Math.round(BOARD_IMAGE_STEP * scale);
|
|
558
|
+
const count = Math.max(batch.count, 1);
|
|
559
|
+
const index = Math.min(Math.max(batch.index, 0), count - 1);
|
|
560
|
+
const leftmostCenter = anchor.x - ((count - 1) * step) / 2;
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
x: Math.round(leftmostCenter + index * step),
|
|
564
|
+
y: anchor.y
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
_getImageGroupAnchor() {
|
|
357
569
|
const composerRect = this._refs?.composer?.getBoundingClientRect?.();
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
570
|
+
if (composerRect) {
|
|
571
|
+
return {
|
|
572
|
+
x: Math.round(composerRect.left + composerRect.width / 2),
|
|
573
|
+
y: Math.round(composerRect.top - 250)
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const chatRect = this._refs?.root?.getBoundingClientRect?.();
|
|
578
|
+
if (chatRect) {
|
|
579
|
+
return {
|
|
580
|
+
x: Math.round(chatRect.left + chatRect.width / 2),
|
|
581
|
+
y: Math.round(chatRect.top - 150)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return { x: 400, y: 200 };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
_shiftExistingImagesForBatch(messages, messageId, scale = 1) {
|
|
589
|
+
const batch = findImageGenerationBatch(messages, messageId);
|
|
590
|
+
const batchKey = getImageGenerationBatchKey(batch);
|
|
591
|
+
if (this._shiftedForImageBatchKeys.has(batchKey)) return;
|
|
592
|
+
|
|
593
|
+
this._shiftedForImageBatchKeys.add(batchKey);
|
|
594
|
+
this._shiftBoardAiImagesLeft(this._getImageBatchWorldBounds(messages, messageId, scale));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
_getImageBatchWorldBounds(messages, messageId, scale = 1) {
|
|
598
|
+
const batch = findImageGenerationBatch(messages, messageId);
|
|
599
|
+
const anchor = this._getImageGroupAnchor();
|
|
600
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
601
|
+
const s = scale || 1;
|
|
602
|
+
const step = Math.round(BOARD_IMAGE_STEP * s);
|
|
603
|
+
const width = Math.round(BOARD_IMAGE_WIDTH * s);
|
|
604
|
+
const count = Math.max(batch.count, 1);
|
|
605
|
+
const leftScreen = anchor.x - ((count - 1) * step) / 2 - width / 2;
|
|
606
|
+
const rightScreen = anchor.x + ((count - 1) * step) / 2 + width / 2;
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
left: Math.round((leftScreen - (world?.x || 0)) / s),
|
|
610
|
+
right: Math.round((rightScreen - (world?.x || 0)) / s)
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
_shiftBoardAiImagesLeft(nextBatchBounds) {
|
|
615
|
+
const aiObjects = this._getBoardAiImageObjects();
|
|
616
|
+
if (aiObjects.length === 0 || !nextBatchBounds) return;
|
|
617
|
+
|
|
618
|
+
const existingRight = Math.max(...aiObjects.map((object) => object.position.x + getBoardObjectWidth(object)));
|
|
619
|
+
const shift = Math.ceil(existingRight + BOARD_IMAGE_GAP - nextBatchBounds.left);
|
|
620
|
+
if (shift <= 0) return;
|
|
621
|
+
|
|
622
|
+
const ids = new Set(aiObjects.map((object) => object.id));
|
|
623
|
+
const objects = this._boardCore?.state?.state?.objects;
|
|
624
|
+
for (const id of ids) {
|
|
625
|
+
const obj = objects?.find((item) => item.id === id);
|
|
626
|
+
if (obj?.position) {
|
|
627
|
+
this._animateBoardImageToPosition(
|
|
628
|
+
id,
|
|
629
|
+
obj.position,
|
|
630
|
+
{ x: Math.round(obj.position.x - shift), y: obj.position.y }
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
_animateBoardImageToPosition(id, fromPosition, toPosition) {
|
|
637
|
+
const updatePosition = this._boardCore?.updateObjectPositionDirect;
|
|
638
|
+
if (!id || typeof updatePosition !== 'function' || !fromPosition || !toPosition) return;
|
|
639
|
+
|
|
640
|
+
this._cancelBoardImageShiftAnimation(id);
|
|
641
|
+
|
|
642
|
+
if (BOARD_IMAGE_REARRANGE_MS <= 0 || prefersReducedMotion()) {
|
|
643
|
+
updatePosition.call(this._boardCore, id, toPosition, { snap: false });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const from = {
|
|
648
|
+
x: Number(fromPosition.x) || 0,
|
|
649
|
+
y: Number(fromPosition.y) || 0
|
|
650
|
+
};
|
|
651
|
+
const to = {
|
|
652
|
+
x: Math.round(Number(toPosition.x) || 0),
|
|
653
|
+
y: Math.round(Number(toPosition.y) || 0)
|
|
654
|
+
};
|
|
655
|
+
const startAt = getAnimationTime();
|
|
656
|
+
const record = { frame: null };
|
|
657
|
+
|
|
658
|
+
const step = (now) => {
|
|
659
|
+
const progress = Math.min(Math.max((now - startAt) / BOARD_IMAGE_REARRANGE_MS, 0), 1);
|
|
660
|
+
const eased = easeOutCubic(progress);
|
|
661
|
+
const next = {
|
|
662
|
+
x: Math.round(from.x + (to.x - from.x) * eased),
|
|
663
|
+
y: Math.round(from.y + (to.y - from.y) * eased)
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
updatePosition.call(this._boardCore, id, next, { snap: false });
|
|
667
|
+
|
|
668
|
+
if (progress < 1) {
|
|
669
|
+
record.frame = this._scheduleAnimationFrame(step);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
updatePosition.call(this._boardCore, id, to, { snap: false });
|
|
674
|
+
this._boardImageShiftAnimations.delete(id);
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
record.frame = this._scheduleAnimationFrame(step);
|
|
678
|
+
this._boardImageShiftAnimations.set(id, record);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
_cancelBoardImageShiftAnimation(id) {
|
|
682
|
+
const record = this._boardImageShiftAnimations.get(id);
|
|
683
|
+
if (!record) return;
|
|
684
|
+
|
|
685
|
+
cancelAnimationFrameSafe(record.frame);
|
|
686
|
+
this._boardImageShiftAnimations.delete(id);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
_cancelBoardImageShiftAnimations() {
|
|
690
|
+
for (const id of this._boardImageShiftAnimations.keys()) {
|
|
691
|
+
this._cancelBoardImageShiftAnimation(id);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
_scheduleAnimationFrame(callback) {
|
|
696
|
+
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
|
697
|
+
return window.requestAnimationFrame(callback);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return setTimeout(() => callback(getAnimationTime()), 16);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
_getBoardAiImageObjects() {
|
|
704
|
+
const objects = this._boardCore?.state?.state?.objects;
|
|
705
|
+
if (!Array.isArray(objects)) return [];
|
|
706
|
+
|
|
707
|
+
return objects.filter((object) => isBoardAiImageObject(object));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
_addImageToBoard(msg) {
|
|
711
|
+
if (!this._boardCore?.eventBus) return;
|
|
712
|
+
const dataUrl = `data:${msg.mimeType || 'image/jpeg'};base64,${msg.imageBase64}`;
|
|
713
|
+
const world = this._boardCore.pixi?.worldLayer || this._boardCore.pixi?.app?.stage;
|
|
714
|
+
const s = world?.scale?.x || 1;
|
|
715
|
+
const messages = this._session.getState().messages;
|
|
716
|
+
this._shiftExistingImagesForBatch(messages, msg.id, s);
|
|
717
|
+
const slot = this._getImageBatchSlot(messages, msg.id, s);
|
|
718
|
+
|
|
366
719
|
this._boardCore.eventBus.emit(Events.UI.PasteImageAt, {
|
|
367
|
-
x
|
|
720
|
+
x: slot.x,
|
|
721
|
+
y: slot.y,
|
|
368
722
|
src: dataUrl,
|
|
369
723
|
name: 'ai-generated.jpg',
|
|
370
724
|
skipUpload: true
|
|
@@ -405,3 +759,161 @@ function parseFormatRatio(formatId) {
|
|
|
405
759
|
|
|
406
760
|
return [width, height];
|
|
407
761
|
}
|
|
762
|
+
|
|
763
|
+
function parseImageCount(countId) {
|
|
764
|
+
if (!countId || countId === 'auto') {
|
|
765
|
+
return 1;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const count = Number.parseInt(countId, 10);
|
|
769
|
+
if (!Number.isFinite(count)) {
|
|
770
|
+
return 1;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return Math.min(Math.max(count, 1), 4);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function findImageGenerationBatch(messages, messageId) {
|
|
777
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
778
|
+
const targetIndex = list.findIndex((message) => message?.id === messageId);
|
|
779
|
+
if (targetIndex === -1) {
|
|
780
|
+
return { index: 0, count: 1 };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let start = targetIndex;
|
|
784
|
+
while (start > 0 && isImageGenerationMessage(list[start - 1])) {
|
|
785
|
+
start--;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
let end = targetIndex;
|
|
789
|
+
while (end + 1 < list.length && isImageGenerationMessage(list[end + 1])) {
|
|
790
|
+
end++;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return {
|
|
794
|
+
index: targetIndex - start,
|
|
795
|
+
count: end - start + 1,
|
|
796
|
+
ids: list.slice(start, end + 1).map((message) => message.id)
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function getImageGenerationBatchKey(batch) {
|
|
801
|
+
return batch.ids?.join('|') || 'unknown';
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function isImageGenerationMessage(message) {
|
|
805
|
+
return message?.role === 'assistant'
|
|
806
|
+
&& (message.kind === 'image' || message.pending || Boolean(message.imageBase64));
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function isBoardAiImageObject(object) {
|
|
810
|
+
return Boolean(object?.id)
|
|
811
|
+
&& object.type === 'image'
|
|
812
|
+
&& object.properties?.name === 'ai-generated.jpg'
|
|
813
|
+
&& object.position
|
|
814
|
+
&& Number.isFinite(object.position.x);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function isReferenceImageObject(object) {
|
|
818
|
+
return Boolean(object?.id)
|
|
819
|
+
&& (object.type === 'image' || object.type === 'revit-screenshot-img')
|
|
820
|
+
&& typeof getImageObjectSource(object) === 'string';
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function createFileFromImageObject(object) {
|
|
824
|
+
const src = getImageObjectSource(object);
|
|
825
|
+
if (!src) return null;
|
|
826
|
+
|
|
827
|
+
const name = getImageObjectFileName(object, src);
|
|
828
|
+
const blob = src.startsWith('data:')
|
|
829
|
+
? dataUrlToBlob(src)
|
|
830
|
+
: await fetchImageBlob(src);
|
|
831
|
+
|
|
832
|
+
return createNamedBlob(blob, name);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function getImageObjectSource(object) {
|
|
836
|
+
const src = object?.src || object?.properties?.src || object?.properties?.url || object?.url;
|
|
837
|
+
return typeof src === 'string' && src.trim() ? src.trim() : null;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function getImageObjectFileName(object, src) {
|
|
841
|
+
const explicitName = object?.properties?.name || object?.name;
|
|
842
|
+
if (typeof explicitName === 'string' && explicitName.trim()) {
|
|
843
|
+
return explicitName.trim();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (!src.startsWith('data:')) {
|
|
847
|
+
const lastPathPart = src.split(/[?#]/)[0].split('/').pop();
|
|
848
|
+
if (lastPathPart) return lastPathPart;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return 'board-reference.png';
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function dataUrlToBlob(dataUrl) {
|
|
855
|
+
const [meta = '', data = ''] = dataUrl.split(',');
|
|
856
|
+
const mimeMatch = meta.match(/^data:([^;]+)/);
|
|
857
|
+
const mimeType = mimeMatch?.[1] || 'image/png';
|
|
858
|
+
const isBase64 = /;base64/i.test(meta);
|
|
859
|
+
const binary = isBase64 ? atob(data) : decodeURIComponent(data);
|
|
860
|
+
const bytes = new Uint8Array(binary.length);
|
|
861
|
+
|
|
862
|
+
for (let i = 0; i < binary.length; i++) {
|
|
863
|
+
bytes[i] = binary.charCodeAt(i);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return new Blob([bytes], { type: mimeType });
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async function fetchImageBlob(src) {
|
|
870
|
+
const response = await fetch(src);
|
|
871
|
+
if (!response.ok) {
|
|
872
|
+
throw new Error(`Cannot load image reference (${response.status})`);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return response.blob();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function createNamedBlob(blob, name) {
|
|
879
|
+
if (typeof File === 'function') {
|
|
880
|
+
return new File([blob], name, { type: blob.type || 'image/png' });
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
blob.name = name;
|
|
884
|
+
return blob;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function getBoardObjectWidth(object) {
|
|
888
|
+
const width = object?.width ?? object?.properties?.width ?? BOARD_IMAGE_WIDTH;
|
|
889
|
+
return Number.isFinite(width) ? Math.max(1, Math.round(width)) : BOARD_IMAGE_WIDTH;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function easeOutCubic(progress) {
|
|
893
|
+
return 1 - Math.pow(1 - progress, 3);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function getAnimationTime() {
|
|
897
|
+
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
|
898
|
+
return performance.now();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return Date.now();
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function cancelAnimationFrameSafe(frame) {
|
|
905
|
+
if (!frame) return;
|
|
906
|
+
|
|
907
|
+
if (typeof window !== 'undefined' && typeof window.cancelAnimationFrame === 'function') {
|
|
908
|
+
window.cancelAnimationFrame(frame);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
clearTimeout(frame);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function prefersReducedMotion() {
|
|
916
|
+
return typeof window !== 'undefined'
|
|
917
|
+
&& typeof window.matchMedia === 'function'
|
|
918
|
+
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
919
|
+
}
|
|
@@ -28,17 +28,21 @@ export function buildChatDom() {
|
|
|
28
28
|
statusBar.setAttribute('aria-atomic', 'true');
|
|
29
29
|
statusBar.innerHTML = '<span class="moodboard-chat__status-bar-text">Идёт процесс генерации изображения…</span>';
|
|
30
30
|
|
|
31
|
+
const pendingImages = createDiv('moodboard-chat__pending-images');
|
|
32
|
+
|
|
31
33
|
const rendererRefs = {
|
|
32
34
|
root,
|
|
33
35
|
history,
|
|
34
36
|
composer,
|
|
35
|
-
statusBar
|
|
37
|
+
statusBar,
|
|
38
|
+
pendingImages
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
composer.appendChild(buildInputRow(refs => Object.assign(rendererRefs, refs)));
|
|
39
42
|
composer.appendChild(buildActionsRow(refs => Object.assign(rendererRefs, refs)));
|
|
40
43
|
|
|
41
44
|
root.appendChild(history);
|
|
45
|
+
root.appendChild(pendingImages);
|
|
42
46
|
root.appendChild(statusBar);
|
|
43
47
|
root.appendChild(composer);
|
|
44
48
|
|
package/src/ui/styles/chat.css
CHANGED
|
@@ -198,6 +198,13 @@
|
|
|
198
198
|
flex-direction: column;
|
|
199
199
|
gap: 0;
|
|
200
200
|
padding-bottom: 10px;
|
|
201
|
+
border-radius: 10px;
|
|
202
|
+
transition: background-color 120ms ease, box-shadow 120ms ease;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.moodboard-chat__input-row.is-reference-drop-target {
|
|
206
|
+
background: rgba(99, 102, 241, 0.08);
|
|
207
|
+
box-shadow: inset 0 0 0 1px rgba(99, 102, 241, 0.22);
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
.moodboard-chat__textarea-row {
|
|
@@ -206,6 +213,19 @@
|
|
|
206
213
|
gap: 8px;
|
|
207
214
|
}
|
|
208
215
|
|
|
216
|
+
.moodboard-chat__reference-drag-preview {
|
|
217
|
+
position: fixed;
|
|
218
|
+
z-index: 2700;
|
|
219
|
+
width: 96px;
|
|
220
|
+
height: 96px;
|
|
221
|
+
object-fit: cover;
|
|
222
|
+
border-radius: 10px;
|
|
223
|
+
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.24);
|
|
224
|
+
transform: translate(-50%, -50%) scale(0.92);
|
|
225
|
+
pointer-events: none;
|
|
226
|
+
opacity: 0.96;
|
|
227
|
+
}
|
|
228
|
+
|
|
209
229
|
.moodboard-chat__input-row.has-attachments .moodboard-chat__textarea-row .moodboard-chat__pill-wrapper {
|
|
210
230
|
position: absolute;
|
|
211
231
|
top: 0;
|
|
@@ -661,6 +681,79 @@
|
|
|
661
681
|
display: block;
|
|
662
682
|
}
|
|
663
683
|
|
|
684
|
+
/* Контейнер блоков-заглушек при параллельной генерации изображений */
|
|
685
|
+
.moodboard-chat__pending-images {
|
|
686
|
+
display: none;
|
|
687
|
+
gap: 8px;
|
|
688
|
+
background: #ffffff;
|
|
689
|
+
border-radius: 12px;
|
|
690
|
+
padding: 8px;
|
|
691
|
+
margin-bottom: 8px;
|
|
692
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.moodboard-chat__pending-images.is-visible {
|
|
696
|
+
display: flex;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.moodboard-chat__pending-image-block {
|
|
700
|
+
flex: 1;
|
|
701
|
+
min-height: 180px;
|
|
702
|
+
background: #6B7E87;
|
|
703
|
+
border-radius: 8px;
|
|
704
|
+
position: relative;
|
|
705
|
+
overflow: hidden;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* Overlay-заглушка на полотне доски во время генерации изображения */
|
|
709
|
+
@keyframes moodboard-pending-shimmer {
|
|
710
|
+
0% { background-position: 200% 0; }
|
|
711
|
+
100% { background-position: -100% 0; }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.moodboard-chat__pending-overlay {
|
|
715
|
+
position: fixed;
|
|
716
|
+
--moodboard-chat-board-animation-ms: 520ms;
|
|
717
|
+
--moodboard-chat-pending-enter-x: 320px;
|
|
718
|
+
background: linear-gradient(
|
|
719
|
+
90deg,
|
|
720
|
+
#5F7179 0%,
|
|
721
|
+
#7A8D96 50%,
|
|
722
|
+
#5F7179 100%
|
|
723
|
+
);
|
|
724
|
+
background-size: 200% 100%;
|
|
725
|
+
animation: moodboard-pending-shimmer 5.76s linear infinite;
|
|
726
|
+
border-radius: 12px;
|
|
727
|
+
overflow: hidden;
|
|
728
|
+
pointer-events: none;
|
|
729
|
+
z-index: 10;
|
|
730
|
+
transition:
|
|
731
|
+
transform var(--moodboard-chat-board-animation-ms) cubic-bezier(0.22, 1, 0.36, 1),
|
|
732
|
+
opacity var(--moodboard-chat-board-animation-ms) ease;
|
|
733
|
+
will-change: transform, opacity;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.moodboard-chat__pending-overlay--enter {
|
|
737
|
+
opacity: 0;
|
|
738
|
+
transform: translateX(var(--moodboard-chat-pending-enter-x));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.moodboard-chat__pending-overlay--entered {
|
|
742
|
+
opacity: 1;
|
|
743
|
+
transform: translateX(0);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.moodboard-chat__pending-image-label {
|
|
747
|
+
position: absolute;
|
|
748
|
+
bottom: 10px;
|
|
749
|
+
left: 12px;
|
|
750
|
+
font-family: 'GeistSans', 'GeistSans Fallback', 'Roboto', Arial, sans-serif;
|
|
751
|
+
font-size: 20px;
|
|
752
|
+
font-weight: 400;
|
|
753
|
+
color: #ffffff;
|
|
754
|
+
pointer-events: none;
|
|
755
|
+
}
|
|
756
|
+
|
|
664
757
|
/* Попап настроек */
|
|
665
758
|
.moodboard-chat__settings-popup {
|
|
666
759
|
position: absolute;
|