@sequent-org/moodboard 1.4.31 → 1.4.33

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 (139) hide show
  1. package/package.json +5 -1
  2. package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
  3. package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
  4. package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  5. package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  6. package/src/assets/icons/attachments.svg +3 -1
  7. package/src/assets/icons/comments.svg +2 -2
  8. package/src/assets/icons/connector.svg +6 -0
  9. package/src/assets/icons/emoji.svg +6 -1
  10. package/src/assets/icons/frame.svg +4 -1
  11. package/src/assets/icons/image.svg +5 -1
  12. package/src/assets/icons/laser.svg +1 -0
  13. package/src/assets/icons/lasso.svg +5 -0
  14. package/src/assets/icons/mindmap.svg +10 -2
  15. package/src/assets/icons/note.svg +4 -1
  16. package/src/assets/icons/pan.svg +5 -2
  17. package/src/assets/icons/pencil.svg +4 -1
  18. package/src/assets/icons/reactions.svg +5 -0
  19. package/src/assets/icons/redo.svg +3 -2
  20. package/src/assets/icons/select.svg +2 -8
  21. package/src/assets/icons/shapes.svg +5 -1
  22. package/src/assets/icons/text-add.svg +15 -1
  23. package/src/assets/icons/undo.svg +3 -2
  24. package/src/assets/reactions/1f44d.svg +20 -0
  25. package/src/assets/reactions/1f44e.svg +20 -0
  26. package/src/assets/reactions/2705.svg +20 -0
  27. package/src/assets/reactions/274c.svg +19 -0
  28. package/src/assets/reactions/2753.svg +20 -0
  29. package/src/assets/reactions/2764.svg +22 -0
  30. package/src/assets/reactions/2b50.svg +19 -0
  31. package/src/assets/reactions/plus-one.svg +25 -0
  32. package/src/core/PixiEngine.js +23 -0
  33. package/src/core/bootstrap/CoreInitializer.js +43 -0
  34. package/src/core/commands/GroupDeleteCommand.js +13 -1
  35. package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
  36. package/src/core/commands/UpdateTextStyleCommand.js +17 -6
  37. package/src/core/commands/index.js +3 -0
  38. package/src/core/events/Events.js +22 -0
  39. package/src/core/flows/LayerAndViewportFlow.js +1 -0
  40. package/src/core/flows/ObjectLifecycleFlow.js +155 -7
  41. package/src/core/index.js +28 -1
  42. package/src/grid/CrossGridZoomPhases.js +3 -3
  43. package/src/initNoBundler.js +1 -1
  44. package/src/moodboard/DataManager.js +28 -0
  45. package/src/moodboard/MoodBoard.js +27 -0
  46. package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
  47. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
  48. package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
  49. package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
  50. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
  51. package/src/objects/ConnectorObject.js +2 -2
  52. package/src/objects/FrameObject.js +119 -59
  53. package/src/objects/ShapeObject.js +49 -74
  54. package/src/objects/shape/ShapeDrawer.js +210 -0
  55. package/src/services/ConnectorBindingResolver.js +112 -0
  56. package/src/services/ConnectorRouter.js +210 -0
  57. package/src/services/comments/CommentService.js +344 -0
  58. package/src/tools/object-tools/CommentTool.js +85 -0
  59. package/src/tools/object-tools/DrawingTool.js +110 -10
  60. package/src/tools/object-tools/LaserPointerTool.js +121 -0
  61. package/src/tools/object-tools/SelectTool.js +25 -1
  62. package/src/tools/object-tools/TextTool.js +6 -1
  63. package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
  64. package/src/tools/object-tools/connector/connectorGesture.js +33 -19
  65. package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
  66. package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
  67. package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
  68. package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
  69. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
  70. package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
  71. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
  72. package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
  73. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
  74. package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
  75. package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
  76. package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
  77. package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
  78. package/src/ui/CommentPopover.js +6 -0
  79. package/src/ui/CommentsBar.js +91 -0
  80. package/src/ui/ConnectorPropertiesPanel.js +150 -0
  81. package/src/ui/ContextMenu.js +25 -0
  82. package/src/ui/DrawingPropertiesPanel.js +362 -0
  83. package/src/ui/FilePropertiesPanel.js +5 -0
  84. package/src/ui/FramePropertiesPanel.js +5 -0
  85. package/src/ui/HtmlTextLayer.js +246 -66
  86. package/src/ui/NotePropertiesPanel.js +6 -0
  87. package/src/ui/ShapePropertiesPanel.js +307 -0
  88. package/src/ui/TextPropertiesPanel.js +100 -1
  89. package/src/ui/Toolbar.js +25 -2
  90. package/src/ui/Topbar.js +2 -2
  91. package/src/ui/animation/HoverLiftController.js +6 -7
  92. package/src/ui/chat/ChatComposer.js +59 -12
  93. package/src/ui/chat/ChatExtendedPromptModal.js +1 -12
  94. package/src/ui/chat/ChatWindow.js +60 -144
  95. package/src/ui/chat/ChatWindowRenderer.js +1 -8
  96. package/src/ui/chat/icons.js +0 -4
  97. package/src/ui/comments/CommentListPanel.js +213 -0
  98. package/src/ui/comments/CommentPinLayer.js +448 -0
  99. package/src/ui/comments/CommentThreadPopover.js +539 -0
  100. package/src/ui/comments/commentFormat.js +32 -0
  101. package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
  102. package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
  103. package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
  104. package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
  105. package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
  106. package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
  107. package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
  108. package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
  109. package/src/ui/connectors/ConnectorLayer.js +264 -57
  110. package/src/ui/handles/HandlesDomRenderer.js +5 -13
  111. package/src/ui/handles/HandlesEventBridge.js +1 -0
  112. package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
  113. package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
  114. package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
  115. package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
  116. package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
  117. package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
  118. package/src/ui/styles/chat.css +682 -28
  119. package/src/ui/styles/index.css +1 -0
  120. package/src/ui/styles/panels.css +112 -2
  121. package/src/ui/styles/shape-properties-panel.css +250 -0
  122. package/src/ui/styles/toolbar.css +7 -2
  123. package/src/ui/styles/topbar.css +1 -1
  124. package/src/ui/styles/workspace.css +257 -6
  125. package/src/ui/text-properties/TextFormatControls.js +88 -0
  126. package/src/ui/text-properties/TextListRenderer.js +137 -0
  127. package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
  128. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
  129. package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
  130. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
  131. package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
  132. package/src/ui/toolbar/ReactionsPopupController.js +88 -0
  133. package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
  134. package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
  135. package/src/ui/toolbar/ToolbarRenderer.js +9 -1
  136. package/src/ui/toolbar/ToolbarStateController.js +4 -1
  137. package/src/utils/iconLoader.js +17 -16
  138. package/src/utils/markdown.js +14 -0
  139. package/src/utils/richText.js +125 -0
