docs-combiner 0.1.4 → 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 = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
93066
93301
 
93067
- ⚠️ Повторная генерация после неудачной попытки.
93302
+ ${imageData.originalPrompt}
93068
93303
 
93069
- ${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPrompt.split('\n').map(line => `- ${line}`).join('\n')}\n\n` : ''}Убедись, что новый вариант соответствует всем правилам из основного промпта.`;
93304
+ ⚠️ Повторная генерация после неудачной попытки. Прикреплено изображение продукта - используй его упаковку как основу. Если в требованиях ниже не указано иное - сохрани форму и цвет упаковки.
93305
+
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 ||
@@ -94499,7 +94781,7 @@ function getTitlesSystemPrompt(geo) {
94499
94781
  - Используй знания лучших практик рекламного копирайтинга
94500
94782
  - Избегай только жёстких медицинских гарантий («лечит», «вылечивает», «гарантируем»). Мягкие обещания результата разрешены («Nopți fără treziri», «Stomac liniștit», «Efect rapid»). Разрешены: сроки («în 14 zile»), цифры соц. доказательства («9 din 10 bărbați»), мягкие результаты. Запрещены: «100% гарантия», «навсегда», «излечение».
94501
94783
  - Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
94502
- - Слова «paraziți», «viermi»использовать осторожно, не в каждом заголовке. Максимум 1 заголовок из 3 может содержать прямое упоминание. Предпочитай мягкие формулировки: «toxine», «disconfort digestiv», «curățare». Если все 3 заголовка содержат «paraziți/viermi» — перепиши 2 из них с мягкими альтернативами
94784
+ - Если в категории продукта есть специфические медицинские термины или диагнозы используй их осторожно, не в каждом заголовке. Максимум 1 заголовок из 3 может содержать прямое упоминание таких терминов. Предпочитай мягкие формулировки, описывающие симптомы и дискомфорт, а не диагнозы. Если все 3 заголовка содержат прямые медицинские термины — перепиши 2 из них с мягкими альтернативами, фокусируясь на ощущениях и симптомах
94503
94785
  - После создания проверь их правильность и разнообразие
94504
94786
  - Верни только заголовки, по одному на строку, без нумерации или буллетов
94505
94787
  - Заголовки должны быть на языке ${geo}
@@ -94560,8 +94842,8 @@ function getTextsSystemPrompt(geo) {
94560
94842
  - Разрешены формулировки: «reduce frecvența», «elimină disconfortul», «rezultat vizibil în X zile», «9 din 10 observă diferența». Запрещены только: «лечит/vindecă», «гарантируем 100%», «навсегда/definitiv».
94561
94843
  - Ты можешь усиливать эффект и результаты, но без прямых медицинских обещаний
94562
94844
  - Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
94563
- - Слова «paraziți», «viermi» можно использовать, но не в каждом тексте. Максимум 1 текст из 3 может содержать прямое упоминание паразитов. Остальные тексты фокусируй на симптомах: «balonare», «disconfort», «digestie grea», «senzație de greutate», «toxine». Эвфемизмы вроде «oaspeți nedoriți»избегай, звучит неестественно
94564
- - Избегай упоминания туалета/стула напрямую («scaun», «constipație», «diaree»). Используй мягкие формулировки: «digestie lentă», «disconfort», «senzație de greutate»
94845
+ - Если в категории продукта есть специфические медицинские термины или диагнозы — используй их осторожно, не в каждом тексте. Максимум 1 текст из 3 может содержать прямое упоминание таких терминов. Остальные тексты фокусируй на симптомах, дискомфорте и ощущениях пользователя, релевантных категории продукта. Избегай эвфемизмов и искусственных замениспользуй естественные формулировки
94846
+ - Избегай прямых упоминаний интимных или деликатных деталей, связанных с проблемой. Используй мягкие формулировки, описывающие дискомфорт и симптомы, а не физиологические процессы
94565
94847
  - Избегай повторения полного названия продукта более 2 раз в одном тексте. Используй местоимения или более короткие ссылки при необходимости
94566
94848
 
94567
94849
  Требования к формату:
@@ -94644,7 +94926,7 @@ OTHER_TEXT: ["..."] (любой другой рекламный текст на
94644
94926
  - ЗАПРЕЩЕНЫ только: «лечит», «вылечивает», «гарантируем», «100%», «навсегда», «полностью избавляет», диагностика заболеваний
94645
94927
  - «Выводит/устраняет токсины» для детокс-продуктов — это мягкий клейм, НЕ медицинская гарантия
94646
94928
  - Если найден запрещённый клейм — ошибка. Мягкие клеймы — не ошибка.
94647
- - Слова «paraziți», «viermi» — разрешены, но не должны повторяться чрезмерно. Если в HEADLINE + BULLETS слово «paraziți» или «viermi» встречается более 1 раза — РЕКОМЕНДАЦИЯ: разнообразить формулировки
94929
+ - Специфические медицинские термины или диагнозы — разрешены, но не должны повторяться чрезмерно. Если в HEADLINE + BULLETS один и тот же специфический термин встречается более 1 раза — РЕКОМЕНДАЦИЯ: разнообразить формулировки, использовать синонимы или фокус на симптомах вместо повторения термина
94648
94930
 
94649
94931
  ШАГ 3 — TEXT LIMIT (критично):
94650
94932
  - Общее число слов в HEADLINE + BULLETS <= 16. Если больше — ошибка.
@@ -94675,8 +94957,18 @@ 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}.
94964
+
94965
+ КРИТИЧНО — РЕЛЕВАНТНОСТЬ ПРОДУКТУ:
94966
+ - Заголовок и буллиты ДОЛЖНЫ соответствовать категории и проблеме продукта из его названия и описания
94967
+ - НЕ копируй примеры из промпта буквально — примеры показывают только ФОРМАТ, не содержание
94968
+ - Определи категорию продукта (диабет, суставы, пищеварение, простата, зрение, похудение и т.д.) и используй ТОЛЬКО релевантные этой категории проблемы и выгоды
94969
+ - Если примеры в промпте про другую категорию — игнорируй их содержание, используй только структуру
94970
+ - Прочитай название и описание продукта внимательно и построй заголовок/буллиты вокруг ЕГО проблематики
94971
+
94680
94972
  Используй предоставленный рендер упаковки продукта как есть (форма/цвет без изменений). На упаковке запрещены текст/цена/буллеты/стикеры.
94681
94973
 
94682
94974
  CRITICAL RULES (это ВАЛИДАЦИЯ, не рекомендации; если нарушено — УДАЛИ ЛИШНЕЕ и ПЕРЕСОБЕРИ МАКЕТ):
@@ -94699,7 +94991,7 @@ HEADLINE (строгое правило):
94699
94991
  - если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт.
94700
94992
  - Каждый из 5 креативов должен иметь УНИКАЛЬНЫЙ заголовок. Не повторяй один и тот же заголовок на разных визуалах. Варьируй: угол боли, формулировку, акцент (проблема vs облегчение)
94701
94993
  - Вторая часть заголовка = конкретное улучшение, не абстрактное слово. ПРАВИЛЬНО: «Stomac liniștit», «Zile fără disconfort», «Confort după masă». НЕ ТАК: «Mai ușor», «Mai bine», «Soluția»
94702
- - Слова «paraziți», «viermi» разрешены в заголовке, но не обязательны. Варьируй между прямыми («Paraziți?») и мягкими («Toxine? Disconfort?») формулировками для разных креативов
94994
+ - Специфические медицинские термины или диагнозы разрешены в заголовке, но не обязательны. Варьируй между прямыми формулировками (если релевантно категории) и мягкими формулировками, фокусирующимися на симптомах и дискомфорте, для разных креативов. Не используй один и тот же термин во всех креативах
94703
94995
  ПРИМЕР (HU, только как формат; для других GEO адаптируй язык, не копируй слова):
94704
94996
  - НЕ ТАК: «Makacs méreganyag ellen» (лозунг)
94705
94997
  - ТАК: «Puffadás gond? Könnyebb napok»
@@ -94743,38 +95035,40 @@ AUTO-CHECK (перед финалом):
94743
95035
  - Нет лишнего текста кроме: заголовка, буллетов, цены, скидки, кнопки
94744
95036
  - Есть скидка «-50%» (вне упаковки) и она слабее CTA
94745
95037
  - CTA визуально сильнее цены/скидки
94746
- Если хоть один пункт нарушен — пересоздай креатив.`;
95038
+ Если хоть один пункт нарушен — пересоздай креатив.
95039
+
95040
+ ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
94747
95041
  }
94748
95042
  const CREO_APPROACHES = [
94749
95043
  {
94750
95044
  name: 'Врач / Эксперт (мужчина)',
94751
95045
  prompt: `ВРАЧ-ЭКСПЕРТ (мужчина): спокойный профессионал, кабинет/клиника; врач слева/по центру, продукт справа, чистый нейтральный фон, фронтальный ракурс. Врач виден минимум по пояс, в белом халате со стетоскопом.`,
94752
- headlineAngle: `ЗАГОЛОВОК ДЛЯ ЭТОГО КРЕАТИВА: угол "ежедневная проблема + спокойствие". Примеры: «Balonare zilnică? Stomac liniștit», «Disconfort frecvent? Liniște digestivă».`,
94753
- bulletsFocus: `БУЛЛИТЫ ДЛЯ ЭТОГО КРЕАТИВА: акцент на натуральности и доверии — выбери 3 из: «Formulă naturală», «Extracte din plante», «Recomandat de specialiști», «Fără chimicale».`
95046
+ headlineAngle: `ЗАГОЛОВОК: угол «ежедневная проблема + спокойствие». Возьми ключевую проблему из описания продукта + мягкое обещание облегчения. Примеры в промпте только для формата, НЕ копируй их содержание.`,
95047
+ bulletsFocus: `БУЛЛИТЫ: акцент на натуральности и доверии — натуральная формула, растительные экстракты, рекомендовано специалистами, без химии. Адаптируй под язык GEO и категорию продукта. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте — только ориентир для смысла, переводи их на нужный язык.`
94754
95048
  },
94755
95049
  {
94756
95050
  name: 'Врач / Эксперт (женщина)',
94757
95051
  prompt: `ВРАЧ-ЭКСПЕРТ (женщина): тёплый свет, уютный кабинет; врач справа/в углу, продукт слева/по центру. Врач в фокусе, чётко виден, лицо читаемое, профессиональный вид.`,
94758
- headlineAngle: `ЗАГОЛОВОК ДЛЯ ЭТОГО КРЕАТИВА: угол "после еды + облегчение". Примеры: «Balonare după masă? Digestie ușoară», «Greutate după masă? Confort rapid».`,
94759
- bulletsFocus: `БУЛЛИТЫ ДЛЯ ЭТОГО КРЕАТИВА: акцент на результате — выбери 3 из: «Reduce balonarea», «Confort abdominal», «Efect în 7-14 zile», «Digestie mai ușoară».`
95052
+ headlineAngle: `ЗАГОЛОВОК: угол «состояние/момент + облегчение». Возьми типичный момент проявления проблемы из описания продукта + конкретный результат. НЕ копируй примеры из других категорий.`,
95053
+ bulletsFocus: `БУЛЛИТЫ: акцент на результате — эффект, срок действия, улучшение состояния. Адаптируй под продукт и язык GEO. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте — только ориентир для смысла, переводи их на нужный язык.`
94760
95054
  },
94761
95055
  {
94762
95056
  name: 'Lifestyle / Clean',
94763
95057
  prompt: `LIFESTYLE/CLEAN: продукт в контексте момента приёма — кухня, обеденный стол, рядом с едой/чаем. Без человека, но с "историей" использования. Тёплые цвета, естественный свет.`,
94764
- headlineAngle: `ЗАГОЛОВОК ДЛЯ ЭТОГО КРЕАТИВА: угол "социальное доказательство". Примеры: «9 din 10 aleg pentru digestie», «Peste 5.000 de clienți mulțumiți», «Ales de mii pentru confort».`,
94765
- bulletsFocus: `БУЛЛИТЫ ДЛЯ ЭТОГО КРЕАТИВА: акцент на простоте использования — выбери 3 из: «Administrare ușoară», «O capsulă pe zi», «Potrivit zilnic», «Fără rețetă».`
95058
+ headlineAngle: `ЗАГОЛОВОК: угол «социальное доказательство». Цифры клиентов/пользователей + основная выгода продукта. Формат: «X из Y выбирают...», «Более X клиентов...». НЕ копируй примеры из других категорий.`,
95059
+ bulletsFocus: `БУЛЛИТЫ: акцент на простоте использования — лёгкость приёма, удобство, доступность без рецепта. Адаптируй под продукт и язык GEO. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте — только ориентир для смысла, переводи их на нужный язык.`
94766
95060
  },
94767
95061
  {
94768
95062
  name: 'WOW-эффект',
94769
95063
  prompt: `WOW-ЭФФЕКТ: визуальная метафора действия — свежесть, очищение, лёгкость. Капли воды, зелёные листья в движении, эффект «чистоты изнутри». Премиально, минималистично. Фокус на эффекте, не на человеке.`,
94770
- headlineAngle: `ЗАГОЛОВОК ДЛЯ ЭТОГО КРЕАТИВА: угол "очищение/детокс". Примеры: «Toxine? Curățare blândă», «Curățare naturală de toxine», «Organism curat, zi de zi».`,
94771
- bulletsFocus: `БУЛЛИТЫ ДЛЯ ЭТОГО КРЕАТИВА: акцент на действии продукта выбери 3 из: «Elimină toxinele», «Curățare naturală», «Susține digestia», «Detox blând».`
95064
+ headlineAngle: `ЗАГОЛОВОК: угол «ключевое действие продукта». Что продукт делает + мягкий результат. Визуальная метафора в заголовке. НЕ копируй примеры из других категорий.`,
95065
+ bulletsFocus: `БУЛЛИТЫ: акцент на механизме действия — что продукт делает, как работает, какой эффект даёт. Адаптируй под продукт и язык GEO. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте только ориентир для смысла, переводи их на нужный язык.`
94772
95066
  },
94773
95067
  {
94774
95068
  name: 'Эмоция результата',
94775
95069
  prompt: `ЭМОЦИЯ РЕЗУЛЬТАТА: реальный человек с естественным облегчением (тихий выдох, расслабленная поза), без стоковой улыбки. Фокус на человеке и его состоянии после приёма.`,
94776
- headlineAngle: `ЗАГОЛОВОК ДЛЯ ЭТОГО КРЕАТИВА: угол "срочность + результат". Примеры: «Doar aziconfort digestiv», «Ofertă limitată fără balonare», «Ultimele bucăți stomac ușor».`,
94777
- bulletsFocus: `БУЛЛИТЫ ДЛЯ ЭТОГО КРЕАТИВА: акцент на ощущениях — выбери 3 из: «Te simți mai ușor», «Mai multă energie», «Fără disconfort», «Confort după masă».`
95070
+ headlineAngle: `ЗАГОЛОВОК: угол «срочность + результат». Ограниченное предложение/время + основная выгода продукта. Формат: «Только сегодня ...», «Последние дни...». НЕ копируй примеры из других категорий.`,
95071
+ bulletsFocus: `БУЛЛИТЫ: акцент на ощущениях пользователя как человек себя чувствует после приёма, улучшение качества жизни. Адаптируй под продукт и язык GEO. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте — только ориентир для смысла, переводи их на нужный язык.`
94778
95072
  }
94779
95073
  ];
94780
95074