docs-combiner 0.1.5 → 0.1.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.
package/dist/renderer.js CHANGED
@@ -89973,6 +89973,13 @@ function App() {
89973
89973
  return saved || _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.imageGeneration;
89974
89974
  });
89975
89975
  const [loadingImageModels, setLoadingImageModels] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
89976
+ const [validationModels, setValidationModels] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
89977
+ const [selectedValidationModel, setSelectedValidationModel] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => {
89978
+ // Load from localStorage or use default
89979
+ const saved = localStorage.getItem('selectedValidationModel');
89980
+ return saved || _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation;
89981
+ });
89982
+ const [loadingValidationModels, setLoadingValidationModels] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
89976
89983
  // Helper function to parse price and currency from combined field
89977
89984
  const parsePriceAndCurrency = (value) => {
89978
89985
  const trimmed = value.trim();
@@ -90263,10 +90270,32 @@ function App() {
90263
90270
  const cachedGenerateGeo = localStorage.getItem('cached_generateGeo');
90264
90271
  const cachedGenerateAdditionalInfo = localStorage.getItem('cached_generateAdditionalInfo');
90265
90272
  const cachedGeneratePriceWithCurrency = localStorage.getItem('cached_generatePriceWithCurrency');
90266
- if (cachedTitles)
90273
+ if (cachedTitles) {
90267
90274
  setTitles(cachedTitles);
90268
- if (cachedTexts)
90269
- setTexts(JSON.parse(cachedTexts));
90275
+ // Restore generatedTitlesData from cached titles string
90276
+ const titlesArray = cachedTitles.split('\n').map(t => t.trim()).filter(t => t);
90277
+ if (titlesArray.length > 0) {
90278
+ setGeneratedTitlesData(Array.from({ length: Math.max(3, titlesArray.length) }, (_, index) => ({
90279
+ index: index + 1,
90280
+ title: titlesArray[index] || '',
90281
+ generating: false,
90282
+ failed: !titlesArray[index]
90283
+ })));
90284
+ }
90285
+ }
90286
+ if (cachedTexts) {
90287
+ const textsArray = JSON.parse(cachedTexts);
90288
+ setTexts(textsArray);
90289
+ // Restore generatedTextsData from cached texts array
90290
+ if (Array.isArray(textsArray) && textsArray.length > 0) {
90291
+ setGeneratedTextsData(Array.from({ length: Math.max(3, textsArray.length) }, (_, index) => ({
90292
+ index: index + 1,
90293
+ text: textsArray[index] || '',
90294
+ generating: false,
90295
+ failed: !textsArray[index]
90296
+ })));
90297
+ }
90298
+ }
90270
90299
  if (cachedDriveFolderUrl)
90271
90300
  setDriveFolderUrl(cachedDriveFolderUrl);
90272
90301
  if (cachedBrand)
@@ -90312,6 +90341,28 @@ function App() {
90312
90341
  // Silent fail
90313
90342
  }
90314
90343
  }, [titles, texts, driveFolderUrl, brand, link, generateProduct, generateGeo, generateAdditionalInfo, generatePriceWithCurrency]);
90344
+ // Sync generatedTitlesData with titles when titles changes (except when user is editing in UI)
90345
+ (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
90346
+ // Only sync if titles has content and generatedTitlesData is empty or out of sync
90347
+ if (titles) {
90348
+ const titlesArray = titles.split('\n').map(t => t.trim()).filter(t => t);
90349
+ const currentTitles = generatedTitlesData.map(t => t.title).filter(Boolean);
90350
+ const titlesString = currentTitles.join('\n');
90351
+ // Only update if titles actually changed and we have titles to restore
90352
+ if (titlesString !== titles && titlesArray.length > 0) {
90353
+ setGeneratedTitlesData(Array.from({ length: Math.max(3, titlesArray.length) }, (_, index) => ({
90354
+ index: index + 1,
90355
+ title: titlesArray[index] || '',
90356
+ generating: false,
90357
+ failed: !titlesArray[index]
90358
+ })));
90359
+ }
90360
+ }
90361
+ else if (titles === '' && generatedTitlesData.length > 0 && generatedTitlesData.some(t => t.title)) {
90362
+ // If titles is cleared, clear generatedTitlesData too
90363
+ setGeneratedTitlesData([]);
90364
+ }
90365
+ }, [titles]); // Only depend on titles, not generatedTitlesData to avoid loops
90315
90366
  // Auto-scroll logs when new logs are added
90316
90367
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
90317
90368
  if (productGenerationLogs.length > 0 && (generatingProduct || uploadingProduct)) {
@@ -90461,6 +90512,7 @@ function App() {
90461
90512
  return;
90462
90513
  }
90463
90514
  fetchImageModels();
90515
+ fetchValidationModels();
90464
90516
  // eslint-disable-next-line react-hooks/exhaustive-deps
90465
90517
  }, [openaiApiKey]);
90466
90518
  // Check folder files when driveFolderUrl changes
@@ -90748,6 +90800,7 @@ function App() {
90748
90800
  if (val) {
90749
90801
  fetchOpenRouterBalance(val);
90750
90802
  fetchImageModels(val);
90803
+ fetchValidationModels(val);
90751
90804
  }
90752
90805
  else {
90753
90806
  setOpenRouterBalance(null);
@@ -90759,6 +90812,10 @@ function App() {
90759
90812
  setSelectedImageModel(modelId);
90760
90813
  localStorage.setItem('selectedImageModel', modelId);
90761
90814
  };
90815
+ const handleValidationModelChange = (modelId) => {
90816
+ setSelectedValidationModel(modelId);
90817
+ localStorage.setItem('selectedValidationModel', modelId);
90818
+ };
90762
90819
  // Fetch OpenRouter account balance and key limit
90763
90820
  const fetchOpenRouterBalance = async (apiKey) => {
90764
90821
  const keyToUse = apiKey ?? openaiApiKey;
@@ -90800,8 +90857,6 @@ function App() {
90800
90857
  }
90801
90858
  })
90802
90859
  ]);
