@sequent-org/moodboard 1.4.30 → 1.4.32

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.
Files changed (61) hide show
  1. package/package.json +3 -1
  2. package/src/core/PixiEngine.js +34 -5
  3. package/src/core/bootstrap/CoreInitializer.js +4 -0
  4. package/src/core/commands/CreateConnectorCommand.js +25 -0
  5. package/src/core/commands/GroupMoveCommand.js +2 -2
  6. package/src/core/commands/MoveObjectCommand.js +1 -1
  7. package/src/core/commands/UpdateConnectorCommand.js +38 -0
  8. package/src/core/events/Events.js +1 -0
  9. package/src/mindmap/MindmapCompoundContract.js +1 -0
  10. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +14 -0
  11. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +18 -0
  12. package/src/objects/ConnectorObject.js +85 -0
  13. package/src/objects/DrawingObject.js +47 -0
  14. package/src/objects/MindmapObject.js +21 -3
  15. package/src/objects/NoteObject.js +16 -8
  16. package/src/objects/ObjectFactory.js +3 -1
  17. package/src/objects/ShapeObject.js +1 -1
  18. package/src/services/ConnectorBindingResolver.js +204 -0
  19. package/src/services/ai/AiClient.js +30 -2
  20. package/src/services/ai/ChatSessionController.js +1 -0
  21. package/src/tools/ToolManager.js +3 -0
  22. package/src/tools/manager/PointerGestureController.js +206 -0
  23. package/src/tools/manager/ToolEventRouter.js +10 -0
  24. package/src/tools/manager/ToolManagerGuards.js +3 -1
  25. package/src/tools/manager/ToolManagerLifecycle.js +70 -58
  26. package/src/tools/object-tools/ConnectorTool.js +147 -0
  27. package/src/tools/object-tools/PlacementTool.js +2 -2
  28. package/src/tools/object-tools/connector/ConnectorDragController.js +296 -0
  29. package/src/tools/object-tools/connector/connectorGesture.js +108 -0
  30. package/src/tools/object-tools/placement/GhostController.js +4 -4
  31. package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -2
  32. package/src/tools/object-tools/placement/PlacementInputRouter.js +5 -5
  33. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +11 -2
  34. package/src/tools/object-tools/selection/SelectInputRouter.js +33 -4
  35. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +12 -0
  36. package/src/tools/object-tools/selection/SelectToolSetup.js +3 -0
  37. package/src/tools/object-tools/selection/TextEditorDomFactory.js +1 -2
  38. package/src/tools/object-tools/selection/TextEditorSyncService.js +4 -4
  39. package/src/tools/object-tools/selection/TextInlineEditorController.js +21 -3
  40. package/src/tools/object-tools/selection/TransformInteractionController.js +4 -6
  41. package/src/ui/HtmlTextLayer.js +212 -5
  42. package/src/ui/animation/HoverLiftController.js +395 -0
  43. package/src/ui/chat/ChatComposer.js +1 -10
  44. package/src/ui/chat/ChatExtendedPromptModal.js +1 -12
  45. package/src/ui/chat/ChatWindow.js +167 -36
  46. package/src/ui/chat/ChatWindowRenderer.js +1 -8
  47. package/src/ui/chat/icons.js +17 -5
  48. package/src/ui/connectors/ConnectionAnchorsLayer.js +231 -0
  49. package/src/ui/connectors/ConnectorLayer.js +251 -0
  50. package/src/ui/handles/HandlesDomRenderer.js +11 -7
  51. package/src/ui/handles/HandlesInteractionController.js +65 -34
  52. package/src/ui/handles/HandlesPositioningService.js +41 -6
  53. package/src/ui/mindmap/MindmapCollapseGraph.js +169 -0
  54. package/src/ui/mindmap/MindmapCollapseLayer.js +380 -0
  55. package/src/ui/mindmap/MindmapConnectionLayer.js +50 -25
  56. package/src/ui/mindmap/MindmapHtmlTextLayer.js +223 -2
  57. package/src/ui/mindmap/MindmapLayoutConfig.js +12 -0
  58. package/src/ui/styles/chat.css +2 -37
  59. package/src/ui/styles/toolbar.css +6 -0
  60. package/src/ui/styles/workspace.css +83 -21
  61. package/src/ui/toolbar/ToolbarPopupsController.js +1 -1