@@ -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,11 +22,15 @@ 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 = [];
29
- /** @type {File[]} */
28
+ /**
29
+ * Внутреннее хранилище вложений. `sourceObjectId` нужен для дедупа
30
+ * reference-картинок, которые приходят из box-select / клика по объекту:
31
+ * один объект на доске = одно превью в композере.
32
+ * @type {{ file: File, sourceObjectId: string|null }[]}
33
+ */
30
34
  this._attachments = [];
31
35
  }
32
36
 
@@ -77,13 +81,54 @@ export class ChatComposer {
77
81
  this._textarea.focus();
78
82
  }
79
83
 
80
- addAttachment(file) {
84
+ /**
85
+ * @param {File} file
86
+ * @param {{ sourceObjectId?: string|null }} [options] — `sourceObjectId`
87
+ * передаётся для reference-картинок с доски; дубликаты по этому id игнорируются.
88
+ */
89
+ addAttachment(file, options = {}) {
81
90
  if (!file) return;
82
- this._attachments.push(file);
91
+ const sourceObjectId = options?.sourceObjectId ?? null;
92
+ if (sourceObjectId && this._attachments.some((entry) => entry.sourceObjectId === sourceObjectId)) {
93
+ return;
94
+ }
95
+ this._attachments.push({ file, sourceObjectId });
83
96
  this._renderAttachmentsPreview();
84
97
  this._refreshSendState();
85
98
  }
86
99
 
100
+ hasAttachmentForObject(sourceObjectId) {
101
+ if (!sourceObjectId) return false;
102
+ return this._attachments.some((entry) => entry.sourceObjectId === sourceObjectId);
103
+ }
104
+
105
+ /**
106
+ * Удаляет превью конкретного объекта доски. Вызывается при снятии фокуса с изображения.
107
+ * @param {string} sourceObjectId
108
+ */
109
+ removeAttachmentForObject(sourceObjectId) {
110
+ if (!sourceObjectId) return;
111
+ const before = this._attachments.length;
112
+ this._attachments = this._attachments.filter((entry) => entry.sourceObjectId !== sourceObjectId);
113
+ if (this._attachments.length !== before) {
114
+ this._renderAttachmentsPreview();
115
+ this._refreshSendState();
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Удаляет все превью, добавленные с доски (sourceObjectId !== null).
121
+ * Файловые вложения (скрепка) не затрагиваются.
122
+ */
123
+ removeAllBoardAttachments() {
124
+ const before = this._attachments.length;
125
+ this._attachments = this._attachments.filter((entry) => entry.sourceObjectId === null);
126
+ if (this._attachments.length !== before) {
127
+ this._renderAttachmentsPreview();
128
+ this._refreshSendState();
129
+ }
130
+ }
131
+
87
132
  destroy() {
88
133
  for (const off of this._listeners) off();
89
134
  this._listeners = [];
@@ -96,7 +141,12 @@ export class ChatComposer {
96
141
  const hasAttachments = this._attachments.length > 0;
97
142
  if (!trimmed && !hasAttachments) return;
98
143
  if (this._send.dataset.state === 'streaming') return;
99
- const attachments = [...this._attachments];
144
+ const attachments = this._attachments.map((entry) => entry.file);
145
+ this._textarea.value = '';
146
+ this._attachments = [];
147
+ this._resizeTextarea();
148
+ if (this._attachmentsPreview) this._renderAttachmentsPreview();
149
+ this._refreshSendState();
100
150
  this._handlers.onSubmit?.(trimmed, attachments);
101
151
  }
102
152
 
@@ -105,16 +155,13 @@ export class ChatComposer {
105
155
  const hasAttachments = this._attachments.length > 0;
106
156
  this._send.dataset.state = (hasText || hasAttachments) ? 'ready' : 'idle';
107
157
  this._send.disabled = false;
108
- if (this._enhancePrompt) {
109
- this._enhancePrompt.dataset.empty = hasText ? 'false' : 'true';
110
- }
111
158
  }
112
159
 
113
160
  _handleFileChange() {
114
161
  const files = Array.from(this._fileInput.files || []);
115
162
  if (!files.length) return;
116
163
  for (const file of files) {
117
- this._attachments.push(file);
164
+ this._attachments.push({ file, sourceObjectId: null });
118
165
  }
119
166
  this._fileInput.value = '';
120
167
  this._renderAttachmentsPreview();
@@ -138,8 +185,8 @@ export class ChatComposer {
138
185
  inputRow?.classList.add('has-attachments');
139
186
  this._textarea.placeholder = 'Опишите правку, изменение или стилевое направление эталонного изображения';
140
187
  for (let i = 0; i < this._attachments.length; i++) {
141
- const file = this._attachments[i];
142
- const item = this._buildAttachmentItem(file, i);
188
+ const entry = this._attachments[i];
189
+ const item = this._buildAttachmentItem(entry.file, i);
143
190
  container.appendChild(item);
144
191
  }
145
192
  }
@@ -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
  }
@@ -60,7 +60,6 @@ const BOARD_IMAGE_REARRANGE_MS = 520;
60
60
  const BOARD_IMAGE_PENDING_ENTER_FACTOR = 1.6;
61
61
  // Каскад между блоками одного батча (мс): пользователь видит, что они приезжают друг за другом.
62
62
  const BOARD_IMAGE_PENDING_STAGGER_MS = 90;
63
- const REFERENCE_DRAG_PREVIEW_SIZE = 96;
64
63
 
65
64
  const MODEL_OPTIONS = [
66
65
  {
@@ -145,12 +144,12 @@ export class ChatWindow {
145
144
  this._pendingOverlays = new Map();
146
145
  this._pendingOverlayTimers = new Map();
147
146
  this._boardImageShiftAnimations = new Map();
148
- this._boardCursor = null;
149
- this._draggedReferenceObject = null;
150
- this._draggedReferenceStartPosition = null;
151
- this._referenceDragPreview = null;
152
- this._referenceDragHandlers = null;
153
147
  this._clearSelectionOnSendClick = null;
148
+ this._selectionHandlers = null;
149
+ // Окно от BoxSelectStart до BoxSelectCommit: в это время SelectionAdd
150
+ // приходит на каждый mousemove и не должен пушить превью в чат —
151
+ // финальный набор картинок мы получим из BoxSelectCommit по strict-contains.
152
+ this._boxSelectActive = false;
154
153
  }
155
154
 
156
155
  attach() {
@@ -167,7 +166,6 @@ export class ChatWindow {
167
166
  attach: this._refs.attach,
168
167
  fileInput: this._refs.fileInput,
169
168
  attachmentsPreview: this._refs.attachmentsPreview,
170
- enhancePrompt: this._refs.enhancePrompt,
171
169
  statusBar: this._refs.statusBar
172
170
  },
173
171
  {
@@ -181,7 +179,7 @@ export class ChatWindow {
181
179
  this._clearSelectionOnSendClick = () => this._clearBoardSelection();
182
180
  this._refs.send.addEventListener('click', this._clearSelectionOnSendClick);
183
181
  this._composer.attach();
184
- this._attachReferenceDragEvents();
182
+ this._attachSelectionEvents();
185
183
 
186
184
  this._extendedPromptModal = new ChatExtendedPromptModal(
187
185
  this._container,
@@ -264,13 +262,12 @@ export class ChatWindow {
264
262
  if (!this._attached) return;
265
263
  this._clearPendingOverlays();
266
264
  this._cancelBoardImageShiftAnimations();
267
- this._clearReferenceDragState();
268
265
  if (this._unsubscribe) { this._unsubscribe(); this._unsubscribe = null; }
269
266
  if (this._clearSelectionOnSendClick && this._refs?.send) {
270
267
  this._refs.send.removeEventListener('click', this._clearSelectionOnSendClick);
271
268
  this._clearSelectionOnSendClick = null;
272
269
  }
273
- this._detachReferenceDragEvents();
270
+ this._detachSelectionEvents();
274
271
  this._shiftedForImageBatchKeys.clear();
275
272
  this._boardImageShiftHistory.clear();
276
273
  this._composer?.destroy();
@@ -466,175 +463,94 @@ export class ChatWindow {
466
463
  };
467
464
  }
468
465
 
469
- _attachReferenceDragEvents() {
466
+ _attachSelectionEvents() {
470
467
  const eventBus = this._boardCore?.eventBus;
471
- if (!eventBus || typeof eventBus.on !== 'function' || this._referenceDragHandlers) return;
468
+ if (!eventBus || typeof eventBus.on !== 'function' || this._selectionHandlers) return;
472
469
 
473
- const onCursorMove = ({ x, y } = {}) => {
474
- if (Number.isFinite(x) && Number.isFinite(y)) {
475
- this._boardCursor = { x, y };
476
- this._updateReferenceDragPreview();
477
- }
470
+ const onSelectionAdd = (data) => {
471
+ void this._handleSelectionAdd(data);
478
472
  };
479
- const onDragStart = (data) => {
480
- this._handleReferenceDragStart(data);
473
+ const onSelectionRemove = (data) => {
474
+ const objectId = data?.object;
475
+ if (objectId) this._composer?.removeAttachmentForObject?.(objectId);
481
476
  };
482
- const onDragEnd = (data) => {
483
- void this._handleReferenceDragEnd(data);
477
+ const onSelectionClear = () => {
478
+ this._composer?.removeAllBoardAttachments?.();
484
479
  };
485
- const onSelectionAdd = (data) => {
486
- void this._handleSelectionAdd(data);
480
+ const onBoxSelectStart = () => {
481
+ this._boxSelectActive = true;
482
+ };
483
+ const onBoxSelectCommit = (data) => {
484
+ void this._handleBoxSelectCommit(data);
487
485
  };
488
486
 
489
- this._referenceDragHandlers = { onCursorMove, onDragStart, onDragEnd, onSelectionAdd };
490
- eventBus.on(Events.UI.CursorMove, onCursorMove);
491
- eventBus.on(Events.Tool.DragStart, onDragStart);
492
- eventBus.on(Events.Tool.DragEnd, onDragEnd);
487
+ this._selectionHandlers = {
488
+ onSelectionAdd,
489
+ onSelectionRemove,
490
+ onSelectionClear,
491
+ onBoxSelectStart,
492
+ onBoxSelectCommit
493
+ };
493
494
  eventBus.on(Events.Tool.SelectionAdd, onSelectionAdd);
495
+ eventBus.on(Events.Tool.SelectionRemove, onSelectionRemove);
496
+ eventBus.on(Events.Tool.SelectionClear, onSelectionClear);
497
+ eventBus.on(Events.Tool.BoxSelectStart, onBoxSelectStart);
498
+ eventBus.on(Events.Tool.BoxSelectCommit, onBoxSelectCommit);
494
499
  }
495
500
 
496
- _detachReferenceDragEvents() {
501
+ _detachSelectionEvents() {
497
502
  const eventBus = this._boardCore?.eventBus;
498
- const handlers = this._referenceDragHandlers;
503
+ const handlers = this._selectionHandlers;
499
504
  if (!eventBus || typeof eventBus.off !== 'function' || !handlers) return;
500
505
 
501
- eventBus.off(Events.UI.CursorMove, handlers.onCursorMove);
502
- eventBus.off(Events.Tool.DragStart, handlers.onDragStart);
503
- eventBus.off(Events.Tool.DragEnd, handlers.onDragEnd);
504
506
  eventBus.off(Events.Tool.SelectionAdd, handlers.onSelectionAdd);
505
- this._referenceDragHandlers = null;
507
+ eventBus.off(Events.Tool.SelectionRemove, handlers.onSelectionRemove);
508
+ eventBus.off(Events.Tool.SelectionClear, handlers.onSelectionClear);
509
+ eventBus.off(Events.Tool.BoxSelectStart, handlers.onBoxSelectStart);
510
+ eventBus.off(Events.Tool.BoxSelectCommit, handlers.onBoxSelectCommit);
511
+ this._selectionHandlers = null;
512
+ this._boxSelectActive = false;
506
513
  }
507
514
 
508
515
  async _handleSelectionAdd(data = {}) {
509
- const objectId = data?.object;
510
- const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
511
-
512
- if (!isReferenceImageObject(object)) return;
513
-
514
- await this._addImageObjectAsReference(object);
515
- }
516
+ // Во время box-select каждый mousemove перевыставляет selection и снова
517
+ // эмитит SelectionAdd для тех же id. Финальный набор reference-картинок
518
+ // мы получим из BoxSelectCommit по strict-contains, поэтому здесь молчим.
519
+ if (this._boxSelectActive) return;
516
520
 
517
- _handleReferenceDragStart(data = {}) {
518
521
  const objectId = data?.object;
519
522
  const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
520
- this._draggedReferenceObject = isReferenceImageObject(object) ? object : null;
521
- this._draggedReferenceStartPosition = this._draggedReferenceObject?.position
522
- ? { ...this._draggedReferenceObject.position }
523
- : null;
524
- this._updateReferenceDragPreview();
525
- }
526
523
 
527
- async _handleReferenceDragEnd(data = {}) {
528
- const isDropTarget = this._isBoardCursorOverInput();
529
- const objectId = data?.object;
530
- const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
531
- const startPosition = this._draggedReferenceStartPosition;
532
- this._clearReferenceDragState();
533
- if (!isDropTarget || !isReferenceImageObject(object)) return null;
524
+ if (!isReferenceImageObject(object)) return;
534
525
 
535
- this._restoreReferenceObjectPosition(objectId, startPosition);
536
526
  await this._addImageObjectAsReference(object);
537
527
  }
538
528
 
539
- _restoreReferenceObjectPosition(objectId, position) {
540
- if (!objectId || !position) return;
541
-
542
- const updatePosition = this._boardCore?.updateObjectPositionDirect;
543
- if (typeof updatePosition === 'function') {
544
- updatePosition.call(this._boardCore, objectId, position, { snap: false });
545
- return;
546
- }
547
-
548
- const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
549
- if (object?.position) {
550
- object.position = { ...position };
551
- }
552
- }
529
+ async _handleBoxSelectCommit(data = {}) {
530
+ this._boxSelectActive = false;
531
+ const ids = Array.isArray(data?.selected) ? data.selected : (Array.isArray(data?.objects) ? data.objects : []);
532
+ if (!ids.length || !this._composer) return;
553
533
 
554
- _updateReferenceDragPreview() {
555
- const object = this._draggedReferenceObject;
556
- if (!object || !this._isBoardCursorOverInput()) {
557
- this._hideReferenceDragPreview();
558
- return;
559
- }
560
-
561
- const src = getImageObjectSource(object);
562
- if (!src) {
563
- this._hideReferenceDragPreview();
564
- return;
565
- }
566
-
567
- const preview = this._ensureReferenceDragPreview(object, src);
568
- const { clientX, clientY } = this._getBoardCursorClientPosition();
569
- preview.style.left = `${Math.round(clientX)}px`;
570
- preview.style.top = `${Math.round(clientY)}px`;
571
- this._refs?.textarea?.closest?.('.moodboard-chat__input-row')?.classList.add('is-reference-drop-target');
572
- }
573
-
574
- _ensureReferenceDragPreview(object, src) {
575
- if (!this._referenceDragPreview) {
576
- const preview = document.createElement('img');
577
- preview.className = 'moodboard-chat__reference-drag-preview';
578
- preview.alt = getImageObjectFileName(object, src);
579
- preview.width = REFERENCE_DRAG_PREVIEW_SIZE;
580
- preview.height = REFERENCE_DRAG_PREVIEW_SIZE;
581
- document.body.appendChild(preview);
582
- this._referenceDragPreview = preview;
583
- }
584
-
585
- if (this._referenceDragPreview.src !== src) {
586
- this._referenceDragPreview.src = src;
534
+ const all = this._boardCore?.state?.state?.objects ?? [];
535
+ const byId = new Map(all.map((item) => [item?.id, item]));
536
+ const seen = new Set();
537
+ for (const id of ids) {
538
+ if (!id || seen.has(id)) continue;
539
+ seen.add(id);
540
+ const obj = byId.get(id);
541
+ if (!obj || !isReferenceImageObject(obj)) continue;
542
+ await this._addImageObjectAsReference(obj);
587
543
  }
588
- this._referenceDragPreview.alt = getImageObjectFileName(object, src);
589
-
590
- return this._referenceDragPreview;
591
- }
592
-
593
- _hideReferenceDragPreview() {
594
- this._referenceDragPreview?.remove();
595
- this._referenceDragPreview = null;
596
- this._refs?.textarea?.closest?.('.moodboard-chat__input-row')?.classList.remove('is-reference-drop-target');
597
- }
598
-
599
- _clearReferenceDragState() {
600
- this._draggedReferenceObject = null;
601
- this._draggedReferenceStartPosition = null;
602
- this._boardCursor = null;
603
- this._hideReferenceDragPreview();
604
- }
605
-
606
- _isBoardCursorOverInput() {
607
- const cursor = this._boardCursor;
608
- const inputRow = this._refs?.textarea?.closest?.('.moodboard-chat__input-row');
609
- if (!cursor || !inputRow) return false;
610
-
611
- const containerRect = this._container.getBoundingClientRect?.();
612
- const rect = inputRow.getBoundingClientRect();
613
- const { clientX, clientY } = this._getBoardCursorClientPosition(containerRect);
614
-
615
- return clientX >= rect.left
616
- && clientX <= rect.right
617
- && clientY >= rect.top
618
- && clientY <= rect.bottom;
619
- }
620
-
621
- _getBoardCursorClientPosition(containerRect = null) {
622
- const rect = containerRect || this._container.getBoundingClientRect?.();
623
- const cursor = this._boardCursor || { x: 0, y: 0 };
624
-
625
- return {
626
- clientX: (rect?.left || 0) + cursor.x,
627
- clientY: (rect?.top || 0) + cursor.y
628
- };
629
544
  }
630
545
 
631
546
  async _addImageObjectAsReference(object) {
632
547
  if (!object || !this._composer) return;
548
+ if (this._composer.hasAttachmentForObject?.(object.id)) return;
633
549
 
634
550
  try {
635
551
  const file = await createFileFromImageObject(object);
636
552
  if (!file) return;
637
- this._composer.addAttachment(file);
553
+ this._composer.addAttachment(file, { sourceObjectId: object.id });
638
554
  this._composer.focus();
639
555
  } catch (err) {
640
556
  console.warn('[ChatWindow] cannot add selected image reference:', err);
@@ -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,9 +39,6 @@ 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
 
@@ -69,7 +66,6 @@ export const ICONS = {
69
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"/>'),
70
67
  attach: ATTACHMENTS_ICON,
71
68
  send: SEND_ICON,
72
- enhancePrompt: ENHANCE_PROMPT_ICON,
73
69
  extendPromptField: EXTEND_PROMPT_FIELD_ICON,
74
70
  sliders: svg('<path d="M4 7h10M18 7h2M4 17h2M10 17h10"/><circle cx="16" cy="7" r="2"/><circle cx="8" cy="17" r="2"/>'),
75
71
  chevronDown: svg('<path d="M6 9l6 6 6-6"/>'),