@sequent-org/moodboard 1.4.4 → 1.4.6

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/core/ApiClient.js +30 -86
  3. package/src/core/HistoryManager.js +0 -102
  4. package/src/core/SaveManager.js +27 -0
  5. package/src/core/commands/CreateObjectCommand.js +1 -9
  6. package/src/core/commands/DeleteObjectCommand.js +19 -88
  7. package/src/core/commands/EditFileNameCommand.js +1 -2
  8. package/src/core/commands/GroupDeleteCommand.js +11 -44
  9. package/src/core/commands/GroupMoveCommand.js +1 -26
  10. package/src/core/commands/GroupReorderZCommand.js +1 -2
  11. package/src/core/commands/GroupResizeCommand.js +1 -4
  12. package/src/core/commands/GroupRotateCommand.js +1 -9
  13. package/src/core/commands/MindmapStatePatchCommand.js +1 -1
  14. package/src/core/commands/MoveObjectCommand.js +1 -3
  15. package/src/core/commands/PasteObjectCommand.js +1 -10
  16. package/src/core/commands/ReorderZCommand.js +1 -1
  17. package/src/core/commands/ResizeObjectCommand.js +1 -3
  18. package/src/core/commands/RotateObjectCommand.js +1 -9
  19. package/src/core/commands/UpdateContentCommand.js +1 -1
  20. package/src/core/commands/UpdateFramePropertiesCommand.js +1 -1
  21. package/src/core/commands/UpdateFrameTypeCommand.js +1 -1
  22. package/src/core/commands/UpdateNoteStyleCommand.js +1 -1
  23. package/src/core/commands/UpdateTextStyleCommand.js +1 -1
  24. package/src/core/events/Events.js +0 -2
  25. package/src/core/flows/ClipboardFlow.js +50 -14
  26. package/src/core/flows/SaveFlow.js +5 -0
  27. package/src/core/index.js +32 -0
  28. package/src/core/keyboard/KeyboardClipboardImagePaste.js +6 -30
  29. package/src/services/FileUploadService.js +2 -2
  30. package/src/services/ImageUploadService.js +2 -2
  31. package/src/tools/manager/ToolEventRouter.js +34 -74
  32. package/src/tools/object-tools/PlacementTool.js +5 -29
  33. package/src/tools/object-tools/placement/PlacementInputRouter.js +1 -6
  34. package/src/tools/object-tools/placement/PlacementPayloadFactory.js +0 -29
@@ -47,32 +47,7 @@ export class GroupMoveCommand extends BaseCommand {
47
47
  }
48
48
 
49
49
  undo() {
50
- // Возвращаем исходные позиции
51
- for (const item of this.moves) {
52
- if (this.coordinatesAreTopLeft) {
53
- // Координаты уже левый-верх (Frame перемещение)
54
- this.core.updateObjectPositionDirect(item.id, item.from);
55
- this.emit(Events.Object.TransformUpdated, {
56
- objectId: item.id,
57
- type: 'position',
58
- position: item.from
59
- });
60
- } else {
61
- // Координаты - центры PIXI (обычное групповое перемещение)
62
- const pixiObject = this.core?.pixi?.objects?.get(item.id);
63
- if (pixiObject) {
64
- const halfW = (pixiObject.width || 0) / 2;
65
- const halfH = (pixiObject.height || 0) / 2;
66
- const topLeft = { x: item.from.x - halfW, y: item.from.y - halfH };
67
- this.core.updateObjectPositionDirect(item.id, topLeft);
68
- this.emit(Events.Object.TransformUpdated, {
69
- objectId: item.id,
70
- type: 'position',
71
- position: topLeft
72
- });
73
- }
74
- }
75
- }
50
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
76
51
  }
77
52
 
