@sequent-org/moodboard 1.4.38 → 1.4.40
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/moodboard/bootstrap/MoodBoardInitializer.js +1 -1
- package/src/services/ai/AiClient.js +4 -3
- package/src/services/ai/ChatSessionController.js +6 -4
- package/src/ui/chat/ChatWindow.js +23 -7
- package/src/ui/chat/formatChatError.js +18 -0
- package/src/ui/chat/icons.js +2 -2
- package/src/ui/styles/chat.css +12 -0
package/package.json
CHANGED
|
@@ -78,7 +78,7 @@ export async function initCoreMoodBoard(board) {
|
|
|
78
78
|
boardId: board.options.boardId || 'workspace-board',
|
|
79
79
|
width: canvasSize.width,
|
|
80
80
|
height: canvasSize.height,
|
|
81
|
-
backgroundColor: board.options.theme === 'dark' ? 0x2a2a2a : parseInt(BOARD_PALETTE[
|
|
81
|
+
backgroundColor: board.options.theme === 'dark' ? 0x2a2a2a : parseInt(BOARD_PALETTE[4].board.replace('#', ''), 16),
|
|
82
82
|
saveEndpoint: board.options.saveEndpoint,
|
|
83
83
|
loadEndpoint: board.options.loadEndpoint,
|
|
84
84
|
};
|
|
@@ -106,8 +106,9 @@ export class AiClient {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Генерация изображения через
|
|
109
|
+
* Генерация изображения через image-провайдера.
|
|
110
110
|
* @param {object} args
|
|
111
|
+
* @param {string} [args.provider='yandex-art']
|
|
111
112
|
* @param {string} args.prompt
|
|
112
113
|
* @param {string} [args.negativePrompt]
|
|
113
114
|
* @param {number} [args.widthRatio]
|
|
@@ -119,10 +120,10 @@ export class AiClient {
|
|
|
119
120
|
* @param {AbortSignal} [args.signal]
|
|
120
121
|
* @returns {Promise<{operationId: string, imageBase64: string, mimeType: string}>}
|
|
121
122
|
*/
|
|
122
|
-
async generateImage({ signal, referenceImages: files, ...payload }) {
|
|
123
|
+
async generateImage({ provider = 'yandex-art', signal, referenceImages: files, ...payload }) {
|
|
123
124
|
const referenceImages = await filesToBase64(files);
|
|
124
125
|
const body = referenceImages ? { ...payload, referenceImages } : payload;
|
|
125
|
-
const res = await this._fetch(`${this._baseUrl}/
|
|
126
|
+
const res = await this._fetch(`${this._baseUrl}/${provider}/image`, {
|
|
126
127
|
method: 'POST',
|
|
127
128
|
headers: {
|
|
128
129
|
'Content-Type': 'application/json',
|
|
@@ -10,7 +10,7 @@ import { CHAT_PRESETS, DEFAULT_PRESET_ID, getPresetById } from './ChatPresets.js
|
|
|
10
10
|
*
|
|
11
11
|
* Состояние:
|
|
12
12
|
* - messages: список сообщений (с временным assistant-сообщением во время стриминга)
|
|
13
|
-
* - providerId: текущий провайдер (yandex-art)
|
|
13
|
+
* - providerId: текущий провайдер (yandex-art/openai-image)
|
|
14
14
|
* - presetId: текущий пресет промпта
|
|
15
15
|
* - settings: { systemPrompt, temperature, maxTokens }
|
|
16
16
|
* - status: 'idle' | 'streaming' | 'error'
|
|
@@ -112,21 +112,22 @@ export class ChatSessionController {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* Отправляет user-сообщение и создаёт изображение через
|
|
115
|
+
* Отправляет user-сообщение и создаёт изображение через выбранный image-провайдер.
|
|
116
116
|
* @param {string} text
|
|
117
|
-
* @param {{widthRatio?: number, heightRatio?: number, model?: string, imageCount?: number}} [options]
|
|
117
|
+
* @param {{provider?: string, widthRatio?: number, heightRatio?: number, model?: string, imageCount?: number}} [options]
|
|
118
118
|
*/
|
|
119
119
|
async send(text, options = {}) {
|
|
120
120
|
const trimmed = (text || '').trim();
|
|
121
121
|
if (!trimmed) return;
|
|
122
122
|
|
|
123
|
+
const provider = options.provider || 'yandex-art';
|
|
123
124
|
const imageCount = normalizeImageCount(options.imageCount);
|
|
124
125
|
const batchId = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
125
126
|
const userMsg = makeMessage('user', trimmed);
|
|
126
127
|
const assistantMsgs = Array.from({ length: imageCount }, (_, index) => makeMessage(
|
|
127
128
|
'assistant',
|
|
128
129
|
imageCount > 1 ? `Генерируется изображение ${index + 1} из ${imageCount}…` : '',
|
|
129
|
-
{ provider
|
|
130
|
+
{ provider, pending: true, kind: 'image', batchId }
|
|
130
131
|
));
|
|
131
132
|
|
|
132
133
|
this._state = {
|
|
@@ -154,6 +155,7 @@ export class ChatSessionController {
|
|
|
154
155
|
|
|
155
156
|
return this._client
|
|
156
157
|
.generateImage({
|
|
158
|
+
provider,
|
|
157
159
|
prompt: trimmed,
|
|
158
160
|
widthRatio: options.widthRatio,
|
|
159
161
|
heightRatio: options.heightRatio,
|
|
@@ -90,7 +90,7 @@ const MODEL_OPTIONS = [
|
|
|
90
90
|
{
|
|
91
91
|
id: 'qwen',
|
|
92
92
|
label: 'Qwen',
|
|
93
|
-
icon:
|
|
93
|
+
icon: '<img src="/icons/qwen.svg" alt="" aria-hidden="true">',
|
|
94
94
|
description: 'Alibaba'
|
|
95
95
|
}
|
|
96
96
|
];
|
|
@@ -255,6 +255,7 @@ export class ChatWindow {
|
|
|
255
255
|
|
|
256
256
|
const initialState = this._session.getState();
|
|
257
257
|
this._markExistingBoardImages(initialState.messages);
|
|
258
|
+
this._reserveCurrentAiImageLaneSlots();
|
|
258
259
|
this._unsubscribe = this._session.subscribe((state) => this._render(state));
|
|
259
260
|
this._render(initialState);
|
|
260
261
|
|
|
@@ -616,7 +617,9 @@ export class ChatWindow {
|
|
|
616
617
|
|
|
617
618
|
_getImageRequestOptions() {
|
|
618
619
|
const [widthRatio, heightRatio] = parseFormatRatio(this._formatId);
|
|
620
|
+
const provider = this._modelId === 'gpt' ? 'openai-image' : 'yandex-art';
|
|
619
621
|
return {
|
|
622
|
+
provider,
|
|
620
623
|
widthRatio,
|
|
621
624
|
heightRatio,
|
|
622
625
|
model: this._modelId === 'yandex' ? 'yandex-art' : undefined,
|
|
@@ -1284,7 +1287,7 @@ export class ChatWindow {
|
|
|
1284
1287
|
const centerWorldY = (y - (world?.y || 0)) / s;
|
|
1285
1288
|
let slot = this._reserveAiImageLaneSlotForMessage(msg.id, centerWorldX, centerWorldY);
|
|
1286
1289
|
|
|
1287
|
-
if (slot && this.
|
|
1290
|
+
if (slot && this._doesBoardAiImageOverlapSlot(slot, msg.id)) {
|
|
1288
1291
|
const right = this._getAiImageLaneRightBoundary(undefined, msg.id);
|
|
1289
1292
|
if (Number.isFinite(right)) {
|
|
1290
1293
|
slot = {
|
|
@@ -1303,9 +1306,17 @@ export class ChatWindow {
|
|
|
1303
1306
|
};
|
|
1304
1307
|
}
|
|
1305
1308
|
|
|
1306
|
-
|
|
1307
|
-
for (const
|
|
1308
|
-
|
|
1309
|
+
_doesBoardAiImageOverlapSlot(candidate, excludeKey) {
|
|
1310
|
+
for (const object of this._getBoardAiImageObjects()) {
|
|
1311
|
+
const key = getAiImageLaneKeyForObject(object);
|
|
1312
|
+
if (key === excludeKey) continue;
|
|
1313
|
+
|
|
1314
|
+
const slot = {
|
|
1315
|
+
x: Math.round(object.position.x),
|
|
1316
|
+
y: Math.round(object.position.y),
|
|
1317
|
+
width: getBoardObjectWidth(object),
|
|
1318
|
+
height: getBoardObjectHeight(object)
|
|
1319
|
+
};
|
|
1309
1320
|
|
|
1310
1321
|
if (rectsOverlap(candidate, slot)) {
|
|
1311
1322
|
return true;
|
|
@@ -1336,7 +1347,12 @@ export class ChatWindow {
|
|
|
1336
1347
|
x = slot.x;
|
|
1337
1348
|
y = slot.y;
|
|
1338
1349
|
}
|
|
1339
|
-
const
|
|
1350
|
+
const centerWorldX = (x - (world?.x || 0)) / s;
|
|
1351
|
+
const centerWorldY = (y - (world?.y || 0)) / s;
|
|
1352
|
+
const reservedSlot = this._reserveAiImageLaneSlotForMessage(msg.id, centerWorldX, centerWorldY);
|
|
1353
|
+
const insertPoint = pendingRecord && reservedSlot && !this._doesBoardAiImageOverlapSlot(reservedSlot, msg.id)
|
|
1354
|
+
? { x, y }
|
|
1355
|
+
: this._resolveAiImageInsertPoint(msg, x, y, s);
|
|
1340
1356
|
|
|
1341
1357
|
this._boardCore.eventBus.emit(Events.UI.PasteImageAt, {
|
|
1342
1358
|
x: insertPoint.x,
|
|
@@ -1444,7 +1460,7 @@ function isImageGenerationMessage(message) {
|
|
|
1444
1460
|
function isBoardAiImageObject(object) {
|
|
1445
1461
|
return Boolean(object?.id)
|
|
1446
1462
|
&& object.type === 'image'
|
|
1447
|
-
&& object.properties?.name === 'ai-generated.jpg'
|
|
1463
|
+
&& (object.properties?.name === 'ai-generated.jpg' || object.properties?.aiMessageId)
|
|
1448
1464
|
&& object.position
|
|
1449
1465
|
&& Number.isFinite(object.position.x);
|
|
1450
1466
|
}
|
|
@@ -68,6 +68,16 @@ const EXACT_MESSAGES_RU = {
|
|
|
68
68
|
'Провайдер Yandex не настроен',
|
|
69
69
|
'Provider "yandex-art" is not configured':
|
|
70
70
|
'Провайдер «yandex-art» не настроен',
|
|
71
|
+
'OpenAI image provider is not configured':
|
|
72
|
+
'Провайдер OpenAI Images не настроен',
|
|
73
|
+
'OpenAI image response does not contain image data':
|
|
74
|
+
'В ответе OpenAI Images нет данных изображения',
|
|
75
|
+
'OpenAI image API returned non-JSON response':
|
|
76
|
+
'OpenAI Images вернул ответ не в формате JSON',
|
|
77
|
+
'OpenAI image operation timed out':
|
|
78
|
+
'Превышено время ожидания генерации OpenAI Images',
|
|
79
|
+
'Provider "openai-image" is not configured':
|
|
80
|
+
'Провайдер «openai-image» не настроен',
|
|
71
81
|
'AI stream error':
|
|
72
82
|
'Ошибка потока ответа ИИ',
|
|
73
83
|
'AiClient.chatStream: empty response body':
|
|
@@ -96,6 +106,14 @@ const PREFIX_MESSAGES_RU = [
|
|
|
96
106
|
pattern: /^YandexART API error \((\d+)\)$/,
|
|
97
107
|
format: ([, status]) => `Ошибка API YandexART (${status})`
|
|
98
108
|
},
|
|
109
|
+
{
|
|
110
|
+
pattern: /^OpenAI image API unreachable: (.+)$/,
|
|
111
|
+
format: ([, detail]) => `API OpenAI Images недоступен: ${detail}`
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
pattern: /^OpenAI image API error \((\d+)\)$/,
|
|
115
|
+
format: ([, status]) => `Ошибка API OpenAI Images (${status})`
|
|
116
|
+
},
|
|
99
117
|
{
|
|
100
118
|
pattern: /^Yandex Operations API error \((\d+)\)$/,
|
|
101
119
|
format: ([, status]) => `Ошибка API операций Yandex (${status})`
|
package/src/ui/chat/icons.js
CHANGED
|
@@ -45,8 +45,8 @@ const EXTEND_PROMPT_FIELD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width=
|
|
|
45
45
|
/** public/icons/google.svg — цветной логотип Google 36×36 */
|
|
46
46
|
const MODEL_GOOGLE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none" viewBox="0 0 36 36"><path fill="#4285F4" d="M29.251 18.49c0-.813-.073-1.596-.209-2.347H18.23v4.445h6.179c-.272 1.43-1.086 2.64-2.307 3.455v2.89h3.726c2.17-2.003 3.423-4.946 3.423-8.442"/><path fill="#34A853" d="M18.229 29.71c3.1 0 5.698-1.023 7.597-2.776l-3.725-2.89c-1.023.688-2.328 1.105-3.872 1.105-2.985 0-5.52-2.014-6.429-4.727H7.98v2.964c1.89 3.746 5.761 6.324 10.249 6.324"/><path fill="#FBBC05" d="M11.801 20.412a6.9 6.9 0 0 1-.365-2.181c0-.762.136-1.492.365-2.181v-2.964h-3.82A11.34 11.34 0 0 0 6.75 18.23c0 1.858.449 3.6 1.231 5.145l2.975-2.317z"/><path fill="#EA4335" d="M18.229 11.321c1.69 0 3.193.585 4.393 1.712l3.288-3.288c-1.994-1.857-4.582-2.995-7.681-2.995-4.488 0-8.36 2.578-10.249 6.335l3.82 2.964c.908-2.714 3.444-4.728 6.429-4.728"/></svg>`;
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
const MODEL_GPT_ICON = `<
|
|
48
|
+
/** public/icons/gpt.svg — логотип GPT 36×36 */
|
|
49
|
+
const MODEL_GPT_ICON = `<img src="/icons/gpt.svg" width="36" height="36" alt="" aria-hidden="true">`;
|
|
50
50
|
|
|
51
51
|
/** Placeholder Alibaba Qwen — буква Q в круге, 36×36 */
|
|
52
52
|
const MODEL_QWEN_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true"><circle cx="18" cy="18" r="16" fill="#6e42ca"/><text x="18" y="23" text-anchor="middle" font-size="16" font-family="Arial,sans-serif" fill="#fff" font-weight="bold">Q</text></svg>`;
|
package/src/ui/styles/chat.css
CHANGED
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
color: #374151;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/* Fallback: на экранах < 1540px центрированный чат перекрывает правый кластер —
|
|
24
|
+
поднимаем чат над кластером и прижимаем к правому краю */
|
|
25
|
+
@media (max-width: 1540px) {
|
|
26
|
+
.moodboard-chat {
|
|
27
|
+
left: auto;
|
|
28
|
+
right: 16px;
|
|
29
|
+
bottom: 84px;
|
|
30
|
+
transform: none;
|
|
31
|
+
width: min(720px, calc(100% - 32px));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
.moodboard-chat__history {
|
|
24
36
|
max-height: 360px;
|
|
25
37
|
overflow-y: auto;
|