@sequent-org/moodboard 1.4.29 → 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 +1 -1
- package/src/ui/chat/ChatComposer.js +7 -0
- package/src/ui/chat/ChatWindow.js +507 -99
- package/src/ui/styles/chat.css +39 -3
package/package.json
CHANGED
|
@@ -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,17 +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._boardAiImageIds = [];
|
|
135
|
-
this._shiftedForPendingMessageIds = new Set();
|
|
136
|
-
this._pendingShiftCount = 0;
|
|
139
|
+
this._shiftedForImageBatchKeys = new Set();
|
|
137
140
|
this._pendingOverlayEls = [];
|
|
138
|
-
this.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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;
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
attach() {
|
|
@@ -165,6 +170,7 @@ export class ChatWindow {
|
|
|
165
170
|
}
|
|
166
171
|
);
|
|
167
172
|
this._composer.attach();
|
|
173
|
+
this._attachReferenceDragEvents();
|
|
168
174
|
|
|
169
175
|
this._extendedPromptModal = new ChatExtendedPromptModal(
|
|
170
176
|
this._container,
|
|
@@ -233,8 +239,6 @@ export class ChatWindow {
|
|
|
233
239
|
);
|
|
234
240
|
this._countMenu.attach();
|
|
235
241
|
|
|
236
|
-
this._boardCore?.eventBus?.on?.(Events.Object.Created, this._onBoardObjectCreated);
|
|
237
|
-
|
|
238
242
|
const initialState = this._session.getState();
|
|
239
243
|
this._markExistingBoardImages(initialState.messages);
|
|
240
244
|
this._unsubscribe = this._session.subscribe((state) => this._render(state));
|
|
@@ -248,11 +252,12 @@ export class ChatWindow {
|
|
|
248
252
|
detach() {
|
|
249
253
|
if (!this._attached) return;
|
|
250
254
|
this._clearPendingOverlays();
|
|
255
|
+
this._cancelBoardImageShiftAnimations();
|
|
256
|
+
this._clearReferenceDragState();
|
|
251
257
|
if (this._unsubscribe) { this._unsubscribe(); this._unsubscribe = null; }
|
|
252
|
-
this.
|
|
253
|
-
this.
|
|
254
|
-
this.
|
|
255
|
-
this._pendingShiftCount = 0;
|
|
258
|
+
this._detachReferenceDragEvents();
|
|
259
|
+
this._shiftedForImageBatchKeys.clear();
|
|
260
|
+
this._pendingOverlayMessageIds.clear();
|
|
256
261
|
this._composer?.destroy();
|
|
257
262
|
this._extendedPromptModal?.destroy();
|
|
258
263
|
this._contentTypeMenu?.destroy();
|
|
@@ -330,59 +335,37 @@ export class ChatWindow {
|
|
|
330
335
|
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
331
336
|
const s = world?.scale?.x || 1;
|
|
332
337
|
|
|
333
|
-
|
|
334
|
-
for (const p of pending) {
|
|
335
|
-
if (!this._shiftedForPendingMessageIds.has(p.id)) {
|
|
336
|
-
this._shiftedForPendingMessageIds.add(p.id);
|
|
337
|
-
newPendingCount++;
|
|
338
|
-
this._pendingShiftCount++;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Сдвигаем все ранее размещённые AI-изображения влево сразу при появлении блока загрузки,
|
|
343
|
-
// чтобы блок не перекрывал их. 320 - ширина блока + отступ в мировых координатах.
|
|
344
|
-
if (newPendingCount > 0 && this._boardAiImageIds.length > 0) {
|
|
345
|
-
const worldShift = Math.round(320 * newPendingCount);
|
|
346
|
-
const objects = this._boardCore?.state?.state?.objects;
|
|
347
|
-
for (const id of this._boardAiImageIds) {
|
|
348
|
-
const obj = objects?.find((o) => o.id === id);
|
|
349
|
-
if (obj?.position) {
|
|
350
|
-
this._boardCore.updateObjectPositionDirect?.(
|
|
351
|
-
id,
|
|
352
|
-
{ x: Math.round(obj.position.x - worldShift), y: obj.position.y },
|
|
353
|
-
{ snap: false }
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const chatRect = this._refs?.root?.getBoundingClientRect?.();
|
|
360
|
-
const composerRect = this._refs?.composer?.getBoundingClientRect?.();
|
|
361
|
-
const cx = chatRect
|
|
362
|
-
? Math.round(chatRect.left + chatRect.width / 2)
|
|
363
|
-
: 400;
|
|
364
|
-
const cy = composerRect
|
|
365
|
-
? Math.round(composerRect.top - 250)
|
|
366
|
-
: 200;
|
|
338
|
+
this._shiftExistingImagesForBatch(messages, pending[0].id, s);
|
|
367
339
|
|
|
368
340
|
const [wr, hr] = parseFormatRatio(this._formatId);
|
|
369
341
|
const ratio = wr / hr;
|
|
370
|
-
const wScreen = Math.round(
|
|
342
|
+
const wScreen = Math.round(BOARD_IMAGE_WIDTH * s);
|
|
371
343
|
const hScreen = Math.round(wScreen / ratio);
|
|
372
344
|
|
|
373
|
-
for (
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const top = Math.round(cy - hScreen / 2);
|
|
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);
|
|
378
349
|
|
|
379
350
|
const overlay = document.createElement('div');
|
|
380
351
|
overlay.className = 'moodboard-chat__pending-overlay';
|
|
381
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
|
+
}
|
|
382
365
|
|
|
383
366
|
const label = document.createElement('span');
|
|
384
367
|
label.className = 'moodboard-chat__pending-image-label';
|
|
385
|
-
label.textContent = 'В
|
|
368
|
+
label.textContent = 'В процессе...';
|
|
386
369
|
overlay.appendChild(label);
|
|
387
370
|
|
|
388
371
|
document.body.appendChild(overlay);
|
|
@@ -407,55 +390,335 @@ export class ChatWindow {
|
|
|
407
390
|
};
|
|
408
391
|
}
|
|
409
392
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const view = this._boardCore.pixi?.app?.view;
|
|
414
|
-
const world = this._boardCore.pixi?.worldLayer || this._boardCore.pixi?.app?.stage;
|
|
415
|
-
const s = world?.scale?.x || 1;
|
|
393
|
+
_attachReferenceDragEvents() {
|
|
394
|
+
const eventBus = this._boardCore?.eventBus;
|
|
395
|
+
if (!eventBus || typeof eventBus.on !== 'function' || this._referenceDragHandlers) return;
|
|
416
396
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
this._pendingShiftCount--;
|
|
422
|
-
myShiftIndex = this._pendingShiftCount;
|
|
423
|
-
} else if (this._boardAiImageIds.length > 0) {
|
|
424
|
-
const worldShift = 320;
|
|
425
|
-
const objects = this._boardCore.state?.state?.objects;
|
|
426
|
-
for (const id of this._boardAiImageIds) {
|
|
427
|
-
const obj = objects?.find((o) => o.id === id);
|
|
428
|
-
if (obj?.position) {
|
|
429
|
-
this._boardCore.updateObjectPositionDirect?.(
|
|
430
|
-
id,
|
|
431
|
-
{ x: Math.round(obj.position.x - worldShift), y: obj.position.y },
|
|
432
|
-
{ snap: false }
|
|
433
|
-
);
|
|
434
|
-
}
|
|
397
|
+
const onCursorMove = ({ x, y } = {}) => {
|
|
398
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
399
|
+
this._boardCursor = { x, y };
|
|
400
|
+
this._updateReferenceDragPreview();
|
|
435
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;
|
|
436
456
|
}
|
|
437
457
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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() {
|
|
444
569
|
const composerRect = this._refs?.composer?.getBoundingClientRect?.();
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
:
|
|
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);
|
|
456
718
|
|
|
457
719
|
this._boardCore.eventBus.emit(Events.UI.PasteImageAt, {
|
|
458
|
-
x
|
|
720
|
+
x: slot.x,
|
|
721
|
+
y: slot.y,
|
|
459
722
|
src: dataUrl,
|
|
460
723
|
name: 'ai-generated.jpg',
|
|
461
724
|
skipUpload: true
|
|
@@ -509,3 +772,148 @@ function parseImageCount(countId) {
|
|
|
509
772
|
|
|
510
773
|
return Math.min(Math.max(count, 1), 4);
|
|
511
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
|
+
}
|
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;
|
|
@@ -687,12 +707,14 @@
|
|
|
687
707
|
|
|
688
708
|
/* Overlay-заглушка на полотне доски во время генерации изображения */
|
|
689
709
|
@keyframes moodboard-pending-shimmer {
|
|
690
|
-
0% { background-position:
|
|
691
|
-
100% { background-position:
|
|
710
|
+
0% { background-position: 200% 0; }
|
|
711
|
+
100% { background-position: -100% 0; }
|
|
692
712
|
}
|
|
693
713
|
|
|
694
714
|
.moodboard-chat__pending-overlay {
|
|
695
715
|
position: fixed;
|
|
716
|
+
--moodboard-chat-board-animation-ms: 520ms;
|
|
717
|
+
--moodboard-chat-pending-enter-x: 320px;
|
|
696
718
|
background: linear-gradient(
|
|
697
719
|
90deg,
|
|
698
720
|
#5F7179 0%,
|
|
@@ -701,10 +723,24 @@
|
|
|
701
723
|
);
|
|
702
724
|
background-size: 200% 100%;
|
|
703
725
|
animation: moodboard-pending-shimmer 5.76s linear infinite;
|
|
704
|
-
border-radius:
|
|
726
|
+
border-radius: 12px;
|
|
705
727
|
overflow: hidden;
|
|
706
728
|
pointer-events: none;
|
|
707
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);
|
|
708
744
|
}
|
|
709
745
|
|
|
710
746
|
.moodboard-chat__pending-image-label {
|