@@ -13,7 +13,7 @@ const CLOSE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12
13
13
 
14
14
  export class ChatComposer {
15
15
  /**
16
- * @param {{ textarea: HTMLTextAreaElement, send: HTMLButtonElement, attach: HTMLButtonElement, fileInput: HTMLInputElement, attachmentsPreview: HTMLElement, enhancePrompt?: HTMLButtonElement, statusBar?: HTMLElement }} refs
16
+ * @param {{ textarea: HTMLTextAreaElement, send: HTMLButtonElement, attach: HTMLButtonElement, fileInput: HTMLInputElement, attachmentsPreview: HTMLElement, statusBar?: HTMLElement }} refs
17
17
  * @param {{ onSubmit: (text: string, attachments: File[]) => void, onAbort: () => void }} handlers
18
18
  */
19
19
  constructor(refs, handlers) {
@@ -22,7 +22,6 @@ export class ChatComposer {
22
22
  this._attach = refs.attach ?? null;
23
23
  this._fileInput = refs.fileInput ?? null;
24
24
  this._attachmentsPreview = refs.attachmentsPreview ?? null;
25
- this._enhancePrompt = refs.enhancePrompt ?? null;
26
25
  this._statusBar = refs.statusBar ?? null;
27
26
  this._handlers = handlers;
28
27
  this._listeners = [];
@@ -97,11 +96,6 @@ export class ChatComposer {
97
96
  if (!trimmed && !hasAttachments) return;
98
97
  if (this._send.dataset.state === 'streaming') return;
99
98
  const attachments = [...this._attachments];
100
- this._attachments = [];
101
- this._textarea.value = '';
102
- this._resizeTextarea();
103
- this._renderAttachmentsPreview();
104
- this._refreshSendState();
105
99
  this._handlers.onSubmit?.(trimmed, attachments);
106
100
  }
107
101
 
@@ -110,9 +104,6 @@ export class ChatComposer {
110
104
  const hasAttachments = this._attachments.length > 0;
111
105
  this._send.dataset.state = (hasText || hasAttachments) ? 'ready' : 'idle';
112
106
  this._send.disabled = false;
113
- if (this._enhancePrompt) {
114
- this._enhancePrompt.dataset.empty = hasText ? 'false' : 'true';
115
- }
116
107
  }
117
108
 
118
109
  _handleFileChange() {
@@ -36,11 +36,6 @@ export class ChatExtendedPromptModal {
36
36
  this._sourceTextarea.value = '';
37
37
  this._sourceTextarea.dispatchEvent(new Event('input', { bubbles: true }));
38
38
  });
39
-
40
- this._on(this._refs.enhanceBtn, 'click', () => {
41
- const originalEnhance = this._sourceTextarea.parentElement?.querySelector('.moodboard-chat__input-icon-btn--enhance-prompt');
42
- if (originalEnhance) originalEnhance.click();
43
- });
44
39
  }
45
40
 
46
41
  show() {
@@ -111,13 +106,7 @@ export class ChatExtendedPromptModal {
111
106
  clearBtn.className = 'moodboard-chat__extended-clear';
112
107
  clearBtn.textContent = 'Очистить';
113
108
 
114
- const enhanceBtn = document.createElement('button');
115
- enhanceBtn.type = 'button';
116
- enhanceBtn.className = 'moodboard-chat__extended-enhance';
117
- enhanceBtn.innerHTML = `<span class="moodboard-chat__extended-enhance-icon">${ICONS.enhancePrompt}</span> Улучшить`;
118
-
119
109
  actions.appendChild(clearBtn);
120
- actions.appendChild(enhanceBtn);
121
110
 
122
111
  body.appendChild(textarea);
123
112
  body.appendChild(actions);
@@ -126,6 +115,6 @@ export class ChatExtendedPromptModal {
126
115
  modal.appendChild(body);
127
116
  overlay.appendChild(modal);
128
117
 
129
- return { overlay, modal, header, title, closeBtn, body, textarea, clearBtn, enhanceBtn };
118
+ return { overlay, modal, header, title, closeBtn, body, textarea, clearBtn };
130
119
  }
131
120
  }
@@ -56,6 +56,10 @@ const BOARD_IMAGE_STEP = 320;
56
56
  const BOARD_IMAGE_GAP = BOARD_IMAGE_STEP - BOARD_IMAGE_WIDTH;
57
57
  // Скорость перестановки AI-изображений на доске и въезда заглушек регулируется здесь.
58
58
  const BOARD_IMAGE_REARRANGE_MS = 520;
59
+ // На сколько колонок «справа» появляется блок-заглушка перед въездом в финальную позицию.
60
+ const BOARD_IMAGE_PENDING_ENTER_FACTOR = 1.6;
61
+ // Каскад между блоками одного батча (мс): пользователь видит, что они приезжают друг за другом.
62
+ const BOARD_IMAGE_PENDING_STAGGER_MS = 90;
59
63
  const REFERENCE_DRAG_PREVIEW_SIZE = 96;
60
64
 
61
65
  const MODEL_OPTIONS = [
@@ -68,25 +72,25 @@ const MODEL_OPTIONS = [
68
72
  {
69
73
  id: 'yandex',
70
74
  label: 'Алиса',
71
- icon: '<img src="/icons/alice.png" width="36" height="36" alt="Алиса" style="object-fit: contain;" />',
75
+ icon: ICONS.modelAlice,
72
76
  description: 'YandexGPT'
73
77
  },
74
78
  {
75
79
  id: 'gpt',
76
80
  label: 'GPT',
77
- icon: '<img src="/icons/gpt.svg" width="36" height="36" alt="GPT" style="object-fit: contain;" />',
81
+ icon: ICONS.modelGpt,
78
82
  description: 'OpenAI'
79
83
  },
80
84
  {
81
85
  id: 'google',
82
86
  label: 'Google',
83
- icon: '<img src="/icons/google.svg" width="36" height="36" alt="Google" style="object-fit: contain;" />',
87
+ icon: ICONS.modelGoogle,
84
88
  description: 'Gemini'
85
89
  },
86
90
  {
87
91
  id: 'qwen',
88
92
  label: 'Qwen',
89
- icon: '<img src="/icons/qwen.svg" width="36" height="36" alt="Qwen" style="object-fit: contain;" />',
93
+ icon: ICONS.modelQwen,
90
94
  description: 'Alibaba'
91
95
  }
92
96
  ];
@@ -137,14 +141,16 @@ export class ChatWindow {
137
141
  this._attached = false;
138
142
  this._boardImageMessageIds = new Set();
139
143
  this._shiftedForImageBatchKeys = new Set();
140
- this._pendingOverlayEls = [];
141
- this._pendingOverlayMessageIds = new Set();
144
+ this._boardImageShiftHistory = new Map();
145
+ this._pendingOverlays = new Map();
146
+ this._pendingOverlayTimers = new Map();
142
147
  this._boardImageShiftAnimations = new Map();
143
148
  this._boardCursor = null;
144
149
  this._draggedReferenceObject = null;
145
150
  this._draggedReferenceStartPosition = null;
146
151
  this._referenceDragPreview = null;
147
152
  this._referenceDragHandlers = null;
153
+ this._clearSelectionOnSendClick = null;
148
154
  }
149
155
 
150
156
  attach() {
@@ -161,14 +167,18 @@ export class ChatWindow {
161
167
  attach: this._refs.attach,
162
168
  fileInput: this._refs.fileInput,
163
169
  attachmentsPreview: this._refs.attachmentsPreview,
164
- enhancePrompt: this._refs.enhancePrompt,
165
170
  statusBar: this._refs.statusBar
166
171
  },
167
172
  {
168
- onSubmit: (text, attachments) => this._session.send(text, this._getImageRequestOptions()),
173
+ onSubmit: (text, attachments) => {
174
+ this._clearBoardSelection();
175
+ return this._session.send(text, { ...this._getImageRequestOptions(), referenceImages: attachments });
176
+ },
169
177
  onAbort: () => this._session.abort()
170
178
  }
171
179
  );
180
+ this._clearSelectionOnSendClick = () => this._clearBoardSelection();
181
+ this._refs.send.addEventListener('click', this._clearSelectionOnSendClick);
172
182
  this._composer.attach();
173
183
  this._attachReferenceDragEvents();
174
184
 
@@ -255,9 +265,13 @@ export class ChatWindow {
255
265
  this._cancelBoardImageShiftAnimations();
256
266
  this._clearReferenceDragState();
257
267
  if (this._unsubscribe) { this._unsubscribe(); this._unsubscribe = null; }
268
+ if (this._clearSelectionOnSendClick && this._refs?.send) {
269
+ this._refs.send.removeEventListener('click', this._clearSelectionOnSendClick);
270
+ this._clearSelectionOnSendClick = null;
271
+ }
258
272
  this._detachReferenceDragEvents();
259
273
  this._shiftedForImageBatchKeys.clear();
260
- this._pendingOverlayMessageIds.clear();
274
+ this._boardImageShiftHistory.clear();
261
275
  this._composer?.destroy();
262
276
  this._extendedPromptModal?.destroy();
263
277
  this._contentTypeMenu?.destroy();
@@ -282,6 +296,15 @@ export class ChatWindow {
282
296
  this.detach();
283
297
  }
284
298
 
299
+ _clearBoardSelection() {
300
+ if (typeof this._boardCore?.selectTool?.clearSelection === 'function') {
301
+ this._boardCore.selectTool.clearSelection();
302
+ return;
303
+ }
304
+
305
+ this._boardCore?.eventBus?.emit(Events.Tool.SelectionClear);
306
+ }
307
+
285
308
  _updateCountPillIcon() {
286
309
  const active = COUNT_OPTIONS.find((o) => o.id === this._countId);
287
310
  if (!active) return;
@@ -314,6 +337,9 @@ export class ChatWindow {
314
337
  _render(state) {
315
338
  if (!this._attached && !this._refs) return;
316
339
  this._syncGeneratedImagesToBoard(state.messages);
340
+ if (state.status !== 'streaming') {
341
+ this._revertFailedBatchShifts(state.messages);
342
+ }
317
343
  this._messageList.render(state.messages);
318
344
  this._contentTypeMenu.refresh();
319
345
  this._modelMenu.refresh();
@@ -327,9 +353,17 @@ export class ChatWindow {
327
353
  }
328
354
 
329
355
  _updatePendingImages(messages) {
330
- this._clearPendingOverlays();
331
-
332
356
  const pending = (messages || []).filter((m) => m.pending && m.kind === 'image');
357
+ const activeIds = new Set(pending.map((m) => m.id));
358
+
359
+ for (const [id, record] of this._pendingOverlays) {
360
+ if (!activeIds.has(id)) {
361
+ record.el.remove();
362
+ this._pendingOverlays.delete(id);
363
+ this._cancelPendingOverlayTimer(id);
364
+ }
365
+ }
366
+
333
367
  if (pending.length === 0) return;
334
368
 
335
369
  const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
@@ -341,27 +375,31 @@ export class ChatWindow {
341
375
  const ratio = wr / hr;
342
376
  const wScreen = Math.round(BOARD_IMAGE_WIDTH * s);
343
377
  const hScreen = Math.round(wScreen / ratio);
378
+ const enterDistance = Math.round(BOARD_IMAGE_STEP * s * BOARD_IMAGE_PENDING_ENTER_FACTOR);
344
379
 
345
- for (const message of pending) {
380
+ let newIndex = 0;
381
+ pending.forEach((message) => {
346
382
  const slot = this._getImageBatchSlot(messages, message.id, s);
347
383
  const left = Math.round(slot.x - wScreen / 2);
348
384
  const top = Math.round(slot.y - hScreen / 2);
349
385
 
386
+ const existing = this._pendingOverlays.get(message.id);
387
+ if (existing) {
388
+ const el = existing.el;
389
+ el.style.left = `${left}px`;
390
+ el.style.top = `${top}px`;
391
+ el.style.width = `${wScreen}px`;
392
+ el.style.height = `${hScreen}px`;
393
+ el.style.setProperty('--moodboard-chat-board-animation-ms', `${BOARD_IMAGE_REARRANGE_MS}ms`);
394
+ el.style.setProperty('--moodboard-chat-pending-enter-x', `${enterDistance}px`);
395
+ return;
396
+ }
397
+
350
398
  const overlay = document.createElement('div');
351
- overlay.className = 'moodboard-chat__pending-overlay';
399
+ overlay.className = 'moodboard-chat__pending-overlay moodboard-chat__pending-overlay--enter';
352
400
  overlay.style.cssText = `left:${left}px;top:${top}px;width:${wScreen}px;height:${hScreen}px`;
353
401
  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
- }
402
+ overlay.style.setProperty('--moodboard-chat-pending-enter-x', `${enterDistance}px`);
365
403
 
366
404
  const label = document.createElement('span');
367
405
  label.className = 'moodboard-chat__pending-image-label';
@@ -369,15 +407,52 @@ export class ChatWindow {
369
407
  overlay.appendChild(label);
370
408
 
371
409
  document.body.appendChild(overlay);
372
- this._pendingOverlayEls.push(overlay);
410
+
411
+ // Принудительный reflow: фиксируем стартовое состояние (translateX справа + opacity 0)
412
+ // в layout до переключения класса. Без этого браузер может смерджить два состояния
413
+ // в один кадр и transition не запустится — заглушка появится мгновенно.
414
+ void overlay.offsetWidth;
415
+
416
+ this._pendingOverlays.set(message.id, { el: overlay });
417
+
418
+ const stagger = newIndex * BOARD_IMAGE_PENDING_STAGGER_MS;
419
+ newIndex += 1;
420
+
421
+ const trigger = () => {
422
+ if (!overlay.isConnected) return;
423
+ overlay.classList.remove('moodboard-chat__pending-overlay--enter');
424
+ overlay.classList.add('moodboard-chat__pending-overlay--entered');
425
+ };
426
+
427
+ if (stagger > 0) {
428
+ const timer = setTimeout(() => {
429
+ this._pendingOverlayTimers.delete(message.id);
430
+ this._scheduleAnimationFrame(trigger);
431
+ }, stagger);
432
+ this._pendingOverlayTimers.set(message.id, timer);
433
+ } else {
434
+ this._scheduleAnimationFrame(trigger);
435
+ }
436
+ });
437
+ }
438
+
439
+ _cancelPendingOverlayTimer(id) {
440
+ const timer = this._pendingOverlayTimers.get(id);
441
+ if (timer !== undefined) {
442
+ clearTimeout(timer);
443
+ this._pendingOverlayTimers.delete(id);
373
444
  }
374
445
  }
375
446
 
376
447
  _clearPendingOverlays() {
377
- for (const el of this._pendingOverlayEls) {
378
- el.remove();
448
+ for (const record of this._pendingOverlays.values()) {
449
+ record.el.remove();
450
+ }
451
+ this._pendingOverlays.clear();
452
+ for (const timer of this._pendingOverlayTimers.values()) {
453
+ clearTimeout(timer);
379
454
  }
380
- this._pendingOverlayEls = [];
455
+ this._pendingOverlayTimers.clear();
381
456
  }
382
457
 
383
458
  _getImageRequestOptions() {
@@ -406,11 +481,15 @@ export class ChatWindow {
406
481
  const onDragEnd = (data) => {
407
482
  void this._handleReferenceDragEnd(data);
408
483
  };
484
+ const onSelectionAdd = (data) => {
485
+ void this._handleSelectionAdd(data);
486
+ };
409
487
 
410
- this._referenceDragHandlers = { onCursorMove, onDragStart, onDragEnd };
488
+ this._referenceDragHandlers = { onCursorMove, onDragStart, onDragEnd, onSelectionAdd };
411
489
  eventBus.on(Events.UI.CursorMove, onCursorMove);
412
490
  eventBus.on(Events.Tool.DragStart, onDragStart);
413
491
  eventBus.on(Events.Tool.DragEnd, onDragEnd);
492
+ eventBus.on(Events.Tool.SelectionAdd, onSelectionAdd);
414
493
  }
415
494
 
416
495
  _detachReferenceDragEvents() {
@@ -421,9 +500,19 @@ export class ChatWindow {
421
500
  eventBus.off(Events.UI.CursorMove, handlers.onCursorMove);
422
501
  eventBus.off(Events.Tool.DragStart, handlers.onDragStart);
423
502
  eventBus.off(Events.Tool.DragEnd, handlers.onDragEnd);
503
+ eventBus.off(Events.Tool.SelectionAdd, handlers.onSelectionAdd);
424
504
  this._referenceDragHandlers = null;
425
505
  }
426
506
 
507
+ async _handleSelectionAdd(data = {}) {
508
+ const objectId = data?.object;
509
+ const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
510
+
511
+ if (!isReferenceImageObject(object)) return;
512
+
513
+ await this._addImageObjectAsReference(object);
514
+ }
515
+
427
516
  _handleReferenceDragStart(data = {}) {
428
517
  const objectId = data?.object;
429
518
  const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
@@ -591,7 +680,7 @@ export class ChatWindow {
591
680
  if (this._shiftedForImageBatchKeys.has(batchKey)) return;
592
681
 
593
682
  this._shiftedForImageBatchKeys.add(batchKey);
594
- this._shiftBoardAiImagesLeft(this._getImageBatchWorldBounds(messages, messageId, scale));
683
+ this._shiftBoardAiImagesLeft(this._getImageBatchWorldBounds(messages, messageId, scale), batchKey);
595
684
  }
596
685
 
597
686
  _getImageBatchWorldBounds(messages, messageId, scale = 1) {
@@ -611,7 +700,7 @@ export class ChatWindow {
611
700
  };
612
701
  }
613
702
 
614
- _shiftBoardAiImagesLeft(nextBatchBounds) {
703
+ _shiftBoardAiImagesLeft(nextBatchBounds, batchKey) {
615
704
  const aiObjects = this._getBoardAiImageObjects();
616
705
  if (aiObjects.length === 0 || !nextBatchBounds) return;
617
706
 
@@ -621,14 +710,56 @@ export class ChatWindow {
621
710
 
622
711
  const ids = new Set(aiObjects.map((object) => object.id));
623
712
  const objects = this._boardCore?.state?.state?.objects;
713
+ const shiftRecord = [];
624
714
  for (const id of ids) {
625
715
  const obj = objects?.find((item) => item.id === id);
626
716
  if (obj?.position) {
627
- this._animateBoardImageToPosition(
628
- id,
629
- obj.position,
630
- { x: Math.round(obj.position.x - shift), y: obj.position.y }
631
- );
717
+ const from = { x: obj.position.x, y: obj.position.y };
718
+ const to = { x: Math.round(obj.position.x - shift), y: obj.position.y };
719
+ shiftRecord.push({ id, from });
720
+ this._animateBoardImageToPosition(id, from, to);
721
+ }
722
+ }
723
+
724
+ if (batchKey && shiftRecord.length > 0) {
725
+ this._boardImageShiftHistory.set(batchKey, shiftRecord);
726
+ }
727
+ }
728
+
729
+ _revertBoardImageShiftForBatch(batchKey) {
730
+ const record = this._boardImageShiftHistory.get(batchKey);
731
+ if (!record) return;
732
+
733
+ const objects = this._boardCore?.state?.state?.objects;
734
+ for (const { id, from } of record) {
735
+ const obj = objects?.find((item) => item.id === id);
736
+ if (obj?.position) {
737
+ this._animateBoardImageToPosition(id, obj.position, from);
738
+ }
739
+ }
740
+
741
+ this._boardImageShiftHistory.delete(batchKey);
742
+ this._shiftedForImageBatchKeys.delete(batchKey);
743
+ }
744
+
745
+ _revertFailedBatchShifts(messages) {
746
+ if (this._boardImageShiftHistory.size === 0) return;
747
+
748
+ for (const batchKey of [...this._boardImageShiftHistory.keys()]) {
749
+ if (batchKey === 'unknown') continue;
750
+
751
+ const messageIds = batchKey.split('|');
752
+ const batchMessages = messageIds
753
+ .map((id) => messages?.find((m) => m.id === id))
754
+ .filter(Boolean);
755
+
756
+ if (batchMessages.length === 0) continue;
757
+
758
+ const allResolved = batchMessages.every((m) => !m.pending);
759
+ const anyImage = batchMessages.some((m) => Boolean(m.imageBase64));
760
+
761
+ if (allResolved && !anyImage) {
762
+ this._revertBoardImageShiftForBatch(batchKey);
632
763
  }
633
764
  }
634
765
  }
@@ -67,26 +67,19 @@ function buildInputRow(collect) {
67
67
  const promptActionsWrapper = document.createElement('div');
68
68
  promptActionsWrapper.className = 'moodboard-chat__pill-wrapper';
69
69
 
70
- const enhancePrompt = createInputIconButton(
71
- 'enhance-prompt',
72
- 'Улучшить промпт',
73
- ICONS.enhancePrompt
74
- );
75
- enhancePrompt.dataset.empty = 'true';
76
70
  const extendPromptField = createInputIconButton(
77
71
  'extend-promt-field',
78
72
  'Развернуть поле ввода',
79
73
  ICONS.extendPromptField
80
74
  );
81
75
 
82
- promptActionsWrapper.appendChild(enhancePrompt);
83
76
  promptActionsWrapper.appendChild(extendPromptField);
84
77
  textareaRow.appendChild(textarea);
85
78
  textareaRow.appendChild(promptActionsWrapper);
86
79
  row.appendChild(attachmentsPreview);
87
80
  row.appendChild(textareaRow);
88
81
 
89
- collect({ textarea, enhancePrompt, extendPromptField, attachmentsPreview });
82
+ collect({ textarea, extendPromptField, attachmentsPreview });
90
83
  return row;
91
84
  }
92
85
 
@@ -39,12 +39,21 @@ const ATTACHMENTS_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" hei
39
39
  /** Кнопка отправки в composer */
40
40
  const SEND_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M7.453 1.204a.87.87 0 0 1 1.093 0l.066.06 4.962 4.961a.6.6 0 0 1-.849.849L8.6 2.949v11.45a.6.6 0 0 1-1.199 0V2.946L3.273 7.074a.6.6 0 1 1-.847-.849l4.96-4.962z"/></svg>`;
41
41
 
42
- /** public/icons/enhance-prompt.svg */
43
- const ENHANCE_PROMPT_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 11.771a.6.6 0 1 1 0 1.2L1.6 13a.6.6 0 0 1 0-1.2zm2-4.37A.6.6 0 1 1 10 8.6H1.6a.6.6 0 1 1 0-1.2zM14.4 3a.6.6 0 1 1 0 1.2H1.6a.6.6 0 1 1 0-1.2zm-1.101 5.5a.346.346 0 0 0-.643 0l-.174.417a2.98 2.98 0 0 1-1.538 1.59l-.489.218a.362.362 0 0 0 0 .658l.519.231c.675.3 1.216.85 1.516 1.538l.168.387a.347.347 0 0 0 .639 0l.168-.387c.3-.689.841-1.237 1.516-1.538l.519-.231a.362.362 0 0 0 0-.658l-.49-.218a3 3 0 0 1-1.538-1.59z"/></svg>`;
44
-
45
42
  /** public/icons/extend-promt-field.svg */
46
43
  const EXTEND_PROMPT_FIELD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M9.696 7.151a.599.599 0 1 1-.847-.847L12.55 2.6H9.272a.6.6 0 0 1 0-1.2H14a.6.6 0 0 1 .6.599v4.728a.6.6 0 1 1-1.2 0V3.449zM1.4 9.272a.6.6 0 1 1 1.2 0v3.28l3.704-3.703a.599.599 0 1 1 .847.847L3.45 13.4h3.279a.6.6 0 0 1 0 1.2H2A.6.6 0 0 1 1.4 14z"/></svg>`;
47
44
 
45
+ /** public/icons/google.svg — цветной логотип Google 36×36 */
46
+ const MODEL_GOOGLE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none" viewBox="0 0 36 36"><path fill="#4285F4" d="M29.251 18.49c0-.813-.073-1.596-.209-2.347H18.23v4.445h6.179c-.272 1.43-1.086 2.64-2.307 3.455v2.89h3.726c2.17-2.003 3.423-4.946 3.423-8.442"/><path fill="#34A853" d="M18.229 29.71c3.1 0 5.698-1.023 7.597-2.776l-3.725-2.89c-1.023.688-2.328 1.105-3.872 1.105-2.985 0-5.52-2.014-6.429-4.727H7.98v2.964c1.89 3.746 5.761 6.324 10.249 6.324"/><path fill="#FBBC05" d="M11.801 20.412a6.9 6.9 0 0 1-.365-2.181c0-.762.136-1.492.365-2.181v-2.964h-3.82A11.34 11.34 0 0 0 6.75 18.23c0 1.858.449 3.6 1.231 5.145l2.975-2.317z"/><path fill="#EA4335" d="M18.229 11.321c1.69 0 3.193.585 4.393 1.712l3.288-3.288c-1.994-1.857-4.582-2.995-7.681-2.995-4.488 0-8.36 2.578-10.249 6.335l3.82 2.964c.908-2.714 3.444-4.728 6.429-4.728"/></svg>`;
47
+
48
+ /** Placeholder OpenAI GPT — буква G в круге, 36×36 */
49
+ const MODEL_GPT_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="#10a37f"/><text x="18" y="23" text-anchor="middle" font-size="16" font-family="Arial,sans-serif" fill="#fff" font-weight="bold">G</text></svg>`;
50
+
51
+ /** Placeholder Alibaba Qwen — буква Q в круге, 36×36 */
52
+ const MODEL_QWEN_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="#6e42ca"/><text x="18" y="23" text-anchor="middle" font-size="16" font-family="Arial,sans-serif" fill="#fff" font-weight="bold">Q</text></svg>`;
53
+
54
+ /** Placeholder Yandex Alice — буква А в круге, 36×36 */
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
+
48
57
  export const ICONS = {
49
58
  image: IMAGE_ICON,
50
59
  video: VIDEO_ICON,
@@ -57,13 +66,16 @@ export const ICONS = {
57
66
  palette: svg('<path d="M12 3a9 9 0 0 0 0 18c1.7 0 2-1 2-2 0-1.5 1-2 2-2h2a3 3 0 0 0 3-3c0-5-4-9-9-9z"/><circle cx="7.5" cy="11" r="1"/><circle cx="11" cy="7" r="1"/><circle cx="15" cy="7" r="1"/><circle cx="17.5" cy="11" r="1"/>'),
58
67
  attach: ATTACHMENTS_ICON,
59
68
  send: SEND_ICON,
60
- enhancePrompt: ENHANCE_PROMPT_ICON,
61
69
  extendPromptField: EXTEND_PROMPT_FIELD_ICON,
62
70
  sliders: svg('<path d="M4 7h10M18 7h2M4 17h2M10 17h10"/><circle cx="16" cy="7" r="2"/><circle cx="8" cy="17" r="2"/>'),
63
71
  chevronDown: svg('<path d="M6 9l6 6 6-6"/>'),
64
72
  trash: svg('<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M6 6l1 14a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-14"/>'),
65
73
  close: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M13.575 12.726a.6.6 0 0 1-.849.849zm-.849-10.3a.6.6 0 0 1 .849.848L8.848 7.999l4.727 4.727-.425.424-.424.425-4.727-4.727-4.725 4.727a.6.6 0 0 1-.848-.849l4.726-4.727-4.726-4.725a.599.599 0 1 1 .848-.848l4.725 4.726z"></path></svg>`,
66
- sparkles: svg('<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/>')
74
+ sparkles: svg('<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/>'),
75
+ modelGoogle: MODEL_GOOGLE_ICON,
76
+ modelGpt: MODEL_GPT_ICON,
77
+ modelQwen: MODEL_QWEN_ICON,
78
+ modelAlice: MODEL_ALICE_ICON,
67
79
  };
68
80
 
69
81
  /**