90803
- logToTerminal('log', `💰 Credits response status: ${creditsResponse.status} ${creditsResponse.statusText}`);
90804
- logToTerminal('log', `💰 Key response status: ${keyResponse.status} ${keyResponse.statusText}`);
90805
90860
  // Process account balance (remaining credits)
90806
90861
  if (creditsResponse.ok) {
90807
90862
  const contentType = creditsResponse.headers.get('content-type') || '';
@@ -90888,7 +90943,6 @@ function App() {
90888
90943
  setOpenRouterBalance(limitValue);
90889
90944
  }
90890
90945
  else if (limit === null || limit === undefined) {
90891
- logToTerminal('log', '✅ No limit set (unlimited)');
90892
90946
  setOpenRouterBalance(-1);
90893
90947
  }
90894
90948
  else {
@@ -90910,14 +90964,12 @@ function App() {
90910
90964
  const limit = keyData.data?.limit ??
90911
90965
  keyData.limit ??
90912
90966
  null;
90913
- logToTerminal('log', `💰 Key limit remaining: ${limitRemaining}, limit: ${limit}`);
90914
90967
  if (limitRemaining !== null && limitRemaining !== undefined) {
90915
90968
  const limitValue = typeof limitRemaining === 'number' ? limitRemaining : parseFloat(limitRemaining);
90916
90969
  logToTerminal('log', `✅ Key limit found: ${limitValue.toFixed(4)}`);
90917
90970
  setOpenRouterBalance(limitValue);
90918
90971
  }
90919
90972
  else if (limit === null || limit === undefined) {
90920
- logToTerminal('log', '✅ No limit set (unlimited)');
90921
90973
  setOpenRouterBalance(-1); // Use -1 to indicate unlimited
90922
90974
  }
90923
90975
  else {
@@ -91017,6 +91069,72 @@ function App() {
91017
91069
  setLoadingImageModels(false);
91018
91070
  }
91019
91071
  };
91072
+ // Fetch available validation models from OpenRouter (models that support image analysis)
91073
+ const fetchValidationModels = async (apiKey) => {
91074
+ const keyToUse = apiKey ?? openaiApiKey;
91075
+ if (!keyToUse) {
91076
+ setValidationModels([]);
91077
+ return;
91078
+ }
91079
+ setLoadingValidationModels(true);
91080
+ try {
91081
+ logToTerminal('log', '🔍 Fetching available validation models (image analysis) from OpenRouter...');
91082
+ const response = await fetch('https://openrouter.ai/api/v1/models', {
91083
+ method: 'GET',
91084
+ headers: {
91085
+ 'Authorization': `Bearer ${keyToUse}`,
91086
+ 'HTTP-Referer': window.location.origin || 'https://docs-combiner.app',
91087
+ 'X-Title': 'Docs Combiner',
91088
+ 'Accept': 'application/json'
91089
+ }
91090
+ });
91091
+ if (!response.ok) {
91092
+ logToTerminal('warn', `⚠️ Failed to fetch validation models. Status: ${response.status}`);
91093
+ setValidationModels([]);
91094
+ return;
91095
+ }
91096
+ const data = await response.json();
91097
+ logToTerminal('log', `✅ Fetched ${data.data?.length || 0} models from OpenRouter`);
91098
+ // Filter models that support image analysis (vision) - they need to accept image_url in content
91099
+ const validationModelsList = (data.data || [])
91100
+ .filter((model) => {
91101
+ // Check if model supports vision/image analysis
91102
+ const modalities = model.modalities || [];
91103
+ const supportsVision = modalities.includes('image') || modalities.includes('image_url') || modalities.includes('vision');
91104
+ // Also check if model supports chat completions with images (most GPT models)
91105
+ const supportsChatWithImages = model.id && (model.id.includes('gpt') ||
91106
+ model.id.includes('claude') ||
91107
+ model.id.includes('gemini'));
91108
+ return supportsVision || supportsChatWithImages;
91109
+ })
91110
+ .map((model) => ({
91111
+ id: model.id,
91112
+ name: model.name || model.id
91113
+ }))
91114
+ .sort((a, b) => a.name.localeCompare(b.name));
91115
+ logToTerminal('log', `✅ Found ${validationModelsList.length} validation models`);
91116
+ setValidationModels(validationModelsList);
91117
+ // If we have models and the current selection is not in the list, reset to default
91118
+ if (validationModelsList.length > 0) {
91119
+ const currentModelExists = validationModelsList.some((m) => m.id === selectedValidationModel);
91120
+ if (!currentModelExists) {
91121
+ // Try to use default model, or first available
91122
+ const defaultModel = validationModelsList.find((m) => m.id === _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation) || validationModelsList[0];
91123
+ if (defaultModel) {
91124
+ setSelectedValidationModel(defaultModel.id);
91125
+ localStorage.setItem('selectedValidationModel', defaultModel.id);
91126
+ }
91127
+ }
91128
+ }
91129
+ }
91130
+ catch (error) {
91131
+ logToTerminal('error', '❌ Failed to fetch validation models:', error instanceof Error ? error.message : String(error));
91132
+ setValidationModels([]);
91133
+ }
91134
+ finally {
91135
+ setLoadingValidationModels(false);
91136
+ }
91137
+ };
91020
91138
  const generateWithGPT = async (product, geo, additionalInfo, type, addLog) => {
91021
91139
  const logMsg = (level, ...args) => {
91022
91140
  logToTerminal(level, ...args);
@@ -91525,17 +91643,30 @@ function App() {
91525
91643
  referenceImageApiUrls.forEach(url => {
91526
91644
  content.push({ type: 'image_url', image_url: { url } });
91527
91645
  });
91646
+ // Log request details for debugging
91647
+ logToTerminal('log', `📤 generateImageWithDALLE: prompt length=${prompt.length}, reference images=${referenceImageApiUrls.length}`);
91648
+ if (referenceImageApiUrls.length > 0) {
91649
+ logToTerminal('log', `📤 Reference images: ${referenceImageApiUrls.map(url => url.substring(0, 50) + '...').join(', ')}`);
91650
+ }
91651
+ logToTerminal('log', `📤 Content array length: ${content.length}, has images: ${content.some(c => c.type === 'image_url')}`);
91528
91652
  // According to OpenRouter docs: use /api/v1/chat/completions with modalities parameter
91653
+ // Always use content array format when we have images, even if only one element
91654
+ // For image generation models, content must be an array with text and image_url objects
91655
+ const finalContent = referenceImageApiUrls.length > 0 ? content : prompt;
91529
91656
  const requestBody = {
91530
- model: _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.imageGeneration,
91657
+ model: selectedImageModel,
91531
91658
  messages: [
91532
91659
  {
91533
91660
  role: 'user',
91534
- content: content.length > 1 ? content : prompt
91661
+ content: finalContent
91535
91662
  }
91536
91663
  ],
91537
91664
  modalities: ['image', 'text'],
91538
91665
  };
91666
+ logToTerminal('log', `📤 Request body: model=${requestBody.model}, content type=${Array.isArray(requestBody.messages[0].content) ? 'array' : 'string'}, content length=${Array.isArray(requestBody.messages[0].content) ? requestBody.messages[0].content.length : 'N/A'}, modalities=${requestBody.modalities.join(',')}`);
91667
+ if (Array.isArray(requestBody.messages[0].content)) {
91668
+ logToTerminal('log', `📤 Content array items: ${requestBody.messages[0].content.map((c) => c.type || 'unknown').join(', ')}`);
91669
+ }
91539
91670
  const startTime = Date.now();
91540
91671
  // Create AbortController for timeout (5 minutes for image generation)
91541
91672
  // Keep timeout active for the entire process, including response reading
@@ -91546,6 +91677,7 @@ function App() {
91546
91677
  }, timeoutMs);
91547
91678
  let response;
91548
91679
  try {
91680
+ logToTerminal('log', `📤 Sending request to OpenRouter API for image generation...`);
91549
91681
  response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
91550
91682
  method: 'POST',
91551
91683
  headers: {
@@ -91557,9 +91689,11 @@ function App() {
91557
91689
  body: JSON.stringify(requestBody),
91558
91690
  signal: controller.signal
91559
91691
  });
91692
+ logToTerminal('log', `📥 Response received: ${response.status} ${response.statusText}`);
91560
91693
  }
91561
91694
  catch (fetchError) {
91562
91695
  clearTimeout(timeoutId);
91696
+ logToTerminal('error', `❌ Fetch error: ${fetchError.message || String(fetchError)}`);
91563
91697
  if (fetchError.name === 'AbortError') {
91564
91698
  throw new Error('Request timeout: Image generation took too long (exceeded 5 minutes)');
91565
91699
  }
@@ -91594,17 +91728,44 @@ function App() {
91594
91728
  throw readError;
91595
91729
  }
91596
91730
  if (!response.ok) {
91731
+ logToTerminal('error', `❌ HTTP error: ${response.status} ${response.statusText}`);
91732
+ logToTerminal('error', `❌ Response text length: ${responseText.length}`);
91733
+ logToTerminal('error', `❌ Response text preview: ${responseText.substring(0, 500)}`);
91597
91734
  let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
91598
91735
  let errorDetails = `Status: ${response.status}\nStatusText: ${response.statusText}`;
91599
91736
  try {
91600
91737
  const error = JSON.parse(responseText);
91601
- errorMessage = error.error?.message || error.message || errorMessage;
91738
+ logToTerminal('error', `❌ Parsed error JSON: ${JSON.stringify(error, null, 2)}`);
91739
+ errorMessage = error.error?.message || error.message || error.error?.code || errorMessage;
91602
91740
  errorDetails += `\n\nError JSON: ${JSON.stringify(error, null, 2)}`;
91741
+ // If there's additional error info, include it
91742
+ if (error.error) {
91743
+ if (error.error.type)
91744
+ errorDetails += `\nError type: ${error.error.type}`;
91745
+ if (error.error.param)
91746
+ errorDetails += `\nError param: ${error.error.param}`;
91747
+ }
91603
91748
  }
91604
91749
  catch (e) {
91605
- errorDetails += `\n\nResponse text: ${responseText}`;
91750
+ logToTerminal('error', `❌ Failed to parse error JSON: ${e}`);
91751
+ errorDetails += `\n\nResponse text (first 1000 chars): ${responseText.substring(0, 1000)}`;
91606
91752
  errorMessage = `${errorMessage}. Response: ${responseText.substring(0, 200)}`;
91607
91753
  }
91754
+ // Log request body for debugging (without sensitive data)
91755
+ const requestBodyForLog = {
91756
+ ...requestBody,
91757
+ messages: requestBody.messages.map((msg) => ({
91758
+ role: msg.role,
91759
+ content: Array.isArray(msg.content)
91760
+ ? msg.content.map((c) => ({
91761
+ type: c.type,
91762
+ text: c.text ? c.text.substring(0, 100) + '...' : undefined,
91763
+ image_url: c.image_url ? '...' : undefined
91764
+ }))
91765
+ : typeof msg.content === 'string' ? msg.content.substring(0, 100) + '...' : msg.content
91766
+ }))
91767
+ };
91768
+ logToTerminal('error', `❌ Request body (sanitized): ${JSON.stringify(requestBodyForLog, null, 2)}`);
91608
91769
  // Create error with full details
91609
91770
  const fullError = new Error(errorMessage);
91610
91771
  fullError.status = response.status;
@@ -91724,6 +91885,11 @@ function App() {
91724
91885
  addLog(formatLogMessage(level, ...args));
91725
91886
  logToTerminal(level, ...args);
91726
91887
  };
91888
+ logMsg('log', `🔍 === Начало валидации креатива ===`);
91889
+ logMsg('log', `📦 Продукт: ${product}`);
91890
+ logMsg('log', `🌍 GEO: ${geo}`);
91891
+ logMsg('log', `🖼️ Image URL тип: ${imageUrl.startsWith('data:') ? 'data URL (base64)' : imageUrl.startsWith('http') ? 'HTTP URL' : 'другой'}`);
91892
+ logMsg('log', `🖼️ Image URL длина: ${imageUrl.length} символов`);
91727
91893
  // Получаем ключевое слово для GEO (примеры из промпта)
91728
91894
  const geoKeywords = {
91729
91895
  'HU': ['prostata', 'paraziták', 'ízület', 'potencia', 'kilók', 'látás'],
@@ -91734,9 +91900,10 @@ function App() {
91734
91900
  'SK': ['prostata', 'parazity', 'kĺby', 'potencia', 'kilo', 'zrak']
91735
91901
  };
91736
91902
  const keywords = geoKeywords[geo.toUpperCase()] || ['продукт', 'проблема'];
91903
+ logMsg('log', `🔑 Ключевые слова: ${keywords.join(', ')}`);
91737
91904
  const validationPrompt = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getValidationPrompt)(product, geo, keywords);
91738
91905
  const requestBody = {
91739
- model: _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation,
91906
+ model: selectedValidationModel,
91740
91907
  messages: [
91741
91908
  {
91742
91909
  role: 'user',
@@ -91749,15 +91916,21 @@ function App() {
91749
91916
  max_tokens: 8000
91750
91917
  };
91751
91918
  logMsg('log', '🔍 Отправка запроса на проверку креатива...');
91919
+ logMsg('log', `📊 Модель валидации: ${selectedValidationModel}`);
91920
+ logMsg('log', `📏 Размер промпта: ${validationPrompt.length} символов`);
91752
91921
  // Add timeout protection for validation request
91753
91922
  const validationStartTime = Date.now();
91754
91923
  const validationTimeoutMs = 2 * 60 * 1000; // 2 minutes timeout for validation
91755
91924
  const validationController = new AbortController();
91756
91925
  const validationTimeoutId = setTimeout(() => {
91926
+ const elapsed = Date.now() - validationStartTime;
91927
+ logMsg('error', `❌ Validation request timeout after ${Math.round(elapsed / 1000)}s`);
91757
91928
  validationController.abort();
91758
91929
  }, validationTimeoutMs);
91759
91930
  let response;
91760
91931
  try {
91932
+ const fetchStartTime = Date.now();
91933
+ logMsg('log', '📤 Отправка fetch запроса...');
91761
91934
  response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
91762
91935
  method: 'POST',
91763
91936
  headers: {
@@ -91769,26 +91942,53 @@ function App() {
91769
91942
  body: JSON.stringify(requestBody),
91770
91943
  signal: validationController.signal
91771
91944
  });
91945
+ const fetchElapsed = Date.now() - fetchStartTime;
91946
+ logMsg('log', `✅ Fetch завершен за ${Math.round(fetchElapsed / 1000)}s, статус: ${response.status} ${response.statusText}`);
91947
+ logMsg('log', `📋 Headers: Content-Type=${response.headers.get('content-type')}, Content-Length=${response.headers.get('content-length') || 'не указан'}`);
91772
91948
  }
91773
91949
  catch (fetchError) {
91774
91950
  clearTimeout(validationTimeoutId);
91951
+ const elapsed = Date.now() - validationStartTime;
91775
91952
  if (fetchError.name === 'AbortError') {
91776
- logMsg('error', '❌ Validation request timeout after 2 minutes');
91777
- throw new Error('Validation timeout: Request took too long (exceeded 2 minutes)');
91953
+ logMsg('error', `❌ Validation request timeout after ${Math.round(elapsed / 1000)}s (fetch stage)`);
91954
+ throw new Error(`Validation timeout: Request took too long (exceeded 2 minutes) - fetch stage`);
91778
91955
  }
91956
+ logMsg('error', `❌ Fetch error: ${fetchError.message || String(fetchError)}`);
91779
91957
  throw fetchError;
91780
91958
  }
91959
+ // Check response status before reading
91960
+ if (!response.ok) {
91961
+ clearTimeout(validationTimeoutId);
91962
+ logMsg('error', `❌ HTTP error: ${response.status} ${response.statusText}`);
91963
+ // Try to read error response
91964
+ try {
91965
+ const errorText = await response.text();
91966
+ logMsg('error', `❌ Error response body: ${errorText.substring(0, 500)}`);
91967
+ }
91968
+ catch (e) {
91969
+ logMsg('error', '❌ Не удалось прочитать тело ошибки');
91970
+ }
91971
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
91972
+ }
91781
91973
  // Read response with timeout protection
91782
91974
  let responseText;
91783
91975
  try {
91976
+ const readStartTime = Date.now();
91784
91977
  const readTimeoutMs = 60000; // 1 minute max for reading response
91978
+ logMsg('log', `📥 Начало чтения ответа (таймаут: ${readTimeoutMs / 1000}s)...`);
91785
91979
  responseText = await Promise.race([
91786
- response.text(),
91980
+ response.text().then(text => {
91981
+ const readElapsed = Date.now() - readStartTime;
91982
+ logMsg('log', `✅ Ответ прочитан за ${Math.round(readElapsed / 1000)}s, размер: ${text.length} символов`);
91983
+ return text;
91984
+ }),
91787
91985
  new Promise((_, reject) => {
91788
91986
  setTimeout(() => {
91987
+ const readElapsed = Date.now() - readStartTime;
91789
91988
  clearTimeout(validationTimeoutId);
91790
91989
  validationController.abort();
91791
- reject(new Error('Validation response reading timeout'));
91990
+ logMsg('error', `❌ Таймаут чтения ответа после ${Math.round(readElapsed / 1000)}s`);
91991
+ reject(new Error(`Validation response reading timeout after ${Math.round(readElapsed / 1000)}s`));
91792
91992
  }, readTimeoutMs);
91793
91993
  })
91794
91994
  ]);
@@ -91796,10 +91996,13 @@ function App() {
91796
91996
  }
91797
91997
  catch (readError) {
91798
91998
  clearTimeout(validationTimeoutId);
91999
+ const totalElapsed = Date.now() - validationStartTime;
91799
92000
  if (readError.name === 'AbortError' || readError.message?.includes('timeout')) {
91800
- logMsg('error', '❌ Validation response reading timeout');
91801
- throw new Error('Validation timeout: Failed to read response body');
92001
+ logMsg('error', `❌ Validation response reading timeout (общее время: ${Math.round(totalElapsed / 1000)}s)`);
92002
+ logMsg('error', `❌ Детали ошибки: ${readError.message || String(readError)}`);
92003
+ throw new Error(`Validation timeout: Failed to read response body after ${Math.round(totalElapsed / 1000)}s`);
91802
92004
  }
92005
+ logMsg('error', `❌ Ошибка чтения ответа: ${readError.message || String(readError)}`);
91803
92006
  throw readError;
91804
92007
  }
91805
92008
  if (!response.ok) {
@@ -91817,14 +92020,24 @@ function App() {
91817
92020
  }
91818
92021
  let data;
91819
92022
  try {
92023
+ logMsg('log', '📝 Парсинг JSON ответа...');
91820
92024
  data = JSON.parse(responseText);
92025
+ logMsg('log', `✅ JSON распарсен успешно. Структура: choices=${data.choices?.length || 0}, message=${data.choices?.[0]?.message ? 'есть' : 'нет'}`);
91821
92026
  }
91822
92027
  catch (e) {
91823
- logMsg('error', '❌ Не удалось распарсить ответ как JSON');
92028
+ logMsg('error', `❌ Не удалось распарсить ответ как JSON. Ошибка: ${e}`);
92029
+ logMsg('error', `❌ Первые 500 символов ответа: ${responseText.substring(0, 500)}`);
91824
92030
  throw new Error(`Invalid JSON response: ${responseText.substring(0, 200)}`);
91825
92031
  }
91826
92032
  const content = data.choices?.[0]?.message?.content || '';
91827
- logMsg('log', '✅ Получен ответ от модели проверки');
92033
+ logMsg('log', `✅ Получен ответ от модели проверки. Длина контента: ${content.length} символов`);
92034
+ if (content.length > 0) {
92035
+ logMsg('log', `📄 Первые 300 символов ответа: ${content.substring(0, 300)}`);
92036
+ }
92037
+ else {
92038
+ logMsg('error', '⚠️ Контент ответа пустой!');
92039
+ logMsg('error', `📋 Полный объект data: ${JSON.stringify(data, null, 2).substring(0, 1000)}`);
92040
+ }
91828
92041
  // Update balance after successful API call
91829
92042
  fetchOpenRouterBalance();
91830
92043
  // Парсим результат
@@ -91855,10 +92068,21 @@ function App() {
91855
92068
  errors.push(...lines.map(l => l.trim()).filter(l => l.length > 0));
91856
92069
  }
91857
92070
  }
92071
+ const totalElapsed = Date.now() - validationStartTime;
92072
+ const finalErrors = errors.length > 0 ? errors : (status === 'needs_rebuild' ? ['Обнаружены нарушения в креативе'] : []);
92073
+ logMsg('log', `✅ === Валидация завершена ===`);
92074
+ logMsg('log', `⏱️ Общее время: ${Math.round(totalElapsed / 1000)}s`);
92075
+ logMsg('log', `📊 Статус: ${status === 'ok' ? '✅ OK' : '❌ НУЖНА ПЕРЕСБОРКА'}`);
92076
+ logMsg('log', `🔍 Найдено ошибок: ${finalErrors.length}`);
92077
+ if (finalErrors.length > 0) {
92078
+ finalErrors.forEach((error, idx) => {
92079
+ logMsg('error', ` ${idx + 1}. ${error}`);
92080
+ });
92081
+ }
91858
92082
  return {
91859
92083
  status,
91860
92084
  result: content,
91861
- errors: errors.length > 0 ? errors : (status === 'needs_rebuild' ? ['Обнаружены нарушения в креативе'] : [])
92085
+ errors: finalErrors
91862
92086
  };
91863
92087
  };
91864
92088
  const generateProductFromBanka = async (bankaImageUrls, additionalPrompt = '', addLog, currentProductImageUrl) => {
@@ -92801,7 +93025,7 @@ function App() {
92801
93025
  setGeneratedImagesData(initialPlaceholders);
92802
93026
  const imagePrompts = approaches.map(approach => `${basePromptStructure}🎯 ПОДХОД: ${approach.name}\n\n${approach.prompt}\n\n${approach.headlineAngle}\n\n${approach.bulletsFocus}\n\nТекст (заголовок/буллеты) — строго следуй указанным углам и акцентам для этого подхода.`);
92803
93027
  addLog(formatLogMessage('log', '📝 Generated prompts for 5 different approaches'));
92804
- const maxParallel = 3;
93028
+ const maxParallel = 5;
92805
93029
  addLog(formatLogMessage('log', `🚀 Generating 5 images in parallel (up to ${maxParallel} at a time)...`));
92806
93030
  const generationStartTime = Date.now();
92807
93031
  const resultsMap = new Map();
@@ -93035,72 +93259,111 @@ function App() {
93035
93259
  // Get current customRegeneratePrompt from state
93036
93260
  const currentImageData = generatedImagesData.find(img => img.index === imageData.index);
93037
93261
  const customPrompt = currentImageData?.customRegeneratePrompt?.trim() || '';
93038
- // Update status to regenerating
93262
+ // Use current imageUrl from state, not from props (to get the latest version)
93263
+ const currentImageUrl = currentImageData?.imageUrl || imageData.imageUrl;
93264
+ // Update status to regenerating - clear old image URL immediately to force re-render
93039
93265
  setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
93040
- ? { ...img, regenerating: true, regenerateStartTime: Date.now(), checkStatus: 'pending' }
93266
+ ? {
93267
+ ...img,
93268
+ regenerating: true,
93269
+ regenerateStartTime: Date.now(),
93270
+ checkStatus: 'pending',
93271
+ imageUrl: undefined // Clear old image URL to force re-render with new one
93272
+ }
93041
93273
  : img));
93042
93274
  addLog(formatLogMessage('log', `🔄 Переделка изображения ${imageData.index} (${imageData.approach})...`));
93043
93275
  // Build improved prompt with custom instructions
93044
93276
  let improvedPrompt = imageData.originalPrompt;
93045
93277
  if (customPrompt) {
93046
- if (imageData.imageUrl) {
93047
- // For existing images, use reference
93048
- improvedPrompt = `${imageData.originalPrompt}
93278
+ if (currentImageUrl) {
93279
+ // For existing images, use reference - only the last generated image is attached
93280
+ improvedPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
93049
93281
 
93050
- ⚠️ КРИТИЧНО: Предыдущий вариант требует изменений. Прикреплены два референса:
93051
- 1. Текущий креатив (требует изменений согласно требованиям ниже)
93052
- 2. Изображение продукта (используй его как основу для упаковки)
93282
+ ${imageData.originalPrompt}
93283
+
93284
+ ⚠️ КРИТИЧНО: Предыдущий вариант требует изменений. Прикреплен текущий креатив (требует изменений согласно требованиям ниже).
93285
+ В креативе уже есть упаковка продукта - используй её как основу. Если в требованиях ниже не указано иное - сохрани форму и цвет упаковки.
93053
93286
 
93054
93287
  ТРЕБОВАНИЯ К ИЗМЕНЕНИЯМ:
93055
93288
  ${customPrompt.split('\n').map(line => `- ${line}`).join('\n')}
93056
93289
 
93057
93290
  ТРЕБОВАНИЯ К ИСПРАВЛЕНИЮ:
93058
- - Учти ВСЕ указанные требования в новом варианте
93291
+ - Учти ВСЕ указанные требования в новом варианте (включая изменения упаковки, если они указаны)
93059
93292
  - Сохрани тот же подход и стиль (${imageData.approach})
93060
- - Используй упаковку продукта из второго референса (не меняй форму/цвет упаковки)
93061
- - Убедись, что новый вариант соответствует всем правилам из основного промпта`;
93293
+ - Если в требованиях не указано изменение упаковки - сохрани упаковку продукта из прикрепленного креатива
93294
+ - Убедись, что новый вариант соответствует всем правилам из основного промпта
93295
+
93296
+ ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
93062
93297
  }
93063
93298
  else {
93064
- // For failed generations, add custom requirements if provided
93065
- improvedPrompt = `${imageData.originalPrompt}
93299
+ // For failed generations, add custom requirements if provided - only product image is attached
93300
+ improvedPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
93301
+
93302
+ ${imageData.originalPrompt}
93066
93303
 
93067
- ⚠️ Повторная генерация после неудачной попытки.
93304
+ ⚠️ Повторная генерация после неудачной попытки. Прикреплено изображение продукта - используй его упаковку как основу. Если в требованиях ниже не указано иное - сохрани форму и цвет упаковки.
93068
93305
 
93069
- ${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPrompt.split('\n').map(line => `- ${line}`).join('\n')}\n\n` : ''}Убедись, что новый вариант соответствует всем правилам из основного промпта.`;
93306
+ ${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPrompt.split('\n').map(line => `- ${line}`).join('\n')}\n\n` : ''}Убедись, что новый вариант соответствует всем правилам из основного промпта.
93307
+
93308
+ ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
93070
93309
  }
93071
93310
  }
93072
93311
  else {
93073
93312
  // If no custom prompt
93074
- if (imageData.imageUrl) {
93075
- // For existing images, just add general improvement instruction
93076
- improvedPrompt = `${imageData.originalPrompt}
93313
+ if (currentImageUrl) {
93314
+ // For existing images, just add general improvement instruction - only the last generated image is attached
93315
+ improvedPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
93316
+
93317
+ ${imageData.originalPrompt}
93077
93318
 
93078
93319
  ⚠️ Переделка: улучши текущий вариант, сохранив подход и стиль (${imageData.approach}).
93079
- Прикреплены два референса:
93080
- 1. Текущий креатив (улучши его)
93081
- 2. Изображение продукта (используй его упаковку как основу, не меняй форму/цвет)`;
93320
+ Прикреплен текущий креатив - улучши его, сохранив упаковку продукта из креатива (не меняй форму/цвет упаковки).
93321
+
93322
+ ВАЖНО: Твой ответ это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
93082
93323
  }
93083
93324
  else {
93084
- // For failed generations, use original prompt as-is
93085
- improvedPrompt = imageData.originalPrompt;
93325
+ // For failed generations, use original prompt as-is - only product image is attached
93326
+ improvedPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
93327
+
93328
+ ${imageData.originalPrompt}
93329
+
93330
+ ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
93086
93331
  }
93087
93332
  }
93088
- // Generate new image: use both current creative image (if exists) and product image as references
93089
- // This allows the model to see what needs to be changed while keeping the product reference
93333
+ // Generate new image: use only the last generated image if it exists, otherwise use product image
93334
+ // If there's a last generated image, attach only that (model will use product from it)
93335
+ // If no last image exists, attach only the product image
93090
93336
  const referenceImages = [];
93091
- if (imageData.imageUrl) {
93092
- // If regenerating existing image, use it as primary reference
93093
- referenceImages.push(imageData.imageUrl);
93337
+ if (currentImageUrl) {
93338
+ // If regenerating existing image, use ONLY the last generated image as reference
93339
+ referenceImages.push(currentImageUrl);
93340
+ addLog(formatLogMessage('log', `🖼️ Используется последнее сгенерированное изображение как референс`));
93341
+ }
93342
+ else {
93343
+ // If no last image exists, use only product image
93344
+ referenceImages.push(imageData.productImageUrl);
93345
+ addLog(formatLogMessage('log', `🖼️ Используется изображение продукта как референс`));
93346
+ }
93347
+ // Log regeneration details
93348
+ addLog(formatLogMessage('log', `📝 Промпт для перегенерации (длина: ${improvedPrompt.length} символов)`));
93349
+ addLog(formatLogMessage('log', `🖼️ Референсных изображений: ${referenceImages.length}`));
93350
+ if (referenceImages.length > 0) {
93351
+ referenceImages.forEach((url, idx) => {
93352
+ const urlPreview = url.length > 100 ? url.substring(0, 100) + '...' : url;
93353
+ addLog(formatLogMessage('log', ` ${idx + 1}. ${urlPreview}`));
93354
+ });
93094
93355
  }
93095
- // Always include product image as reference to maintain product consistency
93096
- referenceImages.push(imageData.productImageUrl);
93356
+ addLog(formatLogMessage('log', `📤 Первые 200 символов промпта: ${improvedPrompt.substring(0, 200)}...`));
93097
93357
  const newImageUrl = await generateImageWithDALLE(improvedPrompt, referenceImages);
93098
93358
  addLog(formatLogMessage('log', `✅ Изображение ${imageData.index} переделано успешно`));
93099
93359
  // Update image data - force React to re-render by creating a completely new object
93100
- // Add timestamp only for HTTP URLs, not for data URLs, and only if timestamp is not already present
93360
+ // Always add timestamp for HTTP URLs to prevent browser caching (remove old timestamp if exists)
93101
93361
  let updatedImageUrl = newImageUrl;
93102
- if (!newImageUrl.startsWith('data:') && !newImageUrl.includes('t=')) {
93103
- updatedImageUrl = newImageUrl + (newImageUrl.includes('?') ? '&' : '?') + `t=${Date.now()}`;
93362
+ if (!newImageUrl.startsWith('data:')) {
93363
+ // Remove existing timestamp parameter if present
93364
+ const urlWithoutTimestamp = newImageUrl.replace(/[?&]t=\d+/g, '');
93365
+ const separator = urlWithoutTimestamp.includes('?') ? '&' : '?';
93366
+ updatedImageUrl = `${urlWithoutTimestamp}${separator}t=${Date.now()}`;
93104
93367
  }
93105
93368
  setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
93106
93369
  ? {
@@ -93171,7 +93434,7 @@ ${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPr
93171
93434
  logToTerminal('log', msg.replace(/\[.*?\]\s*/, ''));
93172
93435
  };
93173
93436
  try {
93174
- // Mark as regenerating (show overlay timer)
93437
+ // Mark as regenerating (show overlay timer) - clear old image URL immediately to force re-render
93175
93438
  setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
93176
93439
  ? {
93177
93440
  ...img,
@@ -93184,17 +93447,26 @@ ${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPr
93184
93447
  checkResult: undefined,
93185
93448
  checkErrors: undefined,
93186
93449
  // Explicitly ignore any previous comments
93187
- customRegeneratePrompt: ''
93450
+ customRegeneratePrompt: '',
93451
+ imageUrl: undefined // Clear old image URL to force re-render with new one
93188
93452
  }
93189
93453
  : img));
93190
93454
  addLog(formatLogMessage('log', `🔁 Переделка заново (с нуля) изображения ${imageData.index} (${imageData.approach})...`));
93191
- // Generate using only product image as reference, and original prompt as-is
93192
- const newImageUrl = await generateImageWithDALLE(imageData.originalPrompt, imageData.productImageUrl);
93455
+ // Generate using only product image as reference, and original prompt with image generation instructions
93456
+ const freshPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
93457
+
93458
+ ${imageData.originalPrompt}
93459
+
93460
+ ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
93461
+ const newImageUrl = await generateImageWithDALLE(freshPrompt, imageData.productImageUrl);
93193
93462
  addLog(formatLogMessage('log', `✅ Изображение ${imageData.index} (с нуля) сгенерировано`));
93194
- // Update image data
93463
+ // Update image data - always add timestamp for HTTP URLs to prevent browser caching
93195
93464
  let updatedImageUrl = newImageUrl;
93196
- if (!newImageUrl.startsWith('data:') && !newImageUrl.includes('t=')) {
93197
- updatedImageUrl = newImageUrl + (newImageUrl.includes('?') ? '&' : '?') + `t=${Date.now()}`;
93465
+ if (!newImageUrl.startsWith('data:')) {
93466
+ // Remove existing timestamp parameter if present
93467
+ const urlWithoutTimestamp = newImageUrl.replace(/[?&]t=\d+/g, '');
93468
+ const separator = urlWithoutTimestamp.includes('?') ? '&' : '?';
93469
+ updatedImageUrl = `${urlWithoutTimestamp}${separator}t=${Date.now()}`;
93198
93470
  }
93199
93471
  setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
93200
93472
  ? {
@@ -93787,6 +94059,16 @@ ${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPr
93787
94059
  !loadingImageModels && imageModels.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_23__["default"], null, selectedImageModel === _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.imageGeneration
93788
94060
  ? 'Используется модель по умолчанию'
93789
94061
  : 'Выбрана модель: ' + (imageModels.find(m => m.id === selectedImageModel)?.name || selectedImageModel)))))),
94062
+ openaiApiKey && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2 } },
94063
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], { fullWidth: true, variant: "outlined" },
94064
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], null, "\u041C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043A\u0440\u0435\u0430\u0442\u0438\u0432\u043E\u0432"),
94065
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_21__["default"], { value: selectedValidationModel, onChange: (e) => handleValidationModelChange(e.target.value), label: "\u041C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043A\u0440\u0435\u0430\u0442\u0438\u0432\u043E\u0432", disabled: loadingValidationModels || validationModels.length === 0 }, loadingValidationModels ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { disabled: true },
94066
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
94067
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }),
94068
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043C\u043E\u0434\u0435\u043B\u0435\u0439...")))) : validationModels.length === 0 ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { disabled: true }, "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043C\u043E\u0434\u0435\u043B\u0435\u0439")) : (validationModels.map((model) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { key: model.id, value: model.id }, model.name))))),
94069
+ !loadingValidationModels && validationModels.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_23__["default"], null, selectedValidationModel === _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation
94070
+ ? 'Используется модель по умолчанию'
94071
+ : 'Выбрана модель: ' + (validationModels.find(m => m.id === selectedValidationModel)?.name || selectedValidationModel)))))),
93790
94072
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
93791
94073
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "primary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_34__["default"], null), onClick: handleGenerateContent, disabled: generating || !openaiApiKey || !generateProduct.trim() || !generateGeo.trim(), sx: { flexGrow: 1 }, size: "large" }, generating ? 'Generating...' : 'Generate Titles & Descriptions'),
93792
94074
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "secondary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_34__["default"], null), onClick: handleGenerateImages, disabled: generatingImages ||
@@ -94675,7 +94957,9 @@ OTHER_TEXT: ["..."] (любой другой рекламный текст на
94675
94957
  * Базовый промпт для генерации изображений креативов
94676
94958
  */
94677
94959
  function getImageGenerationBasePrompt(generateGeo, generatePrice, generateCurrency) {
94678
- return `Создай рекламный креатив для Facebook Ads 1:1 (квадрат).
94960
+ return `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
94961
+
94962
+ Создай рекламный креатив для Facebook Ads 1:1 (квадрат).
94679
94963
  Язык текста: ${generateGeo}.
94680
94964
 
94681
94965
  КРИТИЧНО — РЕЛЕВАНТНОСТЬ ПРОДУКТУ:
@@ -94751,7 +95035,9 @@ AUTO-CHECK (перед финалом):
94751
95035
  - Нет лишнего текста кроме: заголовка, буллетов, цены, скидки, кнопки
94752
95036
  - Есть скидка «-50%» (вне упаковки) и она слабее CTA
94753
95037
  - CTA визуально сильнее цены/скидки
94754
- Если хоть один пункт нарушен — пересоздай креатив.`;
95038
+ Если хоть один пункт нарушен — пересоздай креатив.
95039
+
95040
+ ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
94755
95041
  }
94756
95042
  const CREO_APPROACHES = [
94757
95043
  {