@sequent-org/moodboard 1.4.34 → 1.4.37
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/core/flows/ClipboardFlow.js +2 -1
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +2 -1
- package/src/ui/Topbar.js +2 -7
- package/src/ui/boardPalette.js +12 -0
- package/src/ui/chat/ChatWindow.js +375 -16
- package/src/ui/chat/ChatWindowRenderer.js +6 -1
- package/src/ui/chat/formatChatError.js +229 -0
- package/src/ui/styles/chat.css +24 -0
package/package.json
CHANGED
|
@@ -286,7 +286,7 @@ export function setupClipboardFlow(core) {
|
|
|
286
286
|
}
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
-
core.eventBus.on(Events.UI.PasteImageAt, async ({ x, y, src, name, skipUpload }) => {
|
|
289
|
+
core.eventBus.on(Events.UI.PasteImageAt, async ({ x, y, src, name, skipUpload, aiMessageId }) => {
|
|
290
290
|
if (!src) return;
|
|
291
291
|
const uploaded = await ensureServerImage({ src, name, skipUpload });
|
|
292
292
|
if (!uploaded?.src) return;
|
|
@@ -312,6 +312,7 @@ export function setupClipboardFlow(core) {
|
|
|
312
312
|
name: uploaded.name,
|
|
313
313
|
width: w,
|
|
314
314
|
height: h,
|
|
315
|
+
...(aiMessageId ? { aiMessageId } : {}),
|
|
315
316
|
...revitPayload.properties
|
|
316
317
|
};
|
|
317
318
|
const createdData = core.createObject(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CoreMoodBoard } from '../../core/index.js';
|
|
2
|
+
import { BOARD_PALETTE } from '../../ui/boardPalette.js';
|
|
2
3
|
import { createMoodBoardManagers, wireMoodBoardServices } from './MoodBoardManagersFactory.js';
|
|
3
4
|
import { createMoodBoardUi } from './MoodBoardUiFactory.js';
|
|
4
5
|
import { bindSaveCallbacks } from '../integration/MoodBoardEventBindings.js';
|
|
@@ -77,7 +78,7 @@ export async function initCoreMoodBoard(board) {
|
|
|
77
78
|
boardId: board.options.boardId || 'workspace-board',
|
|
78
79
|
width: canvasSize.width,
|
|
79
80
|
height: canvasSize.height,
|
|
80
|
-
backgroundColor: board.options.theme === 'dark' ? 0x2a2a2a :
|
|
81
|
+
backgroundColor: board.options.theme === 'dark' ? 0x2a2a2a : parseInt(BOARD_PALETTE[0].board.replace('#', ''), 16),
|
|
81
82
|
saveEndpoint: board.options.saveEndpoint,
|
|
82
83
|
loadEndpoint: board.options.loadEndpoint,
|
|
83
84
|
};
|
package/src/ui/Topbar.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Events } from '../core/events/Events.js';
|
|
5
5
|
import { TopbarIconLoader } from '../utils/topbarIconLoader.js';
|
|
6
|
+
import { BOARD_PALETTE } from './boardPalette.js';
|
|
6
7
|
|
|
7
8
|
export class Topbar {
|
|
8
9
|
constructor(container, eventBus, theme = 'light') {
|
|
@@ -15,13 +16,7 @@ export class Topbar {
|
|
|
15
16
|
this.iconLoader = new TopbarIconLoader();
|
|
16
17
|
this.icons = this.iconLoader.icons;
|
|
17
18
|
// Палитра кнопки заливки и соответствие цвету фона доски
|
|
18
|
-
this._palette =
|
|
19
|
-
{ id: 1, name: 'default-light', btnHex: '#d6e8f7', board: '#f0f6fc' },
|
|
20
|
-
{ id: 2, name: 'mint-light', btnHex: '#E8F5E9', board: '#f8fff7' },
|
|
21
|
-
{ id: 3, name: 'peach-light', btnHex: '#FFF3E0', board: '#fffcf7' },
|
|
22
|
-
{ id: 4, name: 'gray-light', btnHex: '#f5f5f5', board: '#f5f5f5' },
|
|
23
|
-
{ id: 5, name: 'white', btnHex: '#ffffff', board: '#ffffff' }
|
|
24
|
-
];
|
|
19
|
+
this._palette = BOARD_PALETTE;
|
|
25
20
|
this._pendingPaintHex = null;
|
|
26
21
|
this.init();
|
|
27
22
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Палитра фонов доски.
|
|
3
|
+
* Первый элемент используется как фон по умолчанию при создании/первом открытии доски.
|
|
4
|
+
* btnHex — цвет кнопки в Topbar; board — реальный цвет фона canvas.
|
|
5
|
+
*/
|
|
6
|
+
export const BOARD_PALETTE = [
|
|
7
|
+
{ id: 1, name: 'default-light', btnHex: '#d6e8f7', board: '#f0f6fc' },
|
|
8
|
+
{ id: 2, name: 'mint-light', btnHex: '#E8F5E9', board: '#f8fff7' },
|
|
9
|
+
{ id: 3, name: 'peach-light', btnHex: '#FFF3E0', board: '#fffcf7' },
|
|
10
|
+
{ id: 4, name: 'gray-light', btnHex: '#f5f5f5', board: '#f5f5f5' },
|
|
11
|
+
{ id: 5, name: 'white', btnHex: '#ffffff', board: '#ffffff' },
|
|
12
|
+
];
|
|
@@ -4,6 +4,7 @@ import { ChatSessionController } from '../../services/ai/ChatSessionController.j
|
|
|
4
4
|
import { Events } from '../../core/events/Events.js';
|
|
5
5
|
|
|
6
6
|
import { buildChatDom } from './ChatWindowRenderer.js';
|
|
7
|
+
import { formatChatErrorForDisplay } from './formatChatError.js';
|
|
7
8
|
import { ChatMessageList } from './ChatMessageList.js';
|
|
8
9
|
import { ChatComposer } from './ChatComposer.js';
|
|
9
10
|
import { ChatPillMenu } from './ChatPillMenu.js';
|
|
@@ -145,7 +146,9 @@ export class ChatWindow {
|
|
|
145
146
|
this._pendingOverlayTimers = new Map();
|
|
146
147
|
this._boardImageShiftAnimations = new Map();
|
|
147
148
|
this._pendingBatchOffsets = new Map();
|
|
148
|
-
this.
|
|
149
|
+
this._aiImageLaneSlots = new Map();
|
|
150
|
+
this._draggingAiImageIds = new Set();
|
|
151
|
+
this._aiImageLaneHandlers = null;
|
|
149
152
|
this._selectionHandlers = null;
|
|
150
153
|
this._viewportHandlers = null;
|
|
151
154
|
// Окно от BoxSelectStart до BoxSelectCommit: в это время SelectionAdd
|
|
@@ -178,11 +181,10 @@ export class ChatWindow {
|
|
|
178
181
|
onAbort: () => this._session.abort()
|
|
179
182
|
}
|
|
180
183
|
);
|
|
181
|
-
this._clearSelectionOnSendClick = () => this._clearBoardSelection();
|
|
182
|
-
this._refs.send.addEventListener('click', this._clearSelectionOnSendClick);
|
|
183
184
|
this._composer.attach();
|
|
184
185
|
this._attachSelectionEvents();
|
|
185
186
|
this._attachViewportSync();
|
|
187
|
+
this._attachAiImageLaneEvents();
|
|
186
188
|
|
|
187
189
|
this._extendedPromptModal = new ChatExtendedPromptModal(
|
|
188
190
|
this._container,
|
|
@@ -266,15 +268,14 @@ export class ChatWindow {
|
|
|
266
268
|
this._clearPendingOverlays();
|
|
267
269
|
this._cancelBoardImageShiftAnimations();
|
|
268
270
|
if (this._unsubscribe) { this._unsubscribe(); this._unsubscribe = null; }
|
|
269
|
-
if (this._clearSelectionOnSendClick && this._refs?.send) {
|
|
270
|
-
this._refs.send.removeEventListener('click', this._clearSelectionOnSendClick);
|
|
271
|
-
this._clearSelectionOnSendClick = null;
|
|
272
|
-
}
|
|
273
271
|
this._detachSelectionEvents();
|
|
274
272
|
this._detachViewportSync();
|
|
273
|
+
this._detachAiImageLaneEvents();
|
|
275
274
|
this._shiftedForImageBatchKeys.clear();
|
|
276
275
|
this._boardImageShiftHistory.clear();
|
|
277
276
|
this._pendingBatchOffsets.clear();
|
|
277
|
+
this._aiImageLaneSlots.clear();
|
|
278
|
+
this._draggingAiImageIds.clear();
|
|
278
279
|
this._composer?.destroy();
|
|
279
280
|
this._extendedPromptModal?.destroy();
|
|
280
281
|
this._contentTypeMenu?.destroy();
|
|
@@ -354,6 +355,26 @@ export class ChatWindow {
|
|
|
354
355
|
this._updateCountPillIcon();
|
|
355
356
|
this._composer.setStreaming(state.status === 'streaming');
|
|
356
357
|
this._updatePendingImages(state.status === 'streaming' ? state.messages : []);
|
|
358
|
+
|
|
359
|
+
const errorBlock = this._refs.errorBlock;
|
|
360
|
+
const wasVisible = errorBlock.classList.contains('is-visible');
|
|
361
|
+
const isError = state.error && state.error !== 'Отменено';
|
|
362
|
+
|
|
363
|
+
if (isError) {
|
|
364
|
+
errorBlock.textContent = formatChatErrorForDisplay(state.error);
|
|
365
|
+
errorBlock.classList.add('is-visible');
|
|
366
|
+
} else {
|
|
367
|
+
errorBlock.classList.remove('is-visible');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const isVisible = errorBlock.classList.contains('is-visible');
|
|
371
|
+
if (!wasVisible && isVisible) {
|
|
372
|
+
requestAnimationFrame(() => {
|
|
373
|
+
this._pushBoardImagesUpIfNeeded();
|
|
374
|
+
});
|
|
375
|
+
} else if (wasVisible && !isVisible) {
|
|
376
|
+
this._pullBoardImagesDownIfNeeded();
|
|
377
|
+
}
|
|
357
378
|
}
|
|
358
379
|
|
|
359
380
|
_updatePendingImages(messages) {
|
|
@@ -420,11 +441,13 @@ export class ChatWindow {
|
|
|
420
441
|
worldX: existing.worldX,
|
|
421
442
|
worldY: existing.worldY
|
|
422
443
|
};
|
|
444
|
+
this._reserveAiImageLaneSlotForMessage(message.id, existing.worldX, existing.worldY);
|
|
423
445
|
this._applyPendingOverlayScreenLayout(existing.el, screenLayout, { animate: true, enterDistance });
|
|
424
446
|
return;
|
|
425
447
|
}
|
|
426
448
|
|
|
427
449
|
const layout = this._computePendingOverlayScreenLayout(messages, message.id, s);
|
|
450
|
+
this._reserveAiImageLaneSlotForMessage(message.id, layout.worldX, layout.worldY);
|
|
428
451
|
|
|
429
452
|
const overlay = document.createElement('div');
|
|
430
453
|
overlay.className = 'moodboard-chat__pending-overlay moodboard-chat__pending-overlay--enter';
|
|
@@ -498,7 +521,8 @@ export class ChatWindow {
|
|
|
498
521
|
|
|
499
522
|
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
500
523
|
const worldX = world?.x || 0;
|
|
501
|
-
const existingRight_world =
|
|
524
|
+
const existingRight_world = this._getAiImageLaneRightBoundary(aiObjects);
|
|
525
|
+
if (!Number.isFinite(existingRight_world)) return baseEnter;
|
|
502
526
|
const existingRight_screen = Math.round(existingRight_world * scale + worldX);
|
|
503
527
|
|
|
504
528
|
const step = Math.round(BOARD_IMAGE_STEP * scale);
|
|
@@ -543,6 +567,7 @@ export class ChatWindow {
|
|
|
543
567
|
const layout = this._computePendingOverlayScreenLayout(messages, messageId, scale);
|
|
544
568
|
record.worldX = layout.worldX;
|
|
545
569
|
record.worldY = layout.worldY;
|
|
570
|
+
this._reserveAiImageLaneSlotForMessage(messageId, layout.worldX, layout.worldY);
|
|
546
571
|
this._applyPendingOverlayScreenLayout(record.el, layout, { animate: true });
|
|
547
572
|
existingOverlaysShifted = true;
|
|
548
573
|
}
|
|
@@ -555,8 +580,25 @@ export class ChatWindow {
|
|
|
555
580
|
const worldShift = shiftAmount / scale;
|
|
556
581
|
for (const obj of this._getBoardAiImageObjects()) {
|
|
557
582
|
if (obj?.position) {
|
|
583
|
+
const key = getAiImageLaneKeyForObject(obj);
|
|
584
|
+
const currentSlot = this._getAiImageLaneSlotForObject(obj);
|
|
558
585
|
const from = { x: obj.position.x, y: obj.position.y };
|
|
559
|
-
|
|
586
|
+
if (this._draggingAiImageIds.has(obj.id)) {
|
|
587
|
+
this._reserveAiImageLaneSlotForObject(obj);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const to = {
|
|
591
|
+
x: Math.round((currentSlot?.x ?? obj.position.x) - worldShift),
|
|
592
|
+
y: currentSlot?.y ?? obj.position.y
|
|
593
|
+
};
|
|
594
|
+
if (key) {
|
|
595
|
+
this._aiImageLaneSlots.set(key, {
|
|
596
|
+
x: to.x,
|
|
597
|
+
y: to.y,
|
|
598
|
+
width: currentSlot?.width ?? getBoardObjectWidth(obj),
|
|
599
|
+
height: currentSlot?.height ?? getBoardObjectHeight(obj)
|
|
600
|
+
});
|
|
601
|
+
}
|
|
560
602
|
this._animateBoardImageToPosition(obj.id, from, to);
|
|
561
603
|
}
|
|
562
604
|
}
|
|
@@ -641,6 +683,7 @@ export class ChatWindow {
|
|
|
641
683
|
const screenX = Math.round(record.worldX * s + (world?.x || 0));
|
|
642
684
|
const screenY = Math.round(record.worldY * s + (world?.y || 0));
|
|
643
685
|
const el = record.el;
|
|
686
|
+
this._reserveAiImageLaneSlotForMessage(messageId, record.worldX, record.worldY);
|
|
644
687
|
if (disableTransition) {
|
|
645
688
|
el.style.transition = 'none';
|
|
646
689
|
}
|
|
@@ -683,6 +726,62 @@ export class ChatWindow {
|
|
|
683
726
|
this._viewportHandlers = null;
|
|
684
727
|
}
|
|
685
728
|
|
|
729
|
+
_attachAiImageLaneEvents() {
|
|
730
|
+
const eventBus = this._boardCore?.eventBus;
|
|
731
|
+
if (!eventBus || typeof eventBus.on !== 'function' || this._aiImageLaneHandlers) return;
|
|
732
|
+
|
|
733
|
+
const onDragStart = (data) => {
|
|
734
|
+
this._startAiImageLaneDrag([data?.object]);
|
|
735
|
+
};
|
|
736
|
+
const onDragUpdate = (data) => {
|
|
737
|
+
this._updateAiImageLaneDrag([data?.object]);
|
|
738
|
+
};
|
|
739
|
+
const onDragEnd = (data) => {
|
|
740
|
+
this._updateAiImageLaneDrag([data?.object]);
|
|
741
|
+
this._endAiImageLaneDrag([data?.object]);
|
|
742
|
+
};
|
|
743
|
+
const onGroupDragStart = (data) => {
|
|
744
|
+
this._startAiImageLaneDrag(data?.objects);
|
|
745
|
+
};
|
|
746
|
+
const onGroupDragUpdate = (data) => {
|
|
747
|
+
this._updateAiImageLaneDrag(data?.objects);
|
|
748
|
+
};
|
|
749
|
+
const onGroupDragEnd = (data) => {
|
|
750
|
+
this._updateAiImageLaneDrag(data?.objects);
|
|
751
|
+
this._endAiImageLaneDrag(data?.objects);
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
this._aiImageLaneHandlers = {
|
|
755
|
+
onDragStart,
|
|
756
|
+
onDragUpdate,
|
|
757
|
+
onDragEnd,
|
|
758
|
+
onGroupDragStart,
|
|
759
|
+
onGroupDragUpdate,
|
|
760
|
+
onGroupDragEnd
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
eventBus.on(Events.Tool.DragStart, onDragStart);
|
|
764
|
+
eventBus.on(Events.Tool.DragUpdate, onDragUpdate);
|
|
765
|
+
eventBus.on(Events.Tool.DragEnd, onDragEnd);
|
|
766
|
+
eventBus.on(Events.Tool.GroupDragStart, onGroupDragStart);
|
|
767
|
+
eventBus.on(Events.Tool.GroupDragUpdate, onGroupDragUpdate);
|
|
768
|
+
eventBus.on(Events.Tool.GroupDragEnd, onGroupDragEnd);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
_detachAiImageLaneEvents() {
|
|
772
|
+
const eventBus = this._boardCore?.eventBus;
|
|
773
|
+
const handlers = this._aiImageLaneHandlers;
|
|
774
|
+
if (!eventBus || typeof eventBus.off !== 'function' || !handlers) return;
|
|
775
|
+
|
|
776
|
+
eventBus.off(Events.Tool.DragStart, handlers.onDragStart);
|
|
777
|
+
eventBus.off(Events.Tool.DragUpdate, handlers.onDragUpdate);
|
|
778
|
+
eventBus.off(Events.Tool.DragEnd, handlers.onDragEnd);
|
|
779
|
+
eventBus.off(Events.Tool.GroupDragStart, handlers.onGroupDragStart);
|
|
780
|
+
eventBus.off(Events.Tool.GroupDragUpdate, handlers.onGroupDragUpdate);
|
|
781
|
+
eventBus.off(Events.Tool.GroupDragEnd, handlers.onGroupDragEnd);
|
|
782
|
+
this._aiImageLaneHandlers = null;
|
|
783
|
+
}
|
|
784
|
+
|
|
686
785
|
_attachSelectionEvents() {
|
|
687
786
|
const eventBus = this._boardCore?.eventBus;
|
|
688
787
|
if (!eventBus || typeof eventBus.on !== 'function' || this._selectionHandlers) return;
|
|
@@ -795,9 +894,23 @@ export class ChatWindow {
|
|
|
795
894
|
_getImageGroupAnchor() {
|
|
796
895
|
const composerRect = this._refs?.composer?.getBoundingClientRect?.();
|
|
797
896
|
if (composerRect) {
|
|
897
|
+
let y = composerRect.top - 250;
|
|
898
|
+
|
|
899
|
+
const errorBlock = this._refs?.errorBlock;
|
|
900
|
+
if (errorBlock && errorBlock.classList.contains('is-visible')) {
|
|
901
|
+
const errorRect = errorBlock.getBoundingClientRect();
|
|
902
|
+
// Низ картинок примерно y + 150.
|
|
903
|
+
// Хотим чтобы errorRect.top был хотя бы y + 150 + 16 (gap)
|
|
904
|
+
// То есть y + 166 <= errorRect.top
|
|
905
|
+
const minY = errorRect.top - 166;
|
|
906
|
+
if (y > minY) {
|
|
907
|
+
y = minY;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
798
911
|
return {
|
|
799
912
|
x: Math.round(composerRect.left + composerRect.width / 2),
|
|
800
|
-
y: Math.round(
|
|
913
|
+
y: Math.round(y)
|
|
801
914
|
};
|
|
802
915
|
}
|
|
803
916
|
|
|
@@ -842,7 +955,8 @@ export class ChatWindow {
|
|
|
842
955
|
const aiObjects = this._getBoardAiImageObjects();
|
|
843
956
|
if (aiObjects.length === 0 || !nextBatchBounds) return;
|
|
844
957
|
|
|
845
|
-
const existingRight =
|
|
958
|
+
const existingRight = this._getAiImageLaneRightBoundary(aiObjects);
|
|
959
|
+
if (!Number.isFinite(existingRight)) return;
|
|
846
960
|
const shift = Math.ceil(existingRight + BOARD_IMAGE_GAP - nextBatchBounds.left);
|
|
847
961
|
if (shift <= 0) return;
|
|
848
962
|
|
|
@@ -852,9 +966,28 @@ export class ChatWindow {
|
|
|
852
966
|
for (const id of ids) {
|
|
853
967
|
const obj = objects?.find((item) => item.id === id);
|
|
854
968
|
if (obj?.position) {
|
|
969
|
+
const key = getAiImageLaneKeyForObject(obj);
|
|
970
|
+
const currentSlot = this._getAiImageLaneSlotForObject(obj);
|
|
855
971
|
const from = { x: obj.position.x, y: obj.position.y };
|
|
856
|
-
|
|
857
|
-
|
|
972
|
+
if (this._draggingAiImageIds.has(id)) {
|
|
973
|
+
this._reserveAiImageLaneSlotForObject(obj);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const fromSlot = currentSlot ? { ...currentSlot } : null;
|
|
977
|
+
const to = {
|
|
978
|
+
x: Math.round((currentSlot?.x ?? obj.position.x) - shift),
|
|
979
|
+
y: currentSlot?.y ?? obj.position.y
|
|
980
|
+
};
|
|
981
|
+
const toSlot = {
|
|
982
|
+
x: to.x,
|
|
983
|
+
y: to.y,
|
|
984
|
+
width: currentSlot?.width ?? getBoardObjectWidth(obj),
|
|
985
|
+
height: currentSlot?.height ?? getBoardObjectHeight(obj)
|
|
986
|
+
};
|
|
987
|
+
if (key) {
|
|
988
|
+
this._aiImageLaneSlots.set(key, toSlot);
|
|
989
|
+
}
|
|
990
|
+
shiftRecord.push({ id, from, fromSlot });
|
|
858
991
|
this._animateBoardImageToPosition(id, from, to);
|
|
859
992
|
}
|
|
860
993
|
}
|
|
@@ -869,9 +1002,13 @@ export class ChatWindow {
|
|
|
869
1002
|
if (!record) return;
|
|
870
1003
|
|
|
871
1004
|
const objects = this._boardCore?.state?.state?.objects;
|
|
872
|
-
for (const { id, from } of record) {
|
|
1005
|
+
for (const { id, from, fromSlot } of record) {
|
|
873
1006
|
const obj = objects?.find((item) => item.id === id);
|
|
874
1007
|
if (obj?.position) {
|
|
1008
|
+
const key = getAiImageLaneKeyForObject(obj);
|
|
1009
|
+
if (key && fromSlot) {
|
|
1010
|
+
this._aiImageLaneSlots.set(key, fromSlot);
|
|
1011
|
+
}
|
|
875
1012
|
this._animateBoardImageToPosition(id, obj.position, from);
|
|
876
1013
|
}
|
|
877
1014
|
}
|
|
@@ -902,6 +1039,58 @@ export class ChatWindow {
|
|
|
902
1039
|
}
|
|
903
1040
|
}
|
|
904
1041
|
|
|
1042
|
+
_pushBoardImagesUpIfNeeded() {
|
|
1043
|
+
const errorBlock = this._refs.errorBlock;
|
|
1044
|
+
if (!errorBlock) return;
|
|
1045
|
+
const errorRect = errorBlock.getBoundingClientRect();
|
|
1046
|
+
|
|
1047
|
+
const aiObjects = this._getBoardAiImageObjects();
|
|
1048
|
+
if (aiObjects.length === 0) return;
|
|
1049
|
+
|
|
1050
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
1051
|
+
const s = world?.scale?.x || 1;
|
|
1052
|
+
|
|
1053
|
+
let maxOverlap = 0;
|
|
1054
|
+
for (const obj of aiObjects) {
|
|
1055
|
+
if (!obj.position) continue;
|
|
1056
|
+
const height = getBoardObjectHeight(obj);
|
|
1057
|
+
const bottomWorld = obj.position.y + height;
|
|
1058
|
+
const bottomScreen = bottomWorld * s + (world?.y || 0);
|
|
1059
|
+
|
|
1060
|
+
const overlap = bottomScreen - (errorRect.top - 16);
|
|
1061
|
+
if (overlap > maxOverlap) {
|
|
1062
|
+
maxOverlap = overlap;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (maxOverlap > 0) {
|
|
1067
|
+
const shiftWorld = Math.ceil(maxOverlap / s);
|
|
1068
|
+
this._errorShiftAmount = shiftWorld;
|
|
1069
|
+
for (const obj of aiObjects) {
|
|
1070
|
+
if (obj.position) {
|
|
1071
|
+
const from = { x: obj.position.x, y: obj.position.y };
|
|
1072
|
+
const to = { x: obj.position.x, y: obj.position.y - shiftWorld };
|
|
1073
|
+
this._animateBoardImageToPosition(obj.id, from, to);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
_pullBoardImagesDownIfNeeded() {
|
|
1080
|
+
if (!this._errorShiftAmount) return;
|
|
1081
|
+
const shiftWorld = this._errorShiftAmount;
|
|
1082
|
+
this._errorShiftAmount = 0;
|
|
1083
|
+
|
|
1084
|
+
const aiObjects = this._getBoardAiImageObjects();
|
|
1085
|
+
for (const obj of aiObjects) {
|
|
1086
|
+
if (obj.position) {
|
|
1087
|
+
const from = { x: obj.position.x, y: obj.position.y };
|
|
1088
|
+
const to = { x: obj.position.x, y: obj.position.y + shiftWorld };
|
|
1089
|
+
this._animateBoardImageToPosition(obj.id, from, to);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
905
1094
|
_animateBoardImageToPosition(id, fromPosition, toPosition) {
|
|
906
1095
|
const updatePosition = this._boardCore?.updateObjectPositionDirect;
|
|
907
1096
|
if (!id || typeof updatePosition !== 'function' || !fromPosition || !toPosition) return;
|
|
@@ -976,6 +1165,156 @@ export class ChatWindow {
|
|
|
976
1165
|
return objects.filter((object) => isBoardAiImageObject(object));
|
|
977
1166
|
}
|
|
978
1167
|
|
|
1168
|
+
_getBoardObjectById(objectId) {
|
|
1169
|
+
const objects = this._boardCore?.state?.state?.objects;
|
|
1170
|
+
if (!objectId || !Array.isArray(objects)) return null;
|
|
1171
|
+
|
|
1172
|
+
return objects.find((object) => object?.id === objectId) || null;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
_startAiImageLaneDrag(ids) {
|
|
1176
|
+
const objects = this._getAiImageObjectsByIds(ids);
|
|
1177
|
+
if (objects.length === 0) return;
|
|
1178
|
+
|
|
1179
|
+
this._reserveCurrentAiImageLaneSlots();
|
|
1180
|
+
for (const object of objects) {
|
|
1181
|
+
this._draggingAiImageIds.add(object.id);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
_updateAiImageLaneDrag(ids) {
|
|
1186
|
+
const objects = this._getAiImageObjectsByIds(ids);
|
|
1187
|
+
for (const object of objects) {
|
|
1188
|
+
this._reserveAiImageLaneSlotForObject(object);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
_endAiImageLaneDrag(ids) {
|
|
1193
|
+
const objects = this._getAiImageObjectsByIds(ids);
|
|
1194
|
+
for (const object of objects) {
|
|
1195
|
+
this._draggingAiImageIds.delete(object.id);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
_getAiImageObjectsByIds(ids) {
|
|
1200
|
+
const list = Array.isArray(ids) ? ids : [];
|
|
1201
|
+
const objects = [];
|
|
1202
|
+
|
|
1203
|
+
for (const id of list) {
|
|
1204
|
+
const object = this._getBoardObjectById(id);
|
|
1205
|
+
if (isBoardAiImageObject(object)) {
|
|
1206
|
+
objects.push(object);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
return objects;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
_reserveCurrentAiImageLaneSlots() {
|
|
1214
|
+
for (const object of this._getBoardAiImageObjects()) {
|
|
1215
|
+
this._reserveAiImageLaneSlotForObject(object);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
_reserveAiImageLaneSlotForObject(object) {
|
|
1220
|
+
const key = getAiImageLaneKeyForObject(object);
|
|
1221
|
+
if (!key || !object?.position) return null;
|
|
1222
|
+
|
|
1223
|
+
const slot = {
|
|
1224
|
+
x: Math.round(object.position.x),
|
|
1225
|
+
y: Math.round(object.position.y),
|
|
1226
|
+
width: getBoardObjectWidth(object),
|
|
1227
|
+
height: getBoardObjectHeight(object)
|
|
1228
|
+
};
|
|
1229
|
+
this._aiImageLaneSlots.set(key, slot);
|
|
1230
|
+
|
|
1231
|
+
return slot;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
_reserveAiImageLaneSlotForMessage(messageId, centerWorldX, centerWorldY) {
|
|
1235
|
+
if (!messageId || !Number.isFinite(centerWorldX) || !Number.isFinite(centerWorldY)) return null;
|
|
1236
|
+
|
|
1237
|
+
const [wr, hr] = parseFormatRatio(this._formatId);
|
|
1238
|
+
const width = BOARD_IMAGE_WIDTH;
|
|
1239
|
+
const height = Math.round(width / (wr / hr));
|
|
1240
|
+
const slot = {
|
|
1241
|
+
x: Math.round(centerWorldX - width / 2),
|
|
1242
|
+
y: Math.round(centerWorldY - height / 2),
|
|
1243
|
+
width,
|
|
1244
|
+
height
|
|
1245
|
+
};
|
|
1246
|
+
this._aiImageLaneSlots.set(messageId, slot);
|
|
1247
|
+
|
|
1248
|
+
return slot;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
_getAiImageLaneSlotForObject(object) {
|
|
1252
|
+
const key = getAiImageLaneKeyForObject(object);
|
|
1253
|
+
return (key && this._aiImageLaneSlots.get(key)) || this._reserveAiImageLaneSlotForObject(object);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
_getAiImageLaneRightBoundary(objects = this._getBoardAiImageObjects(), excludeKey = null) {
|
|
1257
|
+
const rights = [];
|
|
1258
|
+
|
|
1259
|
+
for (const object of objects) {
|
|
1260
|
+
const key = getAiImageLaneKeyForObject(object);
|
|
1261
|
+
if (key && key === excludeKey) continue;
|
|
1262
|
+
|
|
1263
|
+
const slot = this._getAiImageLaneSlotForObject(object);
|
|
1264
|
+
if (slot) {
|
|
1265
|
+
rights.push(slot.x + slot.width);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
for (const [key, slot] of this._aiImageLaneSlots) {
|
|
1270
|
+
if (key === excludeKey) continue;
|
|
1271
|
+
|
|
1272
|
+
if (slot && Number.isFinite(slot.x) && Number.isFinite(slot.width)) {
|
|
1273
|
+
rights.push(slot.x + slot.width);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return rights.length > 0 ? Math.max(...rights) : null;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
_resolveAiImageInsertPoint(msg, x, y, scale) {
|
|
1281
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
1282
|
+
const s = scale || world?.scale?.x || 1;
|
|
1283
|
+
const centerWorldX = (x - (world?.x || 0)) / s;
|
|
1284
|
+
const centerWorldY = (y - (world?.y || 0)) / s;
|
|
1285
|
+
let slot = this._reserveAiImageLaneSlotForMessage(msg.id, centerWorldX, centerWorldY);
|
|
1286
|
+
|
|
1287
|
+
if (slot && this._doesAiImageLaneSlotOverlap(slot, msg.id)) {
|
|
1288
|
+
const right = this._getAiImageLaneRightBoundary(undefined, msg.id);
|
|
1289
|
+
if (Number.isFinite(right)) {
|
|
1290
|
+
slot = {
|
|
1291
|
+
...slot,
|
|
1292
|
+
x: Math.round(right + BOARD_IMAGE_GAP)
|
|
1293
|
+
};
|
|
1294
|
+
this._aiImageLaneSlots.set(msg.id, slot);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (!slot) return { x, y };
|
|
1299
|
+
|
|
1300
|
+
return {
|
|
1301
|
+
x: Math.round((slot.x + slot.width / 2) * s + (world?.x || 0)),
|
|
1302
|
+
y: Math.round((slot.y + slot.height / 2) * s + (world?.y || 0))
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
_doesAiImageLaneSlotOverlap(candidate, candidateKey) {
|
|
1307
|
+
for (const [key, slot] of this._aiImageLaneSlots) {
|
|
1308
|
+
if (key === candidateKey || !slot) continue;
|
|
1309
|
+
|
|
1310
|
+
if (rectsOverlap(candidate, slot)) {
|
|
1311
|
+
return true;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
979
1318
|
_addImageToBoard(msg) {
|
|
980
1319
|
if (!this._boardCore?.eventBus) return;
|
|
981
1320
|
const dataUrl = `data:${msg.mimeType || 'image/jpeg'};base64,${msg.imageBase64}`;
|
|
@@ -997,12 +1336,14 @@ export class ChatWindow {
|
|
|
997
1336
|
x = slot.x;
|
|
998
1337
|
y = slot.y;
|
|
999
1338
|
}
|
|
1339
|
+
const insertPoint = this._resolveAiImageInsertPoint(msg, x, y, s);
|
|
1000
1340
|
|
|
1001
1341
|
this._boardCore.eventBus.emit(Events.UI.PasteImageAt, {
|
|
1002
|
-
x,
|
|
1003
|
-
y,
|
|
1342
|
+
x: insertPoint.x,
|
|
1343
|
+
y: insertPoint.y,
|
|
1004
1344
|
src: dataUrl,
|
|
1005
1345
|
name: 'ai-generated.jpg',
|
|
1346
|
+
aiMessageId: msg.id,
|
|
1006
1347
|
skipUpload: true
|
|
1007
1348
|
});
|
|
1008
1349
|
}
|
|
@@ -1108,6 +1449,19 @@ function isBoardAiImageObject(object) {
|
|
|
1108
1449
|
&& Number.isFinite(object.position.x);
|
|
1109
1450
|
}
|
|
1110
1451
|
|
|
1452
|
+
function getAiImageLaneKeyForObject(object) {
|
|
1453
|
+
return object?.properties?.aiMessageId || object?.id || null;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function rectsOverlap(a, b) {
|
|
1457
|
+
if (!a || !b) return false;
|
|
1458
|
+
|
|
1459
|
+
return a.x < b.x + b.width
|
|
1460
|
+
&& a.x + a.width > b.x
|
|
1461
|
+
&& a.y < b.y + b.height
|
|
1462
|
+
&& a.y + a.height > b.y;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1111
1465
|
function isReferenceImageObject(object) {
|
|
1112
1466
|
return Boolean(object?.id)
|
|
1113
1467
|
&& (object.type === 'image' || object.type === 'revit-screenshot-img')
|
|
@@ -1183,6 +1537,11 @@ function getBoardObjectWidth(object) {
|
|
|
1183
1537
|
return Number.isFinite(width) ? Math.max(1, Math.round(width)) : BOARD_IMAGE_WIDTH;
|
|
1184
1538
|
}
|
|
1185
1539
|
|
|
1540
|
+
function getBoardObjectHeight(object) {
|
|
1541
|
+
const height = object?.height ?? object?.properties?.height ?? BOARD_IMAGE_WIDTH;
|
|
1542
|
+
return Number.isFinite(height) ? Math.max(1, Math.round(height)) : BOARD_IMAGE_WIDTH;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1186
1545
|
function easeOutCubic(progress) {
|
|
1187
1546
|
return 1 - Math.pow(1 - progress, 3);
|
|
1188
1547
|
}
|
|
@@ -21,6 +21,9 @@ export function buildChatDom() {
|
|
|
21
21
|
history.setAttribute('role', 'log');
|
|
22
22
|
history.setAttribute('aria-live', 'polite');
|
|
23
23
|
|
|
24
|
+
const errorBlock = createDiv('moodboard-chat__error-block');
|
|
25
|
+
errorBlock.setAttribute('aria-live', 'assertive');
|
|
26
|
+
|
|
24
27
|
const composer = createDiv('moodboard-chat__composer');
|
|
25
28
|
|
|
26
29
|
const statusBar = createDiv('moodboard-chat__status-bar');
|
|
@@ -35,7 +38,8 @@ export function buildChatDom() {
|
|
|
35
38
|
history,
|
|
36
39
|
composer,
|
|
37
40
|
statusBar,
|
|
38
|
-
pendingImages
|
|
41
|
+
pendingImages,
|
|
42
|
+
errorBlock
|
|
39
43
|
};
|
|
40
44
|
|
|
41
45
|
composer.appendChild(buildInputRow(refs => Object.assign(rendererRefs, refs)));
|
|
@@ -43,6 +47,7 @@ export function buildChatDom() {
|
|
|
43
47
|
|
|
44
48
|
root.appendChild(history);
|
|
45
49
|
root.appendChild(pendingImages);
|
|
50
|
+
root.appendChild(errorBlock);
|
|
46
51
|
root.appendChild(statusBar);
|
|
47
52
|
root.appendChild(composer);
|
|
48
53
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Форматирование текста ошибки для moodboard-chat__error-block:
|
|
3
|
+
* русский текст пользователю, системный префикс AiClient.* (код) — в скобках в конце.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {Record<number, string>} */
|
|
7
|
+
export const HTTP_STATUS_RU = {
|
|
8
|
+
400: 'Некорректный запрос',
|
|
9
|
+
401: 'Требуется авторизация',
|
|
10
|
+
403: 'Доступ запрещён',
|
|
11
|
+
404: 'Ресурс не найден',
|
|
12
|
+
408: 'Превышено время ожидания запроса',
|
|
13
|
+
409: 'Конфликт запроса',
|
|
14
|
+
413: 'Слишком большой запрос',
|
|
15
|
+
422: 'Ошибка валидации данных',
|
|
16
|
+
429: 'Слишком много запросов',
|
|
17
|
+
500: 'Внутренняя ошибка сервера',
|
|
18
|
+
501: 'Операция не поддерживается',
|
|
19
|
+
502: 'Сбой шлюза',
|
|
20
|
+
503: 'Сервис временно недоступен',
|
|
21
|
+
504: 'Превышено время ожидания шлюза'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Англоязычные фразы statusText / Express — ключ в нижнем регистре */
|
|
25
|
+
/** @type {Record<string, string>} */
|
|
26
|
+
const HTTP_STATUS_PHRASE_RU = {
|
|
27
|
+
'bad request': HTTP_STATUS_RU[400],
|
|
28
|
+
'unauthorized': HTTP_STATUS_RU[401],
|
|
29
|
+
'forbidden': HTTP_STATUS_RU[403],
|
|
30
|
+
'not found': HTTP_STATUS_RU[404],
|
|
31
|
+
'request timeout': HTTP_STATUS_RU[408],
|
|
32
|
+
'conflict': HTTP_STATUS_RU[409],
|
|
33
|
+
'payload too large': HTTP_STATUS_RU[413],
|
|
34
|
+
'unprocessable entity': HTTP_STATUS_RU[422],
|
|
35
|
+
'too many requests': HTTP_STATUS_RU[429],
|
|
36
|
+
'internal server error': HTTP_STATUS_RU[500],
|
|
37
|
+
'not implemented': HTTP_STATUS_RU[501],
|
|
38
|
+
'bad gateway': HTTP_STATUS_RU[502],
|
|
39
|
+
'service unavailable': HTTP_STATUS_RU[503],
|
|
40
|
+
'gateway timeout': HTTP_STATUS_RU[504]
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** @type {Record<string, string>} */
|
|
44
|
+
const EXACT_MESSAGES_RU = {
|
|
45
|
+
'YandexART provider does not support reference images':
|
|
46
|
+
'Провайдер YandexART не поддерживает опорные изображения',
|
|
47
|
+
'YandexART provider is not configured':
|
|
48
|
+
'Провайдер YandexART не настроен',
|
|
49
|
+
'YandexART response does not contain image data':
|
|
50
|
+
'В ответе YandexART нет данных изображения',
|
|
51
|
+
'YandexART returned non-JSON response':
|
|
52
|
+
'YandexART вернул ответ не в формате JSON',
|
|
53
|
+
'YandexART did not return operation id':
|
|
54
|
+
'YandexART не вернул идентификатор операции',
|
|
55
|
+
'YandexART operation failed':
|
|
56
|
+
'Операция YandexART завершилась с ошибкой',
|
|
57
|
+
'YandexART operation timed out':
|
|
58
|
+
'Превышено время ожидания операции YandexART',
|
|
59
|
+
'Yandex Operations API returned non-JSON response':
|
|
60
|
+
'API операций Yandex вернул ответ не в формате JSON',
|
|
61
|
+
'YandexART API error':
|
|
62
|
+
'Ошибка API YandexART',
|
|
63
|
+
'Yandex Operations API error':
|
|
64
|
+
'Ошибка API операций Yandex',
|
|
65
|
+
'DeepSeek provider is not configured':
|
|
66
|
+
'Провайдер DeepSeek не настроен',
|
|
67
|
+
'Yandex provider is not configured':
|
|
68
|
+
'Провайдер Yandex не настроен',
|
|
69
|
+
'Provider "yandex-art" is not configured':
|
|
70
|
+
'Провайдер «yandex-art» не настроен',
|
|
71
|
+
'AI stream error':
|
|
72
|
+
'Ошибка потока ответа ИИ',
|
|
73
|
+
'AiClient.chatStream: empty response body':
|
|
74
|
+
'Пустое тело ответа при потоковой генерации',
|
|
75
|
+
'Ошибка запроса':
|
|
76
|
+
'Ошибка запроса',
|
|
77
|
+
'Internal server error':
|
|
78
|
+
HTTP_STATUS_RU[500],
|
|
79
|
+
'Internal Server Error':
|
|
80
|
+
HTTP_STATUS_RU[500],
|
|
81
|
+
'stream error':
|
|
82
|
+
'Ошибка потока ответа'
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** @type {Array<{ pattern: RegExp, format: (match: RegExpMatchArray) => string }>} */
|
|
86
|
+
const PREFIX_MESSAGES_RU = [
|
|
87
|
+
{
|
|
88
|
+
pattern: /^YandexART API unreachable: (.+)$/,
|
|
89
|
+
format: ([, detail]) => `API YandexART недоступен: ${detail}`
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: /^Yandex Operations API unreachable: (.+)$/,
|
|
93
|
+
format: ([, detail]) => `API операций Yandex недоступен: ${detail}`
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
pattern: /^YandexART API error \((\d+)\)$/,
|
|
97
|
+
format: ([, status]) => `Ошибка API YandexART (${status})`
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
pattern: /^Yandex Operations API error \((\d+)\)$/,
|
|
101
|
+
format: ([, status]) => `Ошибка API операций Yandex (${status})`
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
pattern: /^Unknown provider: (.+)$/,
|
|
105
|
+
format: ([, id]) => `Неизвестный провайдер: ${id}`
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
pattern: /^Provider "(.+)" is not configured$/,
|
|
109
|
+
format: ([, id]) => `Провайдер «${id}» не настроен`
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
pattern: /^Cannot load image reference \((\d+)\)$/,
|
|
113
|
+
format: ([, status]) => `Не удалось загрузить опорное изображение (${status})`
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
pattern: /^(\d{3}):\s*(.+)$/,
|
|
117
|
+
format: ([, status, detail]) => {
|
|
118
|
+
const ru = translateByHttpStatus(Number(status)) || detail;
|
|
119
|
+
return `${ru} (${status})`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const AI_CLIENT_WITH_STATUS_RE = /^([A-Za-z][\w.]*)\s*\((\d+)\):\s*(.+)$/s;
|
|
125
|
+
const AI_CLIENT_PLAIN_RE = /^([A-Za-z][\w.]*):\s*(.+)$/s;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string|null|undefined} raw
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
export function formatChatErrorForDisplay(raw) {
|
|
132
|
+
if (raw == null) return '';
|
|
133
|
+
const trimmed = String(raw).trim();
|
|
134
|
+
if (!trimmed) return '';
|
|
135
|
+
|
|
136
|
+
if (trimmed === 'Отменено') {
|
|
137
|
+
return trimmed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const withStatus = trimmed.match(AI_CLIENT_WITH_STATUS_RE);
|
|
141
|
+
if (withStatus) {
|
|
142
|
+
const [, system, status, message] = withStatus;
|
|
143
|
+
const ru = translateErrorMessage(message.trim(), Number(status));
|
|
144
|
+
return `${ru} (${system} (${status}))`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const plain = trimmed.match(AI_CLIENT_PLAIN_RE);
|
|
148
|
+
if (plain) {
|
|
149
|
+
const [, system, message] = plain;
|
|
150
|
+
const statusOnly = /^\d{3}$/.test(message.trim()) ? Number(message.trim()) : undefined;
|
|
151
|
+
const ru = translateErrorMessage(message.trim(), statusOnly);
|
|
152
|
+
return `${ru} (${system})`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return translateErrorMessage(trimmed);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} message
|
|
160
|
+
* @param {number} [httpStatus]
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
export function translateErrorMessage(message, httpStatus) {
|
|
164
|
+
if (!message && Number.isFinite(httpStatus)) {
|
|
165
|
+
return translateByHttpStatus(httpStatus) || message;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!message) return message;
|
|
169
|
+
|
|
170
|
+
if (EXACT_MESSAGES_RU[message]) {
|
|
171
|
+
return EXACT_MESSAGES_RU[message];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const phraseRu = HTTP_STATUS_PHRASE_RU[message.trim().toLowerCase()];
|
|
175
|
+
if (phraseRu) {
|
|
176
|
+
return phraseRu;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (/^\d{3}$/.test(message.trim())) {
|
|
180
|
+
const byCode = translateByHttpStatus(Number(message.trim()));
|
|
181
|
+
if (byCode) return byCode;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const { pattern, format } of PREFIX_MESSAGES_RU) {
|
|
185
|
+
const match = message.match(pattern);
|
|
186
|
+
if (match) {
|
|
187
|
+
return format(match);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (Number.isFinite(httpStatus)) {
|
|
192
|
+
const byStatus = translateByHttpStatus(httpStatus);
|
|
193
|
+
if (byStatus && isGenericHttpStatusMessage(message, httpStatus)) {
|
|
194
|
+
return byStatus;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return message;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {number} status
|
|
203
|
+
* @returns {string|undefined}
|
|
204
|
+
*/
|
|
205
|
+
export function translateByHttpStatus(status) {
|
|
206
|
+
return HTTP_STATUS_RU[status];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {string} message
|
|
211
|
+
* @param {number} [httpStatus]
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
function isGenericHttpStatusMessage(message, httpStatus) {
|
|
215
|
+
const normalized = message.trim().toLowerCase();
|
|
216
|
+
if (HTTP_STATUS_PHRASE_RU[normalized]) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (/^\d{3}$/.test(normalized) && Number(normalized) === httpStatus) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (Number.isFinite(httpStatus) && normalized === String(httpStatus)) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return false;
|
|
229
|
+
}
|
package/src/ui/styles/chat.css
CHANGED
|
@@ -731,6 +731,30 @@
|
|
|
731
731
|
pointer-events: none;
|
|
732
732
|
}
|
|
733
733
|
|
|
734
|
+
/* Блок ошибки сервера */
|
|
735
|
+
.moodboard-chat__error-block {
|
|
736
|
+
display: none;
|
|
737
|
+
background: #ffffff;
|
|
738
|
+
border-radius: 12px;
|
|
739
|
+
padding: 12px 16px;
|
|
740
|
+
margin-bottom: 8px;
|
|
741
|
+
--color-shadow: lab(5.0601% 0 0 / .04);
|
|
742
|
+
box-shadow:
|
|
743
|
+
0 0 0 1px lab(5.0601% 0 0 / .0784),
|
|
744
|
+
0 2px 8px -1.5px var(--color-shadow),
|
|
745
|
+
0 5px 8px -3px var(--color-shadow),
|
|
746
|
+
0 12px 12px -8px var(--color-shadow),
|
|
747
|
+
0 24px 24px -8px var(--color-shadow);
|
|
748
|
+
color: #B91C1C;
|
|
749
|
+
font-size: 14px;
|
|
750
|
+
line-height: 1.5;
|
|
751
|
+
word-break: break-word;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.moodboard-chat__error-block.is-visible {
|
|
755
|
+
display: block;
|
|
756
|
+
}
|
|
757
|
+
|
|
734
758
|
/* Попап настроек */
|
|
735
759
|
.moodboard-chat__settings-popup {
|
|
736
760
|
position: absolute;
|