@sequent-org/moodboard 1.4.38 → 1.4.39

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.4.38",
3
+ "version": "1.4.39",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -106,8 +106,9 @@ export class AiClient {
106
106
  }
107
107
 
108
108
  /**
109
- * Генерация изображения через YandexART.
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}/yandex-art/image`, {
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-сообщение и создаёт изображение через YandexART.
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: 'yandex-art', pending: true, kind: 'image', batchId }
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: ICONS.modelQwen,
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._doesAiImageLaneSlotOverlap(slot, msg.id)) {
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
- _doesAiImageLaneSlotOverlap(candidate, candidateKey) {
1307
- for (const [key, slot] of this._aiImageLaneSlots) {
1308
- if (key === candidateKey || !slot) continue;
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 insertPoint = this._resolveAiImageInsertPoint(msg, x, y, s);
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})`
@@ -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
- /** Placeholder OpenAI GPT буква G в круге, 36×36 */
49
- const MODEL_GPT_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true"><circle cx="18" cy="18" r="16" fill="#10a37f"/><text x="18" y="23" text-anchor="middle" font-size="16" font-family="Arial,sans-serif" fill="#fff" font-weight="bold">G</text></svg>`;
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>`;