78
53
  getDescription() {
@@ -22,8 +22,7 @@ export class GroupReorderZCommand extends BaseCommand {
22
22
  }
23
23
 
24
24
  undo() {
25
- if (!this.beforeOrder) return;
26
- this.applyOrder(this.beforeOrder);
25
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
27
26
  }
28
27
 
29
28
  applyOrder(idOrder) {
@@ -23,10 +23,7 @@ export class GroupResizeCommand extends BaseCommand {
23
23
  }
24
24
 
25
25
  undo() {
26
- for (const c of this.changes) {
27
- this.core.updateObjectSizeAndPositionDirect(c.id, c.fromSize, c.fromPos, c.type || null);
28
- this.emit(Events.Object.TransformUpdated, { objectId: c.id, type: 'resize', size: c.fromSize, position: c.fromPos });
29
- }
26
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
30
27
  }
31
28
 
32
29
  getDescription() {
@@ -28,15 +28,7 @@ export class GroupRotateCommand extends BaseCommand {
28
28
  }
29
29
 
30
30
  undo() {
31
- for (const c of this.changes) {
32
- if (this.core.pixi?.updateObjectRotation) {
33
- this.core.pixi.updateObjectRotation(c.id, c.fromAngle);
34
- }
35
- this.core.updateObjectRotationDirect(c.id, c.fromAngle);
36
- this.core.updateObjectPositionDirect(c.id, c.fromPos);
37
- this.emit(Events.Object.TransformUpdated, { objectId: c.id, type: 'rotation', angle: c.fromAngle });
38
- this.emit(Events.Object.TransformUpdated, { objectId: c.id, type: 'position', position: c.fromPos });
39
- }
31
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
40
32
  }
41
33
 
42
34
  getDescription() {
@@ -29,7 +29,7 @@ export class MindmapStatePatchCommand extends BaseCommand {
29
29
  }
30
30
 
31
31
  undo() {
32
- this._applyEntries(this.beforeEntries);
32
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
33
33
  }
34
34
 
35
35
  _applyEntries(entries) {
@@ -23,9 +23,7 @@ export class MoveObjectCommand extends BaseCommand {
23
23
  }
24
24
 
25
25
  undo() {
26
- // Возвращаем старую позицию
27
- this._setPosition(this.oldPosition);
28
-
26
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
29
27
  }
30
28
 
31
29
  _setPosition(position) {
@@ -96,16 +96,7 @@ export class PasteObjectCommand extends BaseCommand {
96
96
  }
97
97
 
98
98
  undo() {
99
- if (this.newObjectId) {
100
- // Удаляем созданный объект
101
- this.coreMoodboard.state.removeObject(this.newObjectId);
102
- this.coreMoodboard.pixi.removeObject(this.newObjectId);
103
-
104
- // Соответствующего константного события нет — остаёмся без эмита или используем Object.Deleted, если надо глобально
105
- this.emit(Events.Object.Deleted, {
106
- objectId: this.newObjectId
107
- });
108
- }
99
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
109
100
  }
110
101
 
111
102
  getDescription() {
@@ -18,7 +18,7 @@ export class ReorderZCommand extends BaseCommand {
18
18
  }
19
19
 
20
20
  undo() {
21
- this.apply(this.toIndex, this.fromIndex);
21
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
22
22
  }
23
23
 
24
24
  apply(from, to) {
@@ -25,9 +25,7 @@ export class ResizeObjectCommand extends BaseCommand {
25
25
  }
26
26
 
27
27
  undo() {
28
- // Возвращаем старый размер и позицию
29
- this._setSizeAndPosition(this.oldSize, this.oldPosition);
30
- this._updateResizeHandles();
28
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
31
29
  }
32
30
 
33
31
  _setSizeAndPosition(size, position = null) {
@@ -25,15 +25,7 @@ export class RotateObjectCommand extends BaseCommand {
25
25
  }
26
26
 
27
27
  undo() {
28
- // Обновляем угол поворота в состоянии
29
- this._setRotation(this.oldAngle);
30
-
31
- // Возвращаем старый угол поворота
32
- this.emit(Events.Object.Rotate, {
33
- objectId: this.objectId,
34
- angle: this.oldAngle
35
- });
36
- console.log(`↩️ Отменяем поворот объекта ${this.objectId}, возвращаем ${this.oldAngle}°`);
28
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
37
29
  }
38
30
 
39
31
  /**
@@ -22,7 +22,7 @@ export class UpdateContentCommand extends BaseCommand {
22
22
  }
23
23
 
24
24
  undo() {
25
- this._applyContent(this.oldContent, this.oldSize, this.oldPosition);
25
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
26
26
  }
27
27
 
28
28
  canMergeWith(otherCommand) {
@@ -34,7 +34,7 @@ export class UpdateFramePropertiesCommand extends BaseCommand {
34
34
  }
35
35
 
36
36
  undo() {
37
- this._apply(this.oldValue);
37
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
38
38
  }
39
39
 
40
40
  canMergeWith(otherCommand) {
@@ -32,7 +32,7 @@ export class UpdateFrameTypeCommand extends BaseCommand {
32
32
  }
33
33
 
34
34
  undo() {
35
- this._apply(this.oldType, this.oldSize, this.oldPosition);
35
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
36
36
  }
37
37
 
38
38
  _apply(typeValue, size, position) {
@@ -29,7 +29,7 @@ export class UpdateNoteStyleCommand extends BaseCommand {
29
29
  }
30
30
 
31
31
  undo() {
32
- this._apply(this.oldValue);
32
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
33
33
  }
34
34
 
35
35
  canMergeWith(otherCommand) {
@@ -28,7 +28,7 @@ export class UpdateTextStyleCommand extends BaseCommand {
28
28
  }
29
29
 
30
30
  undo() {
31
- this._apply(this.oldValue);
31
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
32
32
  }
33
33
 
34
34
  canMergeWith(otherCommand) {
@@ -96,8 +96,6 @@ export const Events = {
96
96
  Move: 'keyboard:move',
97
97
  Copy: 'keyboard:copy',
98
98
  Paste: 'keyboard:paste',
99
- Undo: 'keyboard:undo',
100
- Redo: 'keyboard:redo',
101
99
  },
102
100
 
103
101
  Object: {
@@ -19,6 +19,38 @@ export function setupClipboardFlow(core) {
19
19
  };
20
20
  };
21
21
 
22
+ const ensureServerImage = async ({ src, name, imageId }) => {
23
+ if (imageId) {
24
+ return { src, name, imageId };
25
+ }
26
+ if (!core.imageUploadService) {
27
+ alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
28
+ return null;
29
+ }
30
+ try {
31
+ let uploadResult = null;
32
+ if (typeof src === 'string' && /^data:image\//i.test(src)) {
33
+ uploadResult = await core.imageUploadService.uploadFromDataUrl(src, name || 'clipboard-image.png');
34
+ } else {
35
+ const response = await fetch(src);
36
+ if (!response.ok) {
37
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
38
+ }
39
+ const blob = await response.blob();
40
+ uploadResult = await core.imageUploadService.uploadImage(blob, name || 'clipboard-image');
41
+ }
42
+ return {
43
+ src: uploadResult.url,
44
+ name: uploadResult.name || name,
45
+ imageId: uploadResult.imageId || uploadResult.id
46
+ };
47
+ } catch (error) {
48
+ console.error('Ошибка загрузки вставленного изображения на сервер:', error);
49
+ alert('Ошибка загрузки изображения на сервер. Изображение не добавлено.');
50
+ return null;
51
+ }
52
+ };
53
+
22
54
  core.eventBus.on(Events.UI.CopyObject, ({ objectId }) => {
23
55
  if (!objectId) return;
24
56
  core.copyObject(objectId);
@@ -186,8 +218,10 @@ export function setupClipboardFlow(core) {
186
218
  core._cursor.y = y;
187
219
  });
188
220
 
189
- core.eventBus.on(Events.UI.PasteImage, ({ src, name, imageId }) => {
221
+ core.eventBus.on(Events.UI.PasteImage, async ({ src, name, imageId }) => {
190
222
  if (!src) return;
223
+ const uploaded = await ensureServerImage({ src, name, imageId });
224
+ if (!uploaded?.imageId) return;
191
225
  const view = core.pixi.app.view;
192
226
  const world = core.pixi.worldLayer || core.pixi.app.stage;
193
227
  const s = world?.scale?.x || 1;
@@ -214,18 +248,18 @@ export function setupClipboardFlow(core) {
214
248
  w = 300;
215
249
  h = Math.max(1, Math.round(w / ar));
216
250
  }
217
- const revitPayload = await resolveRevitImagePayload(src, {
251
+ const revitPayload = await resolveRevitImagePayload(uploaded.src, {
218
252
  source: 'clipboard:paste-image',
219
- name
253
+ name: uploaded.name
220
254
  });
221
255
  const properties = {
222
- src,
223
- name,
256
+ src: uploaded.src,
257
+ name: uploaded.name,
224
258
  width: w,
225
259
  height: h,
226
260
  ...revitPayload.properties
227
261
  };
228
- const extraData = imageId ? { imageId } : {};
262
+ const extraData = uploaded.imageId ? { imageId: uploaded.imageId } : {};
229
263
  core.createObject(
230
264
  revitPayload.type,
231
265
  { x: Math.round(worldX - Math.round(w / 2)), y: Math.round(worldY - Math.round(h / 2)) },
@@ -239,14 +273,16 @@ export function setupClipboardFlow(core) {
239
273
  img.decoding = 'async';
240
274
  img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
241
275
  img.onerror = () => { void placeWithAspect(0, 0); };
242
- img.src = src;
276
+ img.src = uploaded.src;
243
277
  } catch (_) {
244
278
  void placeWithAspect(0, 0);
245
279
  }
246
280
  });
247
281
 
248
- core.eventBus.on(Events.UI.PasteImageAt, ({ x, y, src, name, imageId }) => {
282
+ core.eventBus.on(Events.UI.PasteImageAt, async ({ x, y, src, name, imageId }) => {
249
283
  if (!src) return;
284
+ const uploaded = await ensureServerImage({ src, name, imageId });
285
+ if (!uploaded?.imageId) return;
250
286
  const world = core.pixi.worldLayer || core.pixi.app.stage;
251
287
  const s = world?.scale?.x || 1;
252
288
  const worldX = (x - (world?.x || 0)) / s;
@@ -260,18 +296,18 @@ export function setupClipboardFlow(core) {
260
296
  w = 300;
261
297
  h = Math.max(1, Math.round(w / ar));
262
298
  }
263
- const revitPayload = await resolveRevitImagePayload(src, {
299
+ const revitPayload = await resolveRevitImagePayload(uploaded.src, {
264
300
  source: 'clipboard:paste-image-at',
265
- name
301
+ name: uploaded.name
266
302
  });
267
303
  const properties = {
268
- src,
269
- name,
304
+ src: uploaded.src,
305
+ name: uploaded.name,
270
306
  width: w,
271
307
  height: h,
272
308
  ...revitPayload.properties
273
309
  };
274
- const extraData = imageId ? { imageId } : {};
310
+ const extraData = uploaded.imageId ? { imageId: uploaded.imageId } : {};
275
311
  core.createObject(
276
312
  revitPayload.type,
277
313
  { x: Math.round(worldX - Math.round(w / 2)), y: Math.round(worldY - Math.round(h / 2)) },
@@ -285,7 +321,7 @@ export function setupClipboardFlow(core) {
285
321
  img.decoding = 'async';
286
322
  img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
287
323
  img.onerror = () => { void placeWithAspect(0, 0); };
288
- img.src = src;
324
+ img.src = uploaded.src;
289
325
  } catch (_) {
290
326
  void placeWithAspect(0, 0);
291
327
  }
@@ -23,6 +23,11 @@ export function setupSaveFlow(core) {
23
23
  });
24
24
 
25
25
  core.eventBus.on(Events.Save.Success, async () => {
26
+ if (typeof core.revealPendingObjectsAfterSave === 'function') {
27
+ core.revealPendingObjectsAfterSave();
28
+ } else if (typeof core.revealPendingImageObjectsAfterSave === 'function') {
29
+ core.revealPendingImageObjectsAfterSave();
30
+ }
26
31
  // ВРЕМЕННО ОТКЛЮЧЕНО:
27
32
  // cleanup-фича требует доработки контракта и серверной поддержки.
28
33
  // Автоматический вызов удален, чтобы не запускать cleanup после сохранения.
package/src/core/index.js CHANGED
@@ -66,6 +66,8 @@ export class CoreMoodBoard {
66
66
  csrfToken: this.options.csrfToken
67
67
  });
68
68
  this.gridSnapResolver = new GridSnapResolver(this);
69
+ // Объекты, требующие подтверждения сохранения (image/file), показываем только после save:success.
70
+ this._pendingPersistAckVisibilityIds = new Set();
69
71
 
70
72
  // Связываем SaveManager с ApiClient для правильной обработки изображений
71
73
  this.saveManager.setApiClient(this.apiClient);
@@ -435,9 +437,39 @@ export class CoreMoodBoard {
435
437
  const command = new CreateObjectCommand(this, objectData);
436
438
  this.history.executeCommand(command);
437
439
 
440
+ // Строгий UX-контракт: image/file появляются только после успешного сохранения.
441
+ if (this._isPersistAckRequiredType(type)) {
442
+ this._pendingPersistAckVisibilityIds.add(objectData.id);
443
+ this._setObjectVisibility(objectData.id, false);
444
+ }
445
+
438
446
  return objectData;
439
447
  }
440
448
 
449
+ _isPersistAckRequiredType(type) {
450
+ return type === 'image' || type === 'revit-screenshot-img' || type === 'file';
451
+ }
452
+
453
+ _setObjectVisibility(objectId, visible) {
454
+ const pixiObject = this.pixi?.objects?.get?.(objectId);
455
+ if (pixiObject) {
456
+ pixiObject.visible = !!visible;
457
+ }
458
+ }
459
+
460
+ revealPendingObjectsAfterSave() {
461
+ if (!this._pendingPersistAckVisibilityIds || this._pendingPersistAckVisibilityIds.size === 0) return;
462
+ for (const objectId of this._pendingPersistAckVisibilityIds) {
463
+ this._setObjectVisibility(objectId, true);
464
+ }
465
+ this._pendingPersistAckVisibilityIds.clear();
466
+ }
467
+
468
+ // Backward-compat alias for tests/integrations created in previous step.
469
+ revealPendingImageObjectsAfterSave() {
470
+ this.revealPendingObjectsAfterSave();
471
+ }
472
+
441
473
  // === Прикрепления к фреймам ===
442
474
  // Логика фреймов перенесена в FrameService
443
475
 
@@ -16,11 +16,11 @@ export class KeyboardClipboardImagePaste {
16
16
  imageId: uploadResult.imageId || uploadResult.id
17
17
  });
18
18
  } else {
19
- this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
19
+ alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
20
20
  }
21
21
  } catch (error) {
22
22
  console.error('Ошибка загрузки изображения:', error);
23
- this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
23
+ alert('Ошибка загрузки изображения на сервер. Изображение не добавлено.');
24
24
  }
25
25
  }
26
26
 
@@ -34,29 +34,11 @@ export class KeyboardClipboardImagePaste {
34
34
  imageId: uploadResult.imageId || uploadResult.id
35
35
  });
36
36
  } else {
37
- const reader = new FileReader();
38
- reader.onload = () => {
39
- this.eventBus.emit(Events.UI.PasteImage, {
40
- src: reader.result,
41
- name: fileName
42
- });
43
- };
44
- reader.readAsDataURL(file);
37
+ alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
45
38
  }
46
39
  } catch (error) {
47
40
  console.error('Ошибка загрузки файла изображения:', error);
48
- try {
49
- const reader = new FileReader();
50
- reader.onload = () => {
51
- this.eventBus.emit(Events.UI.PasteImage, {
52
- src: reader.result,
53
- name: fileName
54
- });
55
- };
56
- reader.readAsDataURL(file);
57
- } catch (fallbackError) {
58
- console.error('Критическая ошибка при чтении файла:', fallbackError);
59
- }
41
+ alert('Ошибка загрузки изображения на сервер. Изображение не добавлено.');
60
42
  }
61
43
  }
62
44
 
@@ -105,10 +87,7 @@ export class KeyboardClipboardImagePaste {
105
87
  const dataUrl = await this._blobToDataUrl(blob);
106
88
  this.handleImageUpload(dataUrl, srcInHtml.split('/').pop() || 'image');
107
89
  } catch (_) {
108
- this.eventBus.emit(Events.UI.PasteImage, {
109
- src: srcInHtml,
110
- name: srcInHtml.split('/').pop() || 'image'
111
- });
90
+ alert('Не удалось загрузить изображение из URL. Изображение не добавлено.');
112
91
  }
113
92
  return;
114
93
  }
@@ -151,10 +130,7 @@ export class KeyboardClipboardImagePaste {
151
130
  this.handleImageUpload(dataUrl, trimmed.split('/').pop() || 'image');
152
131
  return;
153
132
  } catch (_) {
154
- this.eventBus.emit(Events.UI.PasteImage, {
155
- src: trimmed,
156
- name: trimmed.split('/').pop() || 'image'
157
- });
133
+ alert('Не удалось загрузить изображение из URL. Изображение не добавлено.');
158
134
  return;
159
135
  }
160
136
  }
@@ -4,8 +4,8 @@
4
4
  export class FileUploadService {
5
5
  constructor(apiClient, options = {}) {
6
6
  this.apiClient = apiClient;
7
- this.uploadEndpoint = '/api/files/upload';
8
- this.deleteEndpoint = '/api/files';
7
+ this.uploadEndpoint = '/api/v2/files/upload';
8
+ this.deleteEndpoint = '/api/v2/files';
9
9
  this.options = {
10
10
  csrfToken: null, // Можно передать токен напрямую
11
11
  csrfTokenSelector: 'meta[name="csrf-token"]', // Селектор для поиска токена в DOM
@@ -4,8 +4,8 @@
4
4
  export class ImageUploadService {
5
5
  constructor(apiClient, options = {}) {
6
6
  this.apiClient = apiClient;
7
- this.uploadEndpoint = '/api/images/upload';
8
- this.deleteEndpoint = '/api/images';
7
+ this.uploadEndpoint = '/api/v2/images/upload';
8
+ this.deleteEndpoint = '/api/v2/images';
9
9
  this.options = {
10
10
  csrfToken: null, // Можно передать токен напрямую
11
11
  csrfTokenSelector: 'meta[name="csrf-token"]', // Селектор для поиска токена в DOM
@@ -339,28 +339,13 @@ export class ToolEventRouter {
339
339
  index
340
340
  };
341
341
  } else {
342
- const localSrc = await new Promise((resolve) => {
343
- const reader = new FileReader();
344
- reader.onload = () => {
345
- resolve(reader.result);
346
- };
347
- reader.readAsDataURL(file);
348
- });
349
- if (!isCurrentDrop()) {
350
- logDropDebug(diagnostics, 'image_local_fallback_stale_drop_ignored', {
351
- fileName: file.name || 'image'
352
- });
353
- return null;
354
- }
355
- logDropDebug(diagnostics, 'image_local_fallback_success', {
356
- fileName: file.name || 'image'
357
- });
358
- return {
359
- src: localSrc,
360
- name: file.name || 'image',
361
- imageId: null,
362
- index
363
- };
342
+ showDropWarning(
343
+ manager,
344
+ `Не удалось добавить "${file.name || 'image'}": сервис загрузки изображений недоступен`,
345
+ diagnostics,
346
+ { fileName: file.name || 'image' }
347
+ );
348
+ return null;
364
349
  }
365
350
  } catch (error) {
366
351
  console.warn('Ошибка загрузки изображения через drag-and-drop:', error);
@@ -368,28 +353,16 @@ export class ToolEventRouter {
368
353
  fileName: file.name || 'image',
369
354
  message: error?.message || String(error)
370
355
  });
371
- const fallbackSrc = await new Promise((resolve) => {
372
- const reader = new FileReader();
373
- reader.onload = () => {
374
- resolve(reader.result);
375
- };
376
- reader.readAsDataURL(file);
377
- });
378
- if (!isCurrentDrop()) {
379
- logDropDebug(diagnostics, 'image_error_fallback_stale_drop_ignored', {
380
- fileName: file.name || 'image'
381
- });
382
- return null;
383
- }
384
- logDropDebug(diagnostics, 'image_error_fallback_success', {
385
- fileName: file.name || 'image'
386
- });
387
- return {
388
- src: fallbackSrc,
389
- name: file.name || 'image',
390
- imageId: null,
391
- index
392
- };
356
+ showDropWarning(
357
+ manager,
358
+ `Не удалось загрузить "${file.name || 'image'}" на сервер. Изображение не добавлено.`,
359
+ diagnostics,
360
+ {
361
+ fileName: file.name || 'image',
362
+ message: error?.message || String(error)
363
+ }
364
+ );
365
+ return null;
393
366
  }
394
367
  });
395
368
  for (const placement of imagePlacements) {
@@ -461,21 +434,13 @@ export class ToolEventRouter {
461
434
  fileId: uploadResult.fileId || uploadResult.id || null
462
435
  };
463
436
  } else {
464
- if (!isCurrentDrop()) {
465
- logDropDebug(diagnostics, 'file_local_fallback_stale_drop_ignored', {
466
- fileName: fallbackProps.fileName
467
- });
468
- return null;
469
- }
470
- logDropDebug(diagnostics, 'file_local_fallback_success', {
471
- fileName: fallbackProps.fileName
472
- });
473
- return {
474
- type: 'file',
475
- id: 'file',
476
- position,
477
- properties: fallbackProps
478
- };
437
+ showDropWarning(
438
+ manager,
439
+ `Не удалось добавить "${fallbackProps.fileName}": сервис загрузки файлов недоступен`,
440
+ diagnostics,
441
+ { fileName: fallbackProps.fileName }
442
+ );
443
+ return null;
479
444
  }
480
445
  } catch (error) {
481
446
  console.warn('Ошибка загрузки файла через drag-and-drop:', error);
@@ -483,21 +448,16 @@ export class ToolEventRouter {
483
448
  fileName: fallbackProps.fileName,
484
449
  message: error?.message || String(error)
485
450
  });
486
- if (!isCurrentDrop()) {
487
- logDropDebug(diagnostics, 'file_error_fallback_stale_drop_ignored', {
488
- fileName: fallbackProps.fileName
489
- });
490
- return null;
491
- }
492
- logDropDebug(diagnostics, 'file_error_fallback_success', {
493
- fileName: fallbackProps.fileName
494
- });
495
- return {
496
- type: 'file',
497
- id: 'file',
498
- position,
499
- properties: fallbackProps
500
- };
451
+ showDropWarning(
452
+ manager,
453
+ `Не удалось загрузить "${fallbackProps.fileName}" на сервер. Файл не добавлен.`,
454
+ diagnostics,
455
+ {
456
+ fileName: fallbackProps.fileName,
457
+ message: error?.message || String(error)
458
+ }
459
+ );
460
+ return null;
501
461
  }
502
462
  });
503
463
  for (const actionPayload of filePlacements) {