docs-combiner 0.1.9 → 0.1.11

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
@@ -99616,37 +99616,34 @@ __webpack_require__.r(__webpack_exports__);
99616
99616
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/InputLabel/InputLabel.js");
99617
99617
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Select/Select.js");
99618
99618
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/MenuItem/MenuItem.js");
99619
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Tooltip/Tooltip.js");
99620
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButtonGroup/ToggleButtonGroup.js");
99621
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButton/ToggleButton.js");
99622
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Paper/Paper.js");
99623
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Dialog/Dialog.js");
99624
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Backdrop/Backdrop.js");
99625
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/DialogTitle/DialogTitle.js");
99626
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/DialogContent/DialogContent.js");
99627
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/DialogActions/DialogActions.js");
99628
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/styles/createTheme.js");
99629
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/styles/ThemeProvider.js");
99630
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/AccountBalance.js");
99631
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/AutoAwesome.js");
99632
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Brightness4.js");
99633
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Brightness7.js");
99634
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/CheckCircle.js");
99635
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/CloudDownload.js");
99636
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/ContentCopy.js");
99637
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/ExpandMore.js");
99638
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/InfoOutlined.js");
99639
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Login.js");
99640
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Logout.js");
99641
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/NoteAdd.js");
99642
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Replay.js");
99643
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Settings.js");
99644
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Visibility.js");
99645
- /* harmony import */ var _PromptManagerDialog__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(/*! ./PromptManagerDialog */ "./src/PromptManagerDialog.tsx");
99646
- /* harmony import */ var _promptOverrides__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(/*! ./promptOverrides */ "./src/promptOverrides.ts");
99647
- /* harmony import */ var xlsx__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(/*! xlsx */ "./node_modules/xlsx/xlsx.mjs");
99648
- /* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(/*! jszip */ "./node_modules/jszip/dist/jszip.min.js");
99649
- /* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_54___default = /*#__PURE__*/__webpack_require__.n(jszip__WEBPACK_IMPORTED_MODULE_54__);
99619
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/FormControlLabel/FormControlLabel.js");
99620
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Checkbox/Checkbox.js");
99621
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Tooltip/Tooltip.js");
99622
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButtonGroup/ToggleButtonGroup.js");
99623
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButton/ToggleButton.js");
99624
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Paper/Paper.js");
99625
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/styles/createTheme.js");
99626
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/styles/ThemeProvider.js");
99627
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/AccountBalance.js");
99628
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/AutoAwesome.js");
99629
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Brightness4.js");
99630
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Brightness7.js");
99631
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/CheckCircle.js");
99632
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/CloudDownload.js");
99633
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/ContentCopy.js");
99634
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/ExpandMore.js");
99635
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/InfoOutlined.js");
99636
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Login.js");
99637
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Logout.js");
99638
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/NoteAdd.js");
99639
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Replay.js");
99640
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Settings.js");
99641
+ /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Visibility.js");
99642
+ /* harmony import */ var _PromptManagerDialog__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(/*! ./PromptManagerDialog */ "./src/PromptManagerDialog.tsx");
99643
+ /* harmony import */ var _promptOverrides__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(/*! ./promptOverrides */ "./src/promptOverrides.ts");
99644
+ /* harmony import */ var xlsx__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(/*! xlsx */ "./node_modules/xlsx/xlsx.mjs");
99645
+ /* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(/*! jszip */ "./node_modules/jszip/dist/jszip.min.js");
99646
+ /* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_51___default = /*#__PURE__*/__webpack_require__.n(jszip__WEBPACK_IMPORTED_MODULE_51__);
99650
99647
 
99651
99648
 
99652
99649
 
@@ -99722,7 +99719,6 @@ function App() {
99722
99719
  const [pairTranslations, setPairTranslations] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({});
99723
99720
  const [translatingPairs, setTranslatingPairs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99724
99721
  const [driveFolderUrl, setDriveFolderUrl] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99725
- const [brand, setBrand] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99726
99722
  const [link, setLink] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99727
99723
  const [openaiApiKey, setOpenaiApiKey] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99728
99724
  const [openRouterBalance, setOpenRouterBalance] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
@@ -99747,6 +99743,10 @@ function App() {
99747
99743
  return saved || _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation;
99748
99744
  });
99749
99745
  const [loadingValidationModels, setLoadingValidationModels] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99746
+ const [validationDisabled, setValidationDisabled] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => {
99747
+ const saved = localStorage.getItem('validationDisabled');
99748
+ return saved === 'true';
99749
+ });
99750
99750
  const [imageAspectRatio, setImageAspectRatio] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => {
99751
99751
  const saved = localStorage.getItem('imageAspectRatio');
99752
99752
  // Migrate legacy values (4:5, 9:16) to 2:3
@@ -99870,28 +99870,66 @@ function App() {
99870
99870
  const { price, currency } = parsePriceAndCurrency(value);
99871
99871
  return price.trim() !== '' && currency.trim() !== '';
99872
99872
  };
99873
+ // Transliterate to Latin and slugify (lowercase, dashes)
99874
+ const transliterateToSlug = (text) => {
99875
+ const cyrillicMap = {
99876
+ 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'e', 'ж': 'zh', 'з': 'z',
99877
+ 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r',
99878
+ 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'shch',
99879
+ 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya',
99880
+ 'А': 'a', 'Б': 'b', 'В': 'v', 'Г': 'g', 'Д': 'd', 'Е': 'e', 'Ё': 'e', 'Ж': 'zh', 'З': 'z',
99881
+ 'И': 'i', 'Й': 'y', 'К': 'k', 'Л': 'l', 'М': 'm', 'Н': 'n', 'О': 'o', 'П': 'p', 'Р': 'r',
99882
+ 'С': 's', 'Т': 't', 'У': 'u', 'Ф': 'f', 'Х': 'kh', 'Ц': 'ts', 'Ч': 'ch', 'Ш': 'sh', 'Щ': 'shch',
99883
+ 'Ъ': '', 'Ы': 'y', 'Ь': '', 'Э': 'e', 'Ю': 'yu', 'Я': 'ya'
99884
+ };
99885
+ let result = '';
99886
+ for (const char of text) {
99887
+ result += cyrillicMap[char] ?? char;
99888
+ }
99889
+ return result
99890
+ .toLowerCase()
99891
+ .replace(/[^a-z0-9\s-]/g, '')
99892
+ .replace(/\s+/g, '-')
99893
+ .replace(/-+/g, '-')
99894
+ .replace(/^-|-$/g, '');
99895
+ };
99896
+ // Auto-generate brand from product, geo, price
99897
+ const brand = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => {
99898
+ const productPart = (() => {
99899
+ const p = generateProduct.trim();
99900
+ if (!p)
99901
+ return '';
99902
+ const dashIdx = p.search(/[-–—]/);
99903
+ const base = dashIdx >= 0 ? p.slice(0, dashIdx).trim() : p.split(/\s+/)[0] || p;
99904
+ return transliterateToSlug(base);
99905
+ })();
99906
+ const geoPart = generateGeo.trim() ? transliterateToSlug(generateGeo.trim()) : '';
99907
+ const pricePart = (() => {
99908
+ const { price, currency } = parsePriceAndCurrency(generatePriceWithCurrency);
99909
+ if (!price && !currency)
99910
+ return '';
99911
+ const p = price ? transliterateToSlug(price) : '';
99912
+ const c = currency ? transliterateToSlug(currency) : '';
99913
+ return [p, c].filter(Boolean).join('-');
99914
+ })();
99915
+ return [productPart, geoPart, pricePart].filter(Boolean).join('-');
99916
+ }, [generateProduct, generateGeo, generatePriceWithCurrency]);
99873
99917
  const [generating, setGenerating] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99874
99918
  const [generatingImages, setGeneratingImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99875
99919
  const [imagesGenerationLogs, setImagesGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99876
99920
  const [generatedImagesData, setGeneratedImagesData] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99877
99921
  const [checkingImages, setCheckingImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99878
99922
  const [uploadingImages, setUploadingImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99879
- const [generatingProduct, setGeneratingProduct] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99880
99923
  const [uploadingProduct, setUploadingProduct] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99881
- const [productModalOpen, setProductModalOpen] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99882
- const [productSourceImages, setProductSourceImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99883
- const [productGeneratedImage, setProductGeneratedImage] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99884
- const [productRegeneratePrompt, setProductRegeneratePrompt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99885
- const [productGenerationLogs, setProductGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99886
99924
  const [folderFilesInfo, setFolderFilesInfo] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99925
+ const productFileInputRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null);
99887
99926
  const [checkingFolderFiles, setCheckingFolderFiles] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99888
99927
  const permissionCheckedFoldersRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(new Set());
99889
99928
  const folderCheckRunningRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(false);
99890
- const [productProcessStartTime, setProductProcessStartTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99891
99929
  const [imagesProcessStartTime, setImagesProcessStartTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99892
99930
  const [contentProcessStartTime, setContentProcessStartTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99893
99931
  const [contentGenerationLogs, setContentGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99894
- const [elapsedTime, setElapsedTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({ product: 0, images: 0, content: 0, landing: 0 });
99932
+ const [elapsedTime, setElapsedTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({ images: 0, content: 0, landing: 0 });
99895
99933
  const [uiNow, setUiNow] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => Date.now());
99896
99934
  const [generatingLanding, setGeneratingLanding] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99897
99935
  const [landingGenerationLogs, setLandingGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
@@ -99958,7 +99996,7 @@ function App() {
99958
99996
  }
99959
99997
  };
99960
99998
  // Create theme based on mode
99961
- const theme = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => (0,_mui_material__WEBPACK_IMPORTED_MODULE_34__["default"])({
99999
+ const theme = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => (0,_mui_material__WEBPACK_IMPORTED_MODULE_31__["default"])({
99962
100000
  palette: {
99963
100001
  mode: darkMode ? 'dark' : 'light',
99964
100002
  ...(darkMode
@@ -100050,7 +100088,7 @@ function App() {
100050
100088
  };
100051
100089
  loadKey();
100052
100090
  // Load prompt overrides from Electron config
100053
- (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_52__.loadOverridesFromElectron)();
100091
+ (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.loadOverridesFromElectron)();
100054
100092
  }, []);
100055
100093
  // Save form fields to localStorage whenever they change
100056
100094
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
@@ -100106,7 +100144,6 @@ function App() {
100106
100144
  setGeneratedImagesData([]);
100107
100145
  setTitles('');
100108
100146
  setTexts(['']);
100109
- setBrand('');
100110
100147
  setLink('');
100111
100148
  return;
100112
100149
  }
@@ -100122,7 +100159,6 @@ function App() {
100122
100159
  setGeneratedImagesData([]);
100123
100160
  setTitles('');
100124
100161
  setTexts(['']);
100125
- setBrand('');
100126
100162
  setLink('');
100127
100163
  return;
100128
100164
  }
@@ -100133,7 +100169,6 @@ function App() {
100133
100169
  setGeneratedImagesData([]);
100134
100170
  setTitles('');
100135
100171
  setTexts(['']);
100136
- setBrand('');
100137
100172
  setLink('');
100138
100173
  setLoadingContentFromDrive(true);
100139
100174
  setLoadingImagesFromDrive(true);
@@ -100146,15 +100181,8 @@ function App() {
100146
100181
  logToTerminal('error', '[Load] Error loading content from Google Drive:', err);
100147
100182
  setLoadingContentFromDrive(false);
100148
100183
  });
100149
- // Load images data from Google Drive (30s timeout)
100150
- const imagesLoadTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout: загрузка изображений заняла более 30s')), 30000));
100151
- Promise.race([loadGeneratedImagesFromDrive(folderId), imagesLoadTimeout]).then((result) => {
100152
- logToTerminal('log', '[Load] Images loading completed, found:', result.found);
100153
- setLoadingImagesFromDrive(false);
100154
- }).catch((err) => {
100155
- logToTerminal('warn', '[Load] Images loading stopped:', err?.message || err);
100156
- setLoadingImagesFromDrive(false);
100157
- });
100184
+ // Креативы не кешируются не загружаем изображения при открытии
100185
+ setLoadingImagesFromDrive(false);
100158
100186
  }, [driveFolderUrl]);
100159
100187
  // Sync generatedTitlesData with titles when titles changes (except when user is editing in UI)
100160
100188
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
@@ -100180,12 +100208,6 @@ function App() {
100180
100208
  }, [titles]); // Only depend on titles, not generatedTitlesData to avoid loops
100181
100209
  // Auto-scroll logs when new logs are added
100182
100210
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
100183
- if (productGenerationLogs.length > 0 && (generatingProduct || uploadingProduct)) {
100184
- const logContainer = document.getElementById('product-generation-logs');
100185
- if (logContainer) {
100186
- logContainer.scrollTop = logContainer.scrollHeight;
100187
- }
100188
- }
100189
100211
  if (imagesGenerationLogs.length > 0 && (generatingImages || uploadingImages)) {
100190
100212
  const logContainer = document.getElementById('images-generation-logs');
100191
100213
  if (logContainer) {
@@ -100198,30 +100220,7 @@ function App() {
100198
100220
  logContainer.scrollTop = logContainer.scrollHeight;
100199
100221
  }
100200
100222
  }
100201
- }, [productGenerationLogs, generatingProduct, uploadingProduct, imagesGenerationLogs, generatingImages, uploadingImages, contentGenerationLogs, generating]);
100202
- // Timer for product generation
100203
- (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
100204
- let interval = null;
100205
- if (generatingProduct || uploadingProduct) {
100206
- interval = setInterval(() => {
100207
- setElapsedTime(prev => {
100208
- if (productProcessStartTime) {
100209
- const elapsed = Math.floor((Date.now() - productProcessStartTime) / 1000);
100210
- return { ...prev, product: elapsed };
100211
- }
100212
- return prev;
100213
- });
100214
- }, 1000);
100215
- }
100216
- else {
100217
- setProductProcessStartTime(null);
100218
- setElapsedTime(prev => ({ ...prev, product: 0 }));
100219
- }
100220
- return () => {
100221
- if (interval)
100222
- clearInterval(interval);
100223
- };
100224
- }, [generatingProduct, uploadingProduct, productProcessStartTime]);
100223
+ }, [imagesGenerationLogs, generatingImages, uploadingImages, contentGenerationLogs, generating]);
100225
100224
  // Timer for images generation
100226
100225
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
100227
100226
  let interval = null;
@@ -100442,19 +100441,12 @@ function App() {
100442
100441
  return;
100443
100442
  if (response.ok) {
100444
100443
  const data = await response.json();
100445
- const bankaFiles = data.files.filter((f) => {
100446
- const name = f.name?.toLowerCase() || '';
100447
- return name.startsWith('banka') && (name.endsWith('.png') || name.endsWith('.jpg') || name.endsWith('.jpeg'));
100448
- });
100449
100444
  const hasProduct = data.files.some((f) => {
100450
100445
  const name = f.name?.toLowerCase() || '';
100451
100446
  return name === 'product.png' || name === 'product.jpg';
100452
100447
  });
100453
100448
  if (!cancelled) {
100454
- setFolderFilesInfo({
100455
- bankaCount: bankaFiles.length,
100456
- hasProduct
100457
- });
100449
+ setFolderFilesInfo({ hasProduct });
100458
100450
  // If product not found, poll every 10 seconds until found
100459
100451
  if (!hasProduct) {
100460
100452
  pollTimeoutId = setTimeout(() => {
@@ -100679,6 +100671,10 @@ function App() {
100679
100671
  setSelectedValidationModel(modelId);
100680
100672
  localStorage.setItem('selectedValidationModel', modelId);
100681
100673
  };
100674
+ const handleValidationDisabledChange = (checked) => {
100675
+ setValidationDisabled(checked);
100676
+ localStorage.setItem('validationDisabled', String(checked));
100677
+ };
100682
100678
  // Fetch OpenRouter account balance and key limit
100683
100679
  const fetchOpenRouterBalance = async (apiKey) => {
100684
100680
  const keyToUse = apiKey ?? openaiApiKey;
@@ -101537,7 +101533,7 @@ function App() {
101537
101533
  setTexts(['']);
101538
101534
  setPairTranslations({});
101539
101535
  // Read pairs count from settings (3–10, default 3)
101540
- const pairsCountInit = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_52__.getPairsCount)();
101536
+ const pairsCountInit = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.getPairsCount)();
101541
101537
  // Initialize placeholders
101542
101538
  const initialTitles = Array.from({ length: pairsCountInit }, (_, index) => ({
101543
101539
  index: index + 1,
@@ -101566,7 +101562,7 @@ function App() {
101566
101562
  }
101567
101563
  // Generate all pairs (title + text) in a single request
101568
101564
  addLog(formatLogMessage('log', '📋 Generating title+text pairs in a single request...'));
101569
- const selectedIndices = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_52__.getSelectedPairApproaches)();
101565
+ const selectedIndices = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.getSelectedPairApproaches)();
101570
101566
  setLastUsedApproachIndices(selectedIndices);
101571
101567
  const pairsCount = selectedIndices.length;
101572
101568
  addLog(formatLogMessage('log', `⚙️ Generating ${pairsCount} pairs (approaches: ${selectedIndices.join(', ')})`));
@@ -101661,7 +101657,7 @@ function App() {
101661
101657
  setGenerating(false);
101662
101658
  }
101663
101659
  };
101664
- const generateImageWithDALLE = async (prompt, referenceImageUrl) => {
101660
+ const generateImageWithDALLE = async (prompt, referenceImageUrl, aspectRatioOverride) => {
101665
101661
  if (!openaiApiKey) {
101666
101662
  throw new Error('OpenRouter API key is not set');
101667
101663
  }
@@ -101719,6 +101715,8 @@ function App() {
101719
101715
  // For image generation models, content must be an array with text and image_url objects
101720
101716
  const finalContent = referenceImageApiUrls.length > 0 ? content : prompt;
101721
101717
  // Build image_config based on the model family.
101718
+ // When aspectRatioOverride is provided (e.g. in 'both' mode), use it. Otherwise use imageAspectRatio.
101719
+ const effectiveRatio = aspectRatioOverride ?? (imageAspectRatio === 'both' ? '1:1' : imageAspectRatio);
101722
101720
  // openai/gpt-5-image* models use OpenAI's gpt-image-1 underneath, which only supports
101723
101721
  // three discrete sizes: 1024x1024, 1024x1536, 1536x1024.
101724
101722
  // The generic OpenRouter `aspect_ratio` param is ignored for these models.
@@ -101732,7 +101730,7 @@ function App() {
101732
101730
  '1:1': '1024x1024',
101733
101731
  '2:3': '1024x1536',
101734
101732
  };
101735
- const gptSize = gptSizeMap[imageAspectRatio] || '1024x1024';
101733
+ const gptSize = gptSizeMap[effectiveRatio] || '1024x1024';
101736
101734
  imageConfig = { size: gptSize };
101737
101735
  imageAspectRatioParam = gptSize;
101738
101736
  }
@@ -101742,7 +101740,7 @@ function App() {
101742
101740
  '1:1': '1:1',
101743
101741
  '2:3': '2:3',
101744
101742
  };
101745
- imageAspectRatioParam = aspectRatioMap[imageAspectRatio] || '1:1';
101743
+ imageAspectRatioParam = aspectRatioMap[effectiveRatio] || '1:1';
101746
101744
  imageConfig = { aspect_ratio: imageAspectRatioParam };
101747
101745
  }
101748
101746
  const requestBody = {
@@ -101921,7 +101919,7 @@ function App() {
101921
101919
  if (imageUrl) {
101922
101920
  const img = new Image();
101923
101921
  img.onload = () => {
101924
- logToTerminal('log', `📐 Actual image dimensions: ${img.naturalWidth}×${img.naturalHeight} (requested: ${imageAspectRatio}, sent image_config: ${JSON.stringify(imageConfig)})`);
101922
+ logToTerminal('log', `📐 Actual image dimensions: ${img.naturalWidth}×${img.naturalHeight} (requested: ${effectiveRatio}, sent image_config: ${JSON.stringify(imageConfig)})`);
101925
101923
  };
101926
101924
  img.src = imageUrl;
101927
101925
  }
@@ -102197,362 +102195,52 @@ function App() {
102197
102195
  errors: finalErrors
102198
102196
  };
102199
102197
  };
102200
- const generateProductFromBanka = async (bankaImageUrls, additionalPrompt = '', addLog, currentProductImageUrl) => {
102201
- if (!openaiApiKey) {
102202
- const errorMsg = 'OpenRouter API key is not set';
102203
- logToTerminal('error', errorMsg);
102204
- if (addLog)
102205
- addLog(formatLogMessage('error', errorMsg));
102206
- throw new Error(errorMsg);
102207
- }
102208
- const logMsg = (level, ...args) => {
102209
- logToTerminal(level, ...args);
102210
- if (addLog)
102211
- addLog(formatLogMessage(level, ...args));
102212
- };
102213
- const isRegeneration = !!currentProductImageUrl;
102214
- logMsg('log', `=== Starting product image ${isRegeneration ? 'regeneration' : 'generation'} from source images ===`);
102215
- logMsg('log', '📸 Source images count:', bankaImageUrls.length);
102216
- if (isRegeneration) {
102217
- logMsg('log', '🔄 Regenerating with current product image as reference');
102218
- }
102219
- // Ensure we have a valid access token before proceeding
102220
- const validToken = await getValidAccessToken();
102221
- if (!validToken) {
102222
- const errorMsg = 'Not logged in to Google Drive or token expired';
102223
- logMsg('error', errorMsg);
102224
- throw new Error(errorMsg);
102225
- }
102226
- logMsg('log', '✅ Valid access token confirmed');
102227
- // Base prompt for product generation
102228
- let basePrompt = `Прикрепил фото упаковки продукта. Сгенерируй пожалуйста её упрощенное изображение для креативов для рекламы в Facebook.
102229
- ВАЖНО: Сохрани точно такую же форму упаковки, как на исходном фото (если это упаковка таблеток - оставь упаковку таблеток, если банка - оставь банку, если тюбик - оставь тюбик).
102230
- Убери мелкий текст. Нужна только сама упаковка продукта, не коробка. Оставь логотип, название, примерно тот же тон.`;
102231
- // If regenerating, update prompt to mention current image
102232
- if (isRegeneration) {
102233
- basePrompt = `${basePrompt}
102234
-
102235
- 🚨 ПЕРЕГЕНЕРАЦИЯ: Прикреплены два типа референсов:
102236
- 1. Исходные фото упаковки продукта (используй их как основу для формы и деталей)
102237
- 2. Текущий вариант сгенерированного продукта (НЕПРАВИЛЬНЫЙ, требует ОБЯЗАТЕЛЬНЫХ изменений согласно требованиям ниже)
102238
-
102239
- ═══════════════════════════════════════════════════════════════
102240
- 🔥 ОБЯЗАТЕЛЬНЫЕ ТРЕБОВАНИЯ ПОЛЬЗОВАТЕЛЯ (ПРИМЕНИ ВСЕ БЕЗ ИСКЛЮЧЕНИЯ):
102241
- ═══════════════════════════════════════════════════════════════`;
102242
- }
102243
- const fullPrompt = additionalPrompt.trim()
102244
- ? `${basePrompt}${isRegeneration ? '' : '\nДополнительные требования: '}${additionalPrompt.split('\n').filter(line => line.trim()).map(line => `• ${line.trim()}`).join('\n')}${isRegeneration ? '\n\n⚠️ ВАЖНО: ОБЯЗАТЕЛЬНО примени ВСЕ требования выше в новом варианте изображения. НЕ копируй предыдущий вариант - создай НОВОЕ изображение с учетом всех требований.' : ''}`
102245
- : basePrompt;
102246
- logMsg('log', '📝 Prompt:', fullPrompt);
102247
- // Convert Drive URLs to direct image URLs for API
102248
- // For Google Drive, convert view URLs to direct download URLs
102249
- // Note: Token is refreshed above to ensure files are accessible
102250
- const convertImageUrl = (url) => {
102251
- const fileIdMatch = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);
102252
- if (fileIdMatch) {
102253
- const fileId = fileIdMatch[1];
102254
- return `https://drive.google.com/uc?export=view&id=${fileId}`;
102255
- }
102256
- return url;
102257
- };
102258
- const sourceImageUrls = bankaImageUrls.map(convertImageUrl);
102259
- // Build image URLs array: current product image first (if regenerating), then source images
102260
- const imageUrls = [];
102261
- if (currentProductImageUrl) {
102262
- imageUrls.push(convertImageUrl(currentProductImageUrl));
102263
- }
102264
- imageUrls.push(...sourceImageUrls);
102265
- logMsg('log', '🖼️ Total reference images:', imageUrls.length, isRegeneration ? '(current product + source images)' : '(source images only)');
102266
- const requestBody = {
102267
- model: _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.imageGeneration,
102268
- messages: [
102269
- {
102270
- role: 'user',
102271
- content: [
102272
- { type: 'text', text: fullPrompt },
102273
- ...imageUrls.map(url => ({ type: 'image_url', image_url: { url } }))
102274
- ]
102275
- }
102276
- ],
102277
- modalities: ['image', 'text'],
102278
- };
102279
- logMsg('log', '📦 Request body prepared with', imageUrls.length, 'source images');
102280
- logMsg('log', '🚀 Sending request to OpenRouter API...');
102281
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
102282
- method: 'POST',
102283
- headers: {
102284
- 'Content-Type': 'application/json',
102285
- 'Authorization': `Bearer ${openaiApiKey}`,
102286
- 'HTTP-Referer': window.location.origin || 'https://docs-combiner.app',
102287
- 'X-Title': 'Docs Combiner'
102288
- },
102289
- body: JSON.stringify(requestBody)
102290
- });
102291
- const responseText = await response.text();
102292
- logMsg('log', '📊 Response status:', response.status, response.statusText);
102293
- if (!response.ok) {
102294
- let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
102295
- try {
102296
- const error = JSON.parse(responseText);
102297
- errorMessage = error.error?.message || error.message || errorMessage;
102298
- logMsg('error', '🔍 Parsed error:', errorMessage);
102299
- }
102300
- catch (e) {
102301
- logMsg('error', '❌ Failed to parse error response as JSON');
102302
- }
102303
- throw new Error(errorMessage);
102304
- }
102305
- logMsg('log', '📥 Parsing response...');
102306
- let data;
102307
- try {
102308
- data = JSON.parse(responseText);
102309
- logMsg('log', '✅ JSON parsed successfully');
102310
- }
102311
- catch (e) {
102312
- logMsg('error', '❌ Failed to parse response as JSON');
102313
- throw new Error(`Invalid JSON response: ${responseText.substring(0, 200)}`);
102314
- }
102315
- if (!data.choices || !data.choices[0] || !data.choices[0].message) {
102316
- logMsg('error', '❌ Invalid response structure');
102317
- throw new Error(`Invalid response structure. Expected choices[0].message`);
102318
- }
102319
- const message = data.choices[0].message;
102320
- if (!message.images || !message.images[0] || !message.images[0].image_url) {
102321
- logMsg('error', '❌ No images in response');
102322
- throw new Error(`No images found in response`);
102198
+ const handleUploadProductFile = () => {
102199
+ if (!driveFolderUrl.trim()) {
102200
+ alert('Укажите папку Google Drive');
102201
+ return;
102323
102202
  }
102324
- const imageUrl = message.images[0].image_url.url;
102325
- logMsg('log', '✅ Product image generated successfully');
102326
- logMsg('log', '=== End product image generation ===');
102327
- // Update balance after successful API call
102328
- fetchOpenRouterBalance();
102329
- return imageUrl;
102203
+ productFileInputRef.current?.click();
102330
102204
  };
102331
- const handleGenerateProduct = async () => {
102205
+ const handleProductFileSelected = async (e) => {
102206
+ const file = e.target.files?.[0];
102207
+ if (!file)
102208
+ return;
102209
+ e.target.value = '';
102332
102210
  const validToken = await getValidAccessToken();
102333
102211
  if (!validToken) {
102334
- alert('Please log in with Google first');
102335
- return;
102336
- }
102337
- if (!driveFolderUrl.trim()) {
102338
- alert('Please fill in Google Drive Folder URL');
102212
+ alert('Войдите в Google Drive');
102339
102213
  return;
102340
102214
  }
102341
- try {
102342
- const folderId = extractFolderId(driveFolderUrl);
102343
- if (!folderId) {
102344
- throw new Error('Invalid Google Drive Folder URL');
102345
- }
102346
- // Check if product.png exists in the folder
102347
- logToTerminal('log', '🔍 Checking for product.png in folder...');
102348
- const existingProductImage = await fetchProductImage(folderId);
102349
- if (existingProductImage) {
102350
- // Product.png exists - open regeneration modal without generating
102351
- logToTerminal('log', '✅ product.png found in folder, opening regeneration modal');
102352
- // Load product image as blob and convert to data URL for display
102353
- if (!productGeneratedImage) {
102354
- try {
102355
- logToTerminal('log', '📥 Downloading product.png for display...');
102356
- let token = await getValidAccessToken();
102357
- if (!token) {
102358
- throw new Error('Not logged in to Google Drive');
102359
- }
102360
- let imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${existingProductImage.id}?alt=media`, {
102361
- headers: {
102362
- 'Authorization': `Bearer ${token}`
102363
- }
102364
- });
102365
- // Handle 401 - try to refresh token and retry
102366
- if (imageResponse.status === 401 && refreshToken) {
102367
- logToTerminal('log', '🔄 Got 401, refreshing token...');
102368
- const newToken = await refreshAccessToken(refreshToken);
102369
- if (newToken) {
102370
- token = newToken;
102371
- logToTerminal('log', '✅ Token refreshed, retrying download...');
102372
- imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${existingProductImage.id}?alt=media`, {
102373
- headers: {
102374
- 'Authorization': `Bearer ${newToken}`
102375
- }
102376
- });
102377
- }
102378
- }
102379
- if (!imageResponse.ok) {
102380
- throw new Error(`Failed to download product image: ${imageResponse.status} ${imageResponse.statusText}`);
102381
- }
102382
- const blob = await imageResponse.blob();
102383
- // Convert blob to data URL using Promise
102384
- const dataUrl = await new Promise((resolve, reject) => {
102385
- const reader = new FileReader();
102386
- reader.onloadend = () => resolve(reader.result);
102387
- reader.onerror = reject;
102388
- reader.readAsDataURL(blob);
102389
- });
102390
- setProductGeneratedImage(dataUrl);
102391
- logToTerminal('log', '✅ Product image loaded and displayed');
102392
- }
102393
- catch (err) {
102394
- logToTerminal('error', '❌ Failed to load product image:', err.message);
102395
- // Fallback to direct URL if blob download fails
102396
- const productImageUrl = `https://drive.google.com/uc?export=view&id=${existingProductImage.id}`;
102397
- setProductGeneratedImage(productImageUrl);
102398
- }
102399
- }
102400
- // If we have source images, just open modal
102401
- if (productSourceImages.length > 0) {
102402
- logToTerminal('log', '✅ Source images available, opening modal');
102403
- setProductModalOpen(true);
102404
- return;
102405
- }
102406
- // If no source images, load them without generating
102407
- logToTerminal('log', '📥 Loading source images without generating');
102408
- setProductModalOpen(true);
102409
- setProductGenerationLogs([]);
102410
- const addLog = (msg) => {
102411
- setProductGenerationLogs(prev => [...prev, msg]);
102412
- };
102413
- try {
102414
- addLog(formatLogMessage('log', '🔍 Загрузка исходных изображений...'));
102415
- logToTerminal('log', '🔍 Loading source images...');
102416
- const bankaImages = await fetchBankaImages(folderId);
102417
- if (bankaImages.length === 0) {
102418
- addLog(formatLogMessage('warn', '⚠️ Исходные изображения не найдены, но есть product.png'));
102419
- logToTerminal('warn', '⚠️ No source images found, but product.png exists');
102420
- }
102421
- else {
102422
- addLog(formatLogMessage('log', `✅ Загружено ${bankaImages.length} исходных изображений`));
102423
- logToTerminal('log', `✅ Loaded ${bankaImages.length} source image(s)`);
102424
- setProductSourceImages(bankaImages.map(img => img.url));
102425
- }
102426
- }
102427
- catch (err) {
102428
- const errorMsg = 'Error loading source images: ' + err.message;
102429
- addLog(formatLogMessage('error', errorMsg));
102430
- logToTerminal('error', '❌ Error loading source images:', err);
102431
- // Don't alert, just log - user can still regenerate with existing product image
102432
- }
102433
- return; // IMPORTANT: Return here to prevent generation
102434
- }
102435
- // Product.png doesn't exist - proceed with full generation
102436
- logToTerminal('log', '🎨 product.png not found, starting full generation');
102437
- }
102438
- catch (err) {
102439
- // If check fails, proceed with generation (better to try than to block)
102440
- logToTerminal('warn', '⚠️ Failed to check for product.png, proceeding with generation:', err.message);
102441
- }
102442
- // Full generation flow - only when product image doesn't exist
102443
- setGeneratingProduct(true);
102444
- setProductGenerationLogs([]);
102445
- setProductProcessStartTime(Date.now());
102446
- setProductModalOpen(true);
102447
- const addLog = (msg) => {
102448
- setProductGenerationLogs(prev => [...prev, msg]);
102449
- };
102450
- try {
102451
- const folderId = extractFolderId(driveFolderUrl);
102452
- if (!folderId) {
102453
- throw new Error('Invalid Google Drive Folder URL');
102454
- }
102455
- addLog(formatLogMessage('log', '🔍 Searching for source images (banka*.png/jpg)...'));
102456
- logToTerminal('log', '🔍 Searching for banka images...');
102457
- const bankaImages = await fetchBankaImages(folderId);
102458
- if (bankaImages.length === 0) {
102459
- addLog(formatLogMessage('error', 'No banka*.png/jpg files found in the folder'));
102460
- alert('No banka*.png/jpg files found in the folder');
102461
- return;
102462
- }
102463
- addLog(formatLogMessage('log', `✅ Found ${bankaImages.length} source image(s)`));
102464
- logToTerminal('log', `✅ Found ${bankaImages.length} banka image(s)`);
102465
- setProductSourceImages(bankaImages.map(img => img.url));
102466
- // Generate product image
102467
- addLog(formatLogMessage('log', '🎨 Generating product image...'));
102468
- logToTerminal('log', '🎨 Generating product image...');
102469
- const generatedImageUrl = await generateProductFromBanka(bankaImages.map(img => img.url), '', addLog);
102470
- setProductGeneratedImage(generatedImageUrl);
102471
- }
102472
- catch (err) {
102473
- const errorMsg = 'Error generating product: ' + err.message;
102474
- addLog(formatLogMessage('error', errorMsg));
102475
- logToTerminal('error', '❌ Error generating product:', err);
102476
- alert(errorMsg);
102477
- }
102478
- finally {
102479
- setGeneratingProduct(false);
102480
- }
102481
- };
102482
- const handleRegenerateProduct = async () => {
102483
- if (!productSourceImages.length || !productGeneratedImage)
102215
+ const folderId = extractFolderId(driveFolderUrl);
102216
+ if (!folderId) {
102217
+ alert('Некорректная ссылка на папку Google Drive');
102484
102218
  return;
102485
- setGeneratingProduct(true);
102486
- setProductGenerationLogs([]);
102487
- if (!productProcessStartTime) {
102488
- setProductProcessStartTime(Date.now());
102489
102219
  }
102490
- const addLog = (msg) => {
102491
- setProductGenerationLogs(prev => [...prev, msg]);
102492
- };
102493
- try {
102494
- if (productRegeneratePrompt.trim()) {
102495
- addLog(formatLogMessage('log', '🔄 Переделка изображения продукта с пользовательскими требованиями:'));
102496
- productRegeneratePrompt.split('\n').filter(line => line.trim()).forEach((line, idx) => {
102497
- addLog(formatLogMessage('log', ` ${idx + 1}. ${line.trim()}`));
102498
- });
102499
- }
102500
- else {
102501
- addLog(formatLogMessage('log', '🔄 Переделка изображения продукта (без дополнительных требований)'));
102502
- }
102503
- logToTerminal('log', '🔄 Regenerating product image with additional prompt:', productRegeneratePrompt);
102504
- // Pass current product image as reference for regeneration
102505
- const generatedImageUrl = await generateProductFromBanka(productSourceImages, productRegeneratePrompt, addLog, productGeneratedImage || undefined);
102506
- setProductGeneratedImage(generatedImageUrl);
102507
- setProductRegeneratePrompt(''); // Clear prompt after regeneration
102508
- }
102509
- catch (err) {
102510
- const errorMsg = 'Error regenerating product: ' + err.message;
102511
- addLog(formatLogMessage('error', errorMsg));
102512
- logToTerminal('error', '❌ Error regenerating product:', err);
102513
- alert(errorMsg);
102514
- }
102515
- finally {
102516
- setGeneratingProduct(false);
102517
- }
102518
- };
102519
- const handleUploadProduct = async () => {
102520
- if (!productGeneratedImage || !driveFolderUrl.trim()) {
102521
- alert('No product image to upload');
102220
+ const ext = file.name.toLowerCase().split('.').pop();
102221
+ if (ext !== 'png' && ext !== 'jpg' && ext !== 'jpeg') {
102222
+ alert('Выберите изображение PNG или JPG');
102522
102223
  return;
102523
102224
  }
102524
102225
  setUploadingProduct(true);
102525
- setProductGenerationLogs([]);
102526
- if (!productProcessStartTime) {
102527
- setProductProcessStartTime(Date.now());
102528
- }
102529
- const addLog = (msg) => {
102530
- setProductGenerationLogs(prev => [...prev, msg]);
102531
- };
102226
+ const addLog = () => { };
102532
102227
  try {
102533
- const folderId = extractFolderId(driveFolderUrl);
102534
- if (!folderId) {
102535
- throw new Error('Invalid Google Drive Folder URL');
102536
- }
102537
- addLog(formatLogMessage('log', '📤 Uploading product.png...'));
102538
- logToTerminal('log', '📤 Uploading product.png...');
102539
- await uploadImageToDrive(productGeneratedImage, 'product.png', folderId, addLog);
102540
- addLog(formatLogMessage('log', ' product.png uploaded successfully'));
102541
- logToTerminal('log', '✅ product.png uploaded successfully');
102542
- // Update folder files info
102543
- if (folderFilesInfo) {
102544
- setFolderFilesInfo({
102545
- ...folderFilesInfo,
102546
- hasProduct: true
102547
- });
102548
- }
102549
- alert('product.png uploaded successfully!');
102550
- setProductModalOpen(false);
102228
+ const dataUrl = await new Promise((resolve, reject) => {
102229
+ const reader = new FileReader();
102230
+ reader.onloadend = () => resolve(reader.result);
102231
+ reader.onerror = reject;
102232
+ reader.readAsDataURL(file);
102233
+ });
102234
+ const filename = ext === 'png' ? 'product.png' : 'product.jpg';
102235
+ logToTerminal('log', '📤 Загрузка product.png/jpg...');
102236
+ await uploadImageToDrive(dataUrl, filename, folderId, addLog);
102237
+ logToTerminal('log', '✅ product.png/jpg загружен');
102238
+ setFolderFilesInfo({ hasProduct: true });
102239
+ alert('product.png/jpg успешно загружен в папку!');
102551
102240
  }
102552
102241
  catch (err) {
102553
- const errorMsg = 'Error uploading product: ' + err.message;
102554
- addLog(formatLogMessage('error', errorMsg));
102555
- logToTerminal('error', '❌ Error uploading product:', err);
102242
+ const errorMsg = 'Ошибка загрузки: ' + err.message;
102243
+ logToTerminal('error', '❌', errorMsg);
102556
102244
  alert(errorMsg);
102557
102245
  }
102558
102246
  finally {
@@ -102796,7 +102484,7 @@ function App() {
102796
102484
  addLog(formatLogMessage(level, ...args));
102797
102485
  };
102798
102486
  logMsg('log', '📦 Creating ZIP archive with HTML and product image...');
102799
- const zip = new (jszip__WEBPACK_IMPORTED_MODULE_54___default())();
102487
+ const zip = new (jszip__WEBPACK_IMPORTED_MODULE_51___default())();
102800
102488
  zip.file('index.html', htmlContent);
102801
102489
  zip.file('product.png', productImageBlob);
102802
102490
  logMsg('log', '✅ Files added to archive: index.html, product.png');
@@ -103115,18 +102803,41 @@ function App() {
103115
102803
  throw new Error(errorMsg);
103116
102804
  }
103117
102805
  addLog(formatLogMessage('log', '✅ product.png found'));
103118
- // Base prompt structure
103119
- // NOTE: This generation prompt is intentionally STRICTER than the image validator (`validateCreativeImage`).
103120
- // Keep the prompt strict even if validator allows some "acceptable" deviations (e.g. 2-line headline, benefit headline, CTA position, discount emphasis).
103121
- // Use original currency symbol if available, otherwise use currency code
103122
- const basePromptStructure = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getImageGenerationBasePrompt)(generateGeo, generatePrice, currencyForPrompt, undefined, imageAspectRatio);
103123
- // Generate images with different approaches (one per approach)
102806
+ // Generate images with different approaches (количество по каждому подходу)
103124
102807
  const approaches = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getCreoApproaches)();
102808
+ if (approaches.length === 0) {
102809
+ addLog(formatLogMessage('error', '❌ Укажите количество изображений (хотя бы 1) в настройках подходов'));
102810
+ alert('Укажите количество изображений (1–4) хотя бы для одного подхода в настройках');
102811
+ setGeneratingImages(false);
102812
+ return;
102813
+ }
102814
+ // При "both" — на каждый подход генерируем и 1:1, и 2:3
102815
+ const useBoth = imageAspectRatio === 'both';
102816
+ const tasks = useBoth
102817
+ ? approaches.flatMap(a => [
102818
+ { approach: a, ratio: '1:1' },
102819
+ { approach: a, ratio: '2:3' }
102820
+ ])
102821
+ : approaches.map(a => ({ approach: a, ratio: imageAspectRatio }));
102822
+ const additionalInfoLine = generateAdditionalInfo.trim()
102823
+ ? `\n📋 ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ О ПРОДУКТЕ: ${generateAdditionalInfo.trim()}`
102824
+ : '';
102825
+ const imagePrompts = tasks.map(t => {
102826
+ const basePromptStructure = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getImageGenerationBasePrompt)(generateGeo, generatePrice, currencyForPrompt, undefined, t.ratio);
102827
+ const bulletsLine = t.approach.noBullets
102828
+ ? t.approach.bulletsFocus
102829
+ : (() => {
102830
+ const [b1, b2, b3] = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.pickRandomBullets)(t.approach.name);
102831
+ return `ОБЯЗАТЕЛЬНЫЕ БУЛЛЕТЫ (используй именно эти 3, в указанном порядке; переведи на язык ${generateGeo}): 1) ${b1} 2) ${b2} 3) ${b3}`;
102832
+ })();
102833
+ return `🏷️ ПРОДУКТ: ${generateProduct}${additionalInfoLine}\n\n${basePromptStructure}🏷️ ПРОДУКТ (повторение для ясности): ${generateProduct}${additionalInfoLine}\n🎯 ПОДХОД: ${t.approach.name}\n\n${t.approach.prompt}\n\n${t.approach.headlineAngle}\n\n${bulletsLine}\n\nТекст — строго следуй правилам этого подхода (HOOK обязателен; буллиты добавляй только если они разрешены для подхода).`;
102834
+ });
103125
102835
  // Initialize placeholders for all images
103126
- const initialPlaceholders = approaches.map((approach, index) => ({
102836
+ const initialPlaceholders = tasks.map((t, index) => ({
103127
102837
  index: index + 1,
103128
102838
  imageUrl: undefined,
103129
- approach: approach.name,
102839
+ approach: t.approach.name + (useBoth ? ` (${t.ratio})` : ''),
102840
+ aspectRatio: t.ratio,
103130
102841
  uploaded: false,
103131
102842
  checking: false,
103132
102843
  checkStatus: 'pending',
@@ -103140,13 +102851,9 @@ function App() {
103140
102851
  generating: false // In sequential mode, images start queued (not generating)
103141
102852
  }));
103142
102853
  setGeneratedImagesData(initialPlaceholders);
103143
- const additionalInfoLine = generateAdditionalInfo.trim()
103144
- ? `\n📋 ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ О ПРОДУКТЕ: ${generateAdditionalInfo.trim()}`
103145
- : '';
103146
- const imagePrompts = approaches.map(approach => `🏷️ ПРОДУКТ: ${generateProduct}${additionalInfoLine}\n\n${basePromptStructure}🏷️ ПРОДУКТ (повторение для ясности): ${generateProduct}${additionalInfoLine}\n🎯 ПОДХОД: ${approach.name}\n\n${approach.prompt}\n\n${approach.headlineAngle}\n\n${approach.bulletsFocus}\n\nТекст — строго следуй правилам этого подхода (HOOK обязателен; буллиты добавляй только если они разрешены для подхода).`);
103147
- addLog(formatLogMessage('log', `📝 Generated prompts for ${approaches.length} approaches`));
103148
- const maxParallel = 3;
103149
- addLog(formatLogMessage('log', `🚀 Generating ${approaches.length} images in parallel (up to ${maxParallel} at a time)...`));
102854
+ addLog(formatLogMessage('log', `📝 Generated prompts for ${tasks.length} images${useBoth ? ' (1:1 + 2:3 на каждый подход)' : ''}`));
102855
+ const maxParallel = 100; // like infinity
102856
+ addLog(formatLogMessage('log', `🚀 Generating ${tasks.length} images in parallel (up to ${maxParallel} at a time)...`));
103150
102857
  const generationStartTime = Date.now();
103151
102858
  const resultsMap = new Map();
103152
102859
  // Ensure each slot has prompt+product reference from the start (needed for regeneration even on failures)
@@ -103157,9 +102864,11 @@ function App() {
103157
102864
  })));
103158
102865
  const runOne = async (i, isRetry) => {
103159
102866
  const imageIndex = i + 1;
103160
- const approachName = approaches[i]?.name || 'Unknown';
102867
+ const task = tasks[i];
102868
+ const approachName = task?.approach.name || 'Unknown';
103161
102869
  const prompt = imagePrompts[i];
103162
- addLog(formatLogMessage('log', `${isRetry ? '🔄' : '🎨'} Генерация изображения ${imageIndex}/${approaches.length} (${approachName})...`));
102870
+ const ratio = task?.ratio || '1:1';
102871
+ addLog(formatLogMessage('log', `${isRetry ? '🔄' : '🎨'} Генерация изображения ${imageIndex}/${tasks.length} (${approachName} ${ratio})...`));
103163
102872
  // Reset slot state for this run
103164
102873
  setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
103165
102874
  ? {
@@ -103177,7 +102886,7 @@ function App() {
103177
102886
  }
103178
102887
  : img));
103179
102888
  try {
103180
- const imageUrl = await generateImageWithDALLE(prompt, productImage.url);
102889
+ const imageUrl = await generateImageWithDALLE(prompt, productImage.url, ratio);
103181
102890
  if (!imageUrl || typeof imageUrl !== 'string' || imageUrl.trim() === '') {
103182
102891
  throw new Error('Generated image URL is empty or invalid');
103183
102892
  }
@@ -103194,59 +102903,46 @@ function App() {
103194
102903
  productImageUrl: productImage.url
103195
102904
  }
103196
102905
  : img));
103197
- addLog(formatLogMessage('log', `✅ Изображение ${imageIndex}/${approaches.length} сгенерировано`));
103198
- // Validate right after generation
103199
- try {
103200
- const validationResult = await validateCreativeImage(imageUrl, generateProduct, generateGeo, addLog, approachName);
103201
- if (!validationResult)
103202
- throw new Error('No validation result');
103203
- setGeneratedImagesData(prev => {
103204
- const updated = prev.map(img => img.index === imageIndex
103205
- ? {
103206
- ...img,
103207
- checking: false,
103208
- checkStatus: validationResult.status,
103209
- checkResult: validationResult.result,
103210
- checkErrors: validationResult.errors,
103211
- customRegeneratePrompt: validationResult.errors.length > 0
103212
- ? validationResult.errors.map(err => err.replace(/^ОШИБКА:\s*/i, '')).join('\n')
103213
- : '',
103214
- originalPrompt: img.originalPrompt || prompt,
103215
- productImageUrl: img.productImageUrl || productImage.url
103216
- }
103217
- : img);
103218
- // Save single image to Google Drive after validation
103219
- if (driveFolderUrl) {
103220
- const folderId = extractFolderId(driveFolderUrl);
103221
- if (folderId) {
103222
- const updatedImage = updated.find(img => img.index === imageIndex);
103223
- if (updatedImage) {
103224
- saveSingleImageToDrive(folderId, updatedImage).catch((err) => {
103225
- console.log(`Failed to save image ${imageIndex} after validation:`, err);
103226
- });
103227
- }
103228
- }
102906
+ addLog(formatLogMessage('log', `✅ Изображение ${imageIndex}/${tasks.length} сгенерировано`));
102907
+ // Validate right after generation (skip if validation disabled)
102908
+ const validationResult = validationDisabled
102909
+ ? { status: 'ok', result: 'Проверка отключена', errors: [], checkFailed: false }
102910
+ : await (async () => {
102911
+ try {
102912
+ const res = await validateCreativeImage(imageUrl, generateProduct, generateGeo, addLog, approachName);
102913
+ if (!res)
102914
+ throw new Error('No validation result');
102915
+ return { ...res, checkFailed: false };
103229
102916
  }
103230
- return updated;
103231
- });
103232
- }
103233
- catch (validationErr) {
103234
- const msg = validationErr?.message || String(validationErr);
103235
- addLog(formatLogMessage('error', `❌ Ошибка проверки изображения ${imageIndex}: ${msg}`));
103236
- setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
102917
+ catch (validationErr) {
102918
+ const msg = validationErr?.message || String(validationErr);
102919
+ addLog(formatLogMessage('error', `❌ Ошибка проверки изображения ${imageIndex}: ${msg}`));
102920
+ return {
102921
+ status: 'needs_rebuild',
102922
+ result: `Ошибка проверки: ${msg}`,
102923
+ errors: [msg],
102924
+ checkFailed: true
102925
+ };
102926
+ }
102927
+ })();
102928
+ setGeneratedImagesData(prev => {
102929
+ const updated = prev.map(img => img.index === imageIndex
103237
102930
  ? {
103238
102931
  ...img,
103239
102932
  checking: false,
103240
- checkStatus: 'needs_rebuild',
103241
- checkFailed: true,
103242
- checkResult: `Ошибка проверки: ${msg}`,
103243
- checkErrors: [msg],
103244
- customRegeneratePrompt: '',
102933
+ checkFailed: validationResult.checkFailed ?? false,
102934
+ checkStatus: validationResult.status,
102935
+ checkResult: validationResult.result,
102936
+ checkErrors: validationResult.errors,
102937
+ customRegeneratePrompt: validationResult.errors.length > 0
102938
+ ? validationResult.errors.map(err => err.replace(/^ОШИБКА:\s*/i, '')).join('\n')
102939
+ : '',
103245
102940
  originalPrompt: img.originalPrompt || prompt,
103246
102941
  productImageUrl: img.productImageUrl || productImage.url
103247
102942
  }
103248
- : img));
103249
- }
102943
+ : img);
102944
+ return updated;
102945
+ });
103250
102946
  return {
103251
102947
  index: imageIndex,
103252
102948
  imageUrl,
@@ -103258,7 +102954,7 @@ function App() {
103258
102954
  }
103259
102955
  catch (err) {
103260
102956
  const errorMessage = err?.message || String(err);
103261
- addLog(formatLogMessage('error', `❌ Не удалось сгенерировать изображение ${imageIndex}/${approaches.length}: ${errorMessage}`));
102957
+ addLog(formatLogMessage('error', `❌ Не удалось сгенерировать изображение ${imageIndex}/${tasks.length}: ${errorMessage}`));
103262
102958
  setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
103263
102959
  ? {
103264
102960
  ...img,
@@ -103554,7 +103250,7 @@ ${imageData.originalPrompt}
103554
103250
  });
103555
103251
  }
103556
103252
  addLog(formatLogMessage('log', `📤 Первые 300 символов промпта: ${improvedPrompt.substring(0, 300)}...`));
103557
- const newImageUrl = await generateImageWithDALLE(improvedPrompt, referenceImages);
103253
+ const newImageUrl = await generateImageWithDALLE(improvedPrompt, referenceImages, imageData.aspectRatio ?? '1:1');
103558
103254
  if (!newImageUrl)
103559
103255
  throw new Error('No image URL returned');
103560
103256
  addLog(formatLogMessage('log', `✅ Изображение ${imageData.index} переделано успешно`));
@@ -103582,9 +103278,15 @@ ${imageData.originalPrompt}
103582
103278
  failed: false // Mark as successful
103583
103279
  }
103584
103280
  : img));
103585
- // Run validation on the new image
103586
- addLog(formatLogMessage('log', `🔍 Проверка переделанного изображения ${imageData.index}...`));
103587
- const validationResult = await validateCreativeImage(newImageUrl, generateProduct, generateGeo, addLog, imageData.approach);
103281
+ // Run validation on the new image (skip if disabled)
103282
+ let validationResult;
103283
+ if (validationDisabled) {
103284
+ validationResult = { status: 'ok', result: 'Проверка отключена', errors: [] };
103285
+ }
103286
+ else {
103287
+ addLog(formatLogMessage('log', `🔍 Проверка переделанного изображения ${imageData.index}...`));
103288
+ validationResult = await validateCreativeImage(newImageUrl, generateProduct, generateGeo, addLog, imageData.approach);
103289
+ }
103588
103290
  // Update with validation result
103589
103291
  setGeneratedImagesData(prev => {
103590
103292
  const updated = prev.map(img => img.index === imageData.index
@@ -103602,18 +103304,6 @@ ${imageData.originalPrompt}
103602
103304
  productImageUrl: img.productImageUrl || imageData.productImageUrl
103603
103305
  }
103604
103306
  : img);
103605
- // Save single image to Google Drive after regeneration
103606
- if (driveFolderUrl) {
103607
- const folderId = extractFolderId(driveFolderUrl);
103608
- if (folderId) {
103609
- const updatedImage = updated.find(img => img.index === imageData.index);
103610
- if (updatedImage) {
103611
- saveSingleImageToDrive(folderId, updatedImage).catch((err) => {
103612
- console.log(`Failed to save image ${imageData.index} after regeneration:`, err);
103613
- });
103614
- }
103615
- }
103616
- }
103617
103307
  return updated;
103618
103308
  });
103619
103309
  const statusEmoji = validationResult.status === 'ok' ? '✅' : '❌';
@@ -103679,7 +103369,7 @@ ${imageData.originalPrompt}
103679
103369
  ${imageData.originalPrompt}
103680
103370
 
103681
103371
  ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай, не вызывай tools. Сразу генерируй визуальный креатив как картинку.`;
103682
- const newImageUrl = await generateImageWithDALLE(freshPrompt, imageData.productImageUrl);
103372
+ const newImageUrl = await generateImageWithDALLE(freshPrompt, imageData.productImageUrl, imageData.aspectRatio ?? '1:1');
103683
103373
  addLog(formatLogMessage('log', `✅ Изображение ${imageData.index} (с нуля) сгенерировано`));
103684
103374
  // Update image data - always add timestamp for HTTP URLs to prevent browser caching
103685
103375
  let updatedImageUrl = newImageUrl;
@@ -103704,9 +103394,15 @@ ${imageData.originalPrompt}
103704
103394
  failed: false
103705
103395
  }
103706
103396
  : img));
103707
- // Validate new image
103708
- addLog(formatLogMessage('log', `🔍 Проверка изображения ${imageData.index} (с нуля)...`));
103709
- const validationResult = await validateCreativeImage(newImageUrl, generateProduct, generateGeo, addLog, imageData.approach);
103397
+ // Validate new image (skip if disabled)
103398
+ let validationResult;
103399
+ if (validationDisabled) {
103400
+ validationResult = { status: 'ok', result: 'Проверка отключена', errors: [] };
103401
+ }
103402
+ else {
103403
+ addLog(formatLogMessage('log', `🔍 Проверка изображения ${imageData.index} (с нуля)...`));
103404
+ validationResult = await validateCreativeImage(newImageUrl, generateProduct, generateGeo, addLog, imageData.approach);
103405
+ }
103710
103406
  setGeneratedImagesData(prev => {
103711
103407
  const updated = prev.map(img => img.index === imageData.index
103712
103408
  ? {
@@ -103722,18 +103418,6 @@ ${imageData.originalPrompt}
103722
103418
  productImageUrl: img.productImageUrl || imageData.productImageUrl
103723
103419
  }
103724
103420
  : img);
103725
- // Save single image to Google Drive after regeneration
103726
- if (driveFolderUrl) {
103727
- const folderId = extractFolderId(driveFolderUrl);
103728
- if (folderId) {
103729
- const updatedImage = updated.find(img => img.index === imageData.index);
103730
- if (updatedImage) {
103731
- saveSingleImageToDrive(folderId, updatedImage).catch((err) => {
103732
- console.log(`Failed to save image ${imageData.index} after regeneration:`, err);
103733
- });
103734
- }
103735
- }
103736
- }
103737
103421
  return updated;
103738
103422
  });
103739
103423
  }
@@ -103774,40 +103458,47 @@ ${imageData.originalPrompt}
103774
103458
  checkErrors: undefined,
103775
103459
  }
103776
103460
  : img));
103777
- addLog(formatLogMessage('log', `🔍 Повторная проверка изображения ${imageData.index}...`));
103778
- try {
103779
- const validationResult = await validateCreativeImage(imageData.imageUrl, generateProduct, generateGeo, addLog, imageData.approach);
103780
- setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
103781
- ? {
103782
- ...img,
103783
- checking: false,
103784
- checkFailed: false,
103785
- checkStatus: validationResult.status,
103786
- checkResult: validationResult.result,
103787
- checkErrors: validationResult.errors,
103788
- customRegeneratePrompt: validationResult.errors.length > 0
103789
- ? validationResult.errors.map(err => err.replace(/^ОШИБКА:\s*/i, '')).join('\n')
103790
- : '',
103791
- }
103792
- : img));
103793
- const statusEmoji = validationResult.status === 'ok' ? '✅' : '❌';
103794
- addLog(formatLogMessage('log', `${statusEmoji} Повторная проверка изображения ${imageData.index}: ${validationResult.status === 'ok' ? 'OK' : 'НУЖНА ПЕРЕСБОРКА'}`));
103461
+ let validationResult;
103462
+ if (validationDisabled) {
103463
+ validationResult = { status: 'ok', result: 'Проверка отключена', errors: [] };
103795
103464
  }
103796
- catch (err) {
103797
- const msg = err?.message || String(err);
103798
- addLog(formatLogMessage('error', `❌ Ошибка повторной проверки изображения ${imageData.index}: ${msg}`));
103799
- setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
103800
- ? {
103801
- ...img,
103802
- checking: false,
103803
- checkFailed: true,
103804
- checkStatus: 'needs_rebuild',
103805
- checkResult: `Ошибка проверки: ${msg}`,
103806
- checkErrors: [msg],
103807
- customRegeneratePrompt: '',
103808
- }
103809
- : img));
103465
+ else {
103466
+ addLog(formatLogMessage('log', `🔍 Повторная проверка изображения ${imageData.index}...`));
103467
+ try {
103468
+ validationResult = await validateCreativeImage(imageData.imageUrl, generateProduct, generateGeo, addLog, imageData.approach);
103469
+ }
103470
+ catch (err) {
103471
+ const msg = err?.message || String(err);
103472
+ addLog(formatLogMessage('error', `❌ Ошибка повторной проверки изображения ${imageData.index}: ${msg}`));
103473
+ setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
103474
+ ? {
103475
+ ...img,
103476
+ checking: false,
103477
+ checkFailed: true,
103478
+ checkStatus: 'needs_rebuild',
103479
+ checkResult: `Ошибка проверки: ${msg}`,
103480
+ checkErrors: [msg],
103481
+ customRegeneratePrompt: '',
103482
+ }
103483
+ : img));
103484
+ return;
103485
+ }
103810
103486
  }
103487
+ setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
103488
+ ? {
103489
+ ...img,
103490
+ checking: false,
103491
+ checkFailed: false,
103492
+ checkStatus: validationResult.status,
103493
+ checkResult: validationResult.result,
103494
+ checkErrors: validationResult.errors,
103495
+ customRegeneratePrompt: validationResult.errors.length > 0
103496
+ ? validationResult.errors.map(err => err.replace(/^ОШИБКА:\s*/i, '')).join('\n')
103497
+ : '',
103498
+ }
103499
+ : img));
103500
+ const statusEmoji = validationResult.status === 'ok' ? '✅' : '❌';
103501
+ addLog(formatLogMessage('log', `${statusEmoji} Повторная проверка изображения ${imageData.index}: ${validationResult.status === 'ok' ? 'OK' : 'НУЖНА ПЕРЕСБОРКА'}`));
103811
103502
  };
103812
103503
  const handleUploadImage = async (imageData, folderId) => {
103813
103504
  if (!imageData.imageUrl) {
@@ -103934,11 +103625,33 @@ ${imageData.originalPrompt}
103934
103625
  const handleLinkChange = (val) => {
103935
103626
  setLink(val);
103936
103627
  try {
103937
- new URL(val);
103628
+ const toTest = /^https?:\/\//i.test(val.trim()) ? val : 'https://' + val.trim();
103629
+ new URL(toTest);
103630
+ setLinkError('');
103631
+ }
103632
+ catch {
103633
+ setLinkError(val.trim() ? 'Invalid URL format' : '');
103634
+ }
103635
+ };
103636
+ const handleLinkBlur = () => {
103637
+ const trimmed = link.trim();
103638
+ if (!trimmed)
103639
+ return;
103640
+ let toParse = trimmed;
103641
+ if (!/^https?:\/\//i.test(trimmed)) {
103642
+ toParse = 'https://' + trimmed;
103643
+ }
103644
+ try {
103645
+ const parsed = new URL(toParse);
103646
+ let path = parsed.pathname || '/';
103647
+ if (!path.endsWith('/'))
103648
+ path += '/';
103649
+ const result = `https://${parsed.host}${path}${parsed.search}${parsed.hash}`;
103650
+ setLink(result);
103938
103651
  setLinkError('');
103939
103652
  }
103940
103653
  catch {
103941
- setLinkError('Invalid URL format');
103654
+ // invalid — leave as is
103942
103655
  }
103943
103656
  };
103944
103657
  const extractFolderId = (url) => {
@@ -103982,41 +103695,13 @@ ${imageData.originalPrompt}
103982
103695
  throw new Error(err.error?.message || 'Failed to fetch Drive files');
103983
103696
  }
103984
103697
  const data = await response.json();
103985
- // Filter out banka* files and product.png
103698
+ // Filter out product.png/jpg (used for product reference, not creative images)
103986
103699
  const filteredFiles = data.files.filter((f) => {
103987
103700
  const name = f.name?.toLowerCase() || '';
103988
- return !name.startsWith('banka') && name !== 'product.png' && name !== 'product.jpg';
103701
+ return name !== 'product.png' && name !== 'product.jpg';
103989
103702
  });
103990
103703
  return filteredFiles.map((f) => f.id ? `https://drive.google.com/file/d/${f.id}/view?usp=sharing` : '');
103991
103704
  };
103992
- const fetchBankaImages = async (folderId) => {
103993
- const validToken = await getValidAccessToken();
103994
- if (!validToken)
103995
- throw new Error('Not logged in');
103996
- const q = `'${folderId}' in parents and trashed = false and mimeType contains 'image/'`;
103997
- const fields = 'files(id, name)';
103998
- const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent(fields)}`;
103999
- const response = await fetch(url, {
104000
- headers: {
104001
- 'Authorization': `Bearer ${validToken}`
104002
- }
104003
- });
104004
- if (!response.ok) {
104005
- const err = await response.json();
104006
- throw new Error(err.error?.message || 'Failed to fetch Drive files');
104007
- }
104008
- const data = await response.json();
104009
- // Filter only banka* files (case insensitive)
104010
- const bankaFiles = data.files.filter((f) => {
104011
- const name = f.name?.toLowerCase() || '';
104012
- return name.startsWith('banka') && (name.endsWith('.png') || name.endsWith('.jpg') || name.endsWith('.jpeg'));
104013
- });
104014
- return bankaFiles.map((f) => ({
104015
- id: f.id,
104016
- name: f.name,
104017
- url: `https://drive.google.com/file/d/${f.id}/view?usp=sharing`
104018
- }));
104019
- };
104020
103705
  const fetchProductImage = async (folderId) => {
104021
103706
  const validToken = await getValidAccessToken();
104022
103707
  if (!validToken)
@@ -104393,11 +104078,7 @@ ${imageData.originalPrompt}
104393
104078
  logToTerminal('log', '[Load Content] Loaded generatePriceWithCurrency:', settings.generatePriceWithCurrency);
104394
104079
  }
104395
104080
  }
104396
- // Load brand and link
104397
- if (loadedData.brand !== undefined) {
104398
- setBrand(loadedData.brand || '');
104399
- logToTerminal('log', '[Load Content] Loaded brand:', loadedData.brand || '(empty)');
104400
- }
104081
+ // Load link (brand is auto-generated from product, geo, price)
104401
104082
  if (loadedData.link !== undefined) {
104402
104083
  setLink(loadedData.link || '');
104403
104084
  logToTerminal('log', '[Load Content] Loaded link:', loadedData.link || '(empty)');
@@ -104715,8 +104396,8 @@ ${imageData.originalPrompt}
104715
104396
  }
104716
104397
  setGeneratedData(rows);
104717
104398
  // Create workbook
104718
- const wb = xlsx__WEBPACK_IMPORTED_MODULE_53__.utils.book_new();
104719
- const ws = xlsx__WEBPACK_IMPORTED_MODULE_53__.utils.aoa_to_sheet(rows);
104399
+ const wb = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_new();
104400
+ const ws = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.aoa_to_sheet(rows);
104720
104401
  // Set column widths (approximate pixel width / 7)
104721
104402
  ws['!cols'] = [
104722
104403
  { wch: 20 }, // id
@@ -104729,10 +104410,10 @@ ${imageData.originalPrompt}
104729
104410
  { wch: 40 }, // image_link
104730
104411
  { wch: 20 } // brand
104731
104412
  ];
104732
- xlsx__WEBPACK_IMPORTED_MODULE_53__.utils.book_append_sheet(wb, ws, "Products");
104413
+ xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_append_sheet(wb, ws, "Products");
104733
104414
  // Generate buffer
104734
- const wbout = xlsx__WEBPACK_IMPORTED_MODULE_53__.write(wb, { bookType: 'xlsx', type: 'array' });
104735
- // Upload to Drive
104415
+ const wbout = xlsx__WEBPACK_IMPORTED_MODULE_50__.write(wb, { bookType: 'xlsx', type: 'array' });
104416
+ // Upload to Drive (имя файла по бренду)
104736
104417
  const dateStr = new Date().toISOString().split('T')[0];
104737
104418
  const fileName = `${brand}-${dateStr}.xlsx`;
104738
104419
  const result = await uploadFileToDrive(wbout, fileName, folderId || undefined);
@@ -104853,13 +104534,13 @@ ${imageData.originalPrompt}
104853
104534
  setTestLoading(true);
104854
104535
  try {
104855
104536
  // Create simple test workbook with structure
104856
- const wb = xlsx__WEBPACK_IMPORTED_MODULE_53__.utils.book_new();
104537
+ const wb = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_new();
104857
104538
  const rows = [
104858
104539
  INSTRUCTION_ROW,
104859
104540
  ['id', 'title', 'description', 'availability', 'condition', 'price', 'link', 'image_link', 'brand'],
104860
104541
  ['test1', 'Test Title', 'Test Description', 'in stock', 'new', '10.00 USD', 'http://test.com', 'http://test.com/img.jpg', 'TestBrand']
104861
104542
  ];
104862
- const ws = xlsx__WEBPACK_IMPORTED_MODULE_53__.utils.aoa_to_sheet(rows);
104543
+ const ws = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.aoa_to_sheet(rows);
104863
104544
  // Set column widths
104864
104545
  ws['!cols'] = [
104865
104546
  { wch: 20 }, // id
@@ -104872,8 +104553,8 @@ ${imageData.originalPrompt}
104872
104553
  { wch: 40 }, // image_link
104873
104554
  { wch: 20 } // brand
104874
104555
  ];
104875
- xlsx__WEBPACK_IMPORTED_MODULE_53__.utils.book_append_sheet(wb, ws, "Test");
104876
- const wbout = xlsx__WEBPACK_IMPORTED_MODULE_53__.write(wb, { bookType: 'xlsx', type: 'array' });
104556
+ xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_append_sheet(wb, ws, "Test");
104557
+ const wbout = xlsx__WEBPACK_IMPORTED_MODULE_50__.write(wb, { bookType: 'xlsx', type: 'array' });
104877
104558
  // Try to extract folder ID if available, otherwise upload to root
104878
104559
  const folderId = driveFolderUrl ? extractFolderId(driveFolderUrl) : undefined;
104879
104560
  const result = await uploadFileToDrive(wbout, 'test_table.xlsx', folderId || undefined);
@@ -104907,7 +104588,7 @@ ${imageData.originalPrompt}
104907
104588
  };
104908
104589
  // Show lock screen if not unlocked
104909
104590
  if (!unlocked) {
104910
- return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_35__["default"], { theme: theme },
104591
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { theme: theme },
104911
104592
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_4__["default"], null),
104912
104593
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { onClick: handleSecretClick, sx: {
104913
104594
  width: '100vw',
@@ -104947,24 +104628,24 @@ ${imageData.originalPrompt}
104947
104628
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("br", null),
104948
104629
  "Please contact system administrator"))));
104949
104630
  }
104950
- return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_35__["default"], { theme: theme },
104631
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { theme: theme },
104951
104632
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_4__["default"], null),
104952
104633
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_7__["default"], { maxWidth: "lg", sx: { py: 4 } },
104953
104634
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 } },
104954
104635
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h4", component: "h1", sx: { fontWeight: 'bold', color: 'primary.main' } }, "Docs Combiner"),
104955
104636
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
104956
104637
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { onClick: () => setPromptManagerOpen(true), color: "inherit", "aria-label": "manage prompts", sx: { mr: 1 } },
104957
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_49__["default"], null)),
104958
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { onClick: toggleTheme, color: "inherit", "aria-label": "toggle theme" }, darkMode ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_39__["default"], null) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null)))),
104638
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_46__["default"], null)),
104639
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { onClick: toggleTheme, color: "inherit", "aria-label": "toggle theme" }, darkMode ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_36__["default"], null) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_35__["default"], null)))),
104959
104640
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_9__["default"], { variant: "outlined", sx: { mb: 4 } },
104960
104641
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_10__["default"], null,
104961
104642
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 3 },
104962
104643
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
104963
104644
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true }, "Google Drive Authentication"),
104964
104645
  accessToken ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_12__["default"], null,
104965
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_13__["default"], { expandIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_43__["default"], null) },
104646
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_13__["default"], { expandIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_40__["default"], null) },
104966
104647
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { sx: { display: 'flex', alignItems: 'center', color: 'success.main' } },
104967
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_40__["default"], { sx: { mr: 1 } }),
104648
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_37__["default"], { sx: { mr: 1 } }),
104968
104649
  " Logged In (Credentials Hidden)")),
104969
104650
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_14__["default"], null,
104970
104651
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 2 },
@@ -104975,7 +104656,7 @@ ${imageData.originalPrompt}
104975
104656
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Client ID", variant: "outlined", fullWidth: true, value: clientId, onChange: (e) => handleClientIdChange(e.target.value), helperText: "From Google Cloud Console (OAuth 2.0 Client ID)" }),
104976
104657
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Client Secret", variant: "outlined", fullWidth: true, value: clientSecret, onChange: (e) => handleClientSecretChange(e.target.value) }))),
104977
104658
  openaiApiKey && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 2, mt: 2 } },
104978
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_36__["default"], { color: openRouterAccountBalance !== null ? 'primary' : 'disabled' }),
104659
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_33__["default"], { color: openRouterAccountBalance !== null ? 'primary' : 'disabled' }),
104979
104660
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", color: openRouterAccountBalance !== null ? 'text.primary' : 'text.secondary' },
104980
104661
  "\u0411\u0430\u043B\u0430\u043D\u0441: ",
104981
104662
  openRouterBalanceLoading ? 'Loading...' : (openRouterAccountBalance !== null ? `$${openRouterAccountBalance.toFixed(2)}` : 'N/A')),
@@ -104984,8 +104665,8 @@ ${imageData.originalPrompt}
104984
104665
  "\u041B\u0438\u043C\u0438\u0442 \u043A\u043B\u044E\u0447\u0430: ",
104985
104666
  openRouterBalanceLoading ? 'Loading...' : (openRouterBalance === -1 ? 'Без лимита' : (openRouterBalance !== null ? `${openRouterBalance.toFixed(4)}` : 'N/A'))))),
104986
104667
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, alignItems: "center", sx: { mt: 2 } },
104987
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: accessToken ? "success" : "primary", onClick: handleLogin, disabled: authLoading || !clientId || !clientSecret, startIcon: authLoading ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : (accessToken ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_40__["default"], null) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_45__["default"], null)), sx: { flexGrow: 1 } }, authLoading ? 'Logging in...' : (accessToken ? 'Logged In' : 'Login with Google')),
104988
- accessToken && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "error", onClick: handleLogout, startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_46__["default"], null) }, "Logout")))),
104668
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: accessToken ? "success" : "primary", onClick: handleLogin, disabled: authLoading || !clientId || !clientSecret, startIcon: authLoading ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : (accessToken ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_37__["default"], null) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_42__["default"], null)), sx: { flexGrow: 1 } }, authLoading ? 'Logging in...' : (accessToken ? 'Logged In' : 'Login with Google')),
104669
+ accessToken && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "error", onClick: handleLogout, startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_43__["default"], null) }, "Logout")))),
104989
104670
  !accessToken && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
104990
104671
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], null),
104991
104672
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
@@ -105001,18 +104682,15 @@ ${imageData.originalPrompt}
105001
104682
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 14 }),
105002
104683
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0444\u0430\u0439\u043B\u043E\u0432...")))),
105003
104684
  !checkingFolderFiles && folderFilesInfo && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], null,
105004
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "span", sx: { display: 'block' } }, folderFilesInfo.bankaCount > 0 ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'success.main' } },
105005
- "\u2713 \u041D\u0430\u0439\u0434\u0435\u043D\u043E \u0438\u0441\u0445\u043E\u0434\u043D\u044B\u0445 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439: ",
105006
- folderFilesInfo.bankaCount)) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'warning.main' } }, "\u26A0 \u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F (banka*.png/jpg) \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B"))),
105007
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "span", sx: { display: 'block', mt: 0.5 } }, folderFilesInfo.hasProduct ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'success.main' } }, "\u2713 product.png/jpg \u043D\u0430\u0439\u0434\u0435\u043D")) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'info.main' } }, "\u2139 product.png/jpg \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"))))),
104685
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "span", sx: { display: 'block' } }, folderFilesInfo.hasProduct ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'success.main' } }, "\u2713 product.png/jpg \u043D\u0430\u0439\u0434\u0435\u043D")) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'info.main' } }, "\u2139 product.png/jpg \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u2014 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u043A\u043D\u043E\u043F\u043A\u0443 \u043D\u0438\u0436\u0435"))))),
105008
104686
  !checkingFolderFiles && !folderFilesInfo && driveFolderUrl.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], null, "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u0443\u044E \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u043F\u0430\u043F\u043A\u0443 Google Drive"))))),
105009
104687
  !driveFolderUrl.trim() ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "info", sx: { mt: 2 } },
105010
104688
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body1", sx: { fontWeight: 'bold', mb: 1 } }, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043F\u0430\u043F\u043A\u0443 Google Drive \u0434\u043B\u044F \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u044F"),
105011
104689
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2" }, "\u0414\u043B\u044F \u0440\u0430\u0431\u043E\u0442\u044B \u0441 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0435\u0439 \u043A\u043E\u043D\u0442\u0435\u043D\u0442\u0430 \u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u0430\u043F\u043A\u0443 Google Drive. \u0412\u0441\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C\u0441\u044F \u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044C\u0441\u044F \u0438\u0437 \u044D\u0442\u043E\u0439 \u043F\u0430\u043F\u043A\u0438."))) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
105012
104690
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: { xs: 'column', sm: 'row' }, spacing: 2 },
105013
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Brand (Short ID)", variant: "outlined", fullWidth: true, value: brand, onChange: (e) => setBrand(e.target.value), placeholder: "e.g. NIKE" }),
105014
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { flexGrow: 1 } },
105015
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Link", variant: "outlined", fullWidth: true, value: link, onChange: (e) => handleLinkChange(e.target.value), error: !!linkError, helperText: linkError, placeholder: "http://example.com/product" }))),
104691
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Brand (Short ID)", variant: "outlined", sx: { flex: '0 0 320px', minWidth: 280 }, value: brand, InputProps: { readOnly: true }, placeholder: "\u0410\u0432\u0442\u043E: \u0442\u043E\u0432\u0430\u0440-\u0433\u0435\u043E-\u0446\u0435\u043D\u0430", helperText: "\u0410\u0432\u0442\u043E\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F \u0438\u0437 \u0442\u043E\u0432\u0430\u0440\u0430, \u0433\u0435\u043E \u0438 \u0446\u0435\u043D\u044B" }),
104692
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { flex: 1, minWidth: 0 } },
104693
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Link", variant: "outlined", fullWidth: true, value: link, onChange: (e) => handleLinkChange(e.target.value), onBlur: handleLinkBlur, error: !!linkError, helperText: linkError, placeholder: "https://example.com/product/" }))),
105016
104694
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { sx: { my: 2 } }),
105017
104695
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true }, "AI Generation Settings"),
105018
104696
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
@@ -105040,18 +104718,22 @@ ${imageData.originalPrompt}
105040
104718
  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_24__["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_24__["default"], { key: model.id, value: model.id }, model.name))))),
105041
104719
  !loadingValidationModels && validationModels.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], null, selectedValidationModel === _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation
105042
104720
  ? 'Используется модель по умолчанию'
105043
- : 'Выбрана модель: ' + (validationModels.find(m => m.id === selectedValidationModel)?.name || selectedValidationModel)))))),
104721
+ : 'Выбрана модель: ' + (validationModels.find(m => m.id === selectedValidationModel)?.name || selectedValidationModel))),
104722
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_25__["default"], { control: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_26__["default"], { checked: validationDisabled, onChange: (e) => handleValidationDisabledChange(e.target.checked), color: "primary" }), label: "\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443", sx: { mt: 1 } })))),
105044
104723
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
105045
- 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_37__["default"], null), onClick: handleGenerateContent, disabled: generating || loadingContentFromDrive || !openaiApiKey || !generateProduct.trim() || !generateGeo.trim(), sx: { flexGrow: 1 }, size: "large" }, generating ? 'Generating...' : 'Generate Titles & Descriptions'),
104724
+ 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 || loadingContentFromDrive || !openaiApiKey || !generateProduct.trim() || !generateGeo.trim(), sx: { flexGrow: 1 }, size: "large" }, generating ? 'Generating...' : 'Generate Titles & Descriptions'),
105046
104725
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 1, alignItems: "center", sx: { flexGrow: 1 } },
105047
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_25__["default"], { title: imageAspectRatio === '1:1'
104726
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["default"], { title: imageAspectRatio === '1:1'
105048
104727
  ? '1:1 — квадрат (1024×1024 px)'
105049
- : '2:3 — портрет (1024×1536 px)', placement: "top" },
104728
+ : imageAspectRatio === '2:3'
104729
+ ? '2:3 — портрет (1024×1536 px)'
104730
+ : 'Оба — квадрат и портрет на каждый подход', placement: "top" },
105050
104731
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
105051
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_26__["default"], { value: imageAspectRatio, exclusive: true, onChange: (_e, val) => {
104732
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_28__["default"], { value: imageAspectRatio, exclusive: true, onChange: (_e, val) => {
105052
104733
  if (val && val !== imageAspectRatio) {
105053
104734
  if (generatedImagesData.length > 0) {
105054
- if (window.confirm(`Сменить формат на ${val}? Сгенерированные изображения будут очищены.`)) {
104735
+ const label = val === 'both' ? 'оба (1:1 + 2:3)' : val;
104736
+ if (window.confirm(`Сменить формат на ${label}? Сгенерированные изображения будут очищены.`)) {
105055
104737
  setGeneratedImagesData([]);
105056
104738
  setImageAspectRatio(val);
105057
104739
  localStorage.setItem('imageAspectRatio', val);
@@ -105063,9 +104745,10 @@ ${imageData.originalPrompt}
105063
104745
  }
105064
104746
  }
105065
104747
  }, size: "small", disabled: generatingImages, sx: { height: 42 } },
105066
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["default"], { value: "1:1", sx: { px: 1.5, fontWeight: 600, fontSize: '0.8rem' } }, "1:1"),
105067
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["default"], { value: "2:3", sx: { px: 1.5, fontWeight: 600, fontSize: '0.8rem' } }, "2:3")))),
105068
- 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_37__["default"], null), onClick: handleGenerateImages, disabled: generatingImages ||
104748
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_29__["default"], { value: "1:1", sx: { px: 1.5, fontWeight: 600, fontSize: '0.8rem' } }, "1:1"),
104749
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_29__["default"], { value: "2:3", sx: { px: 1.5, fontWeight: 600, fontSize: '0.8rem' } }, "2:3"),
104750
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_29__["default"], { value: "both", sx: { px: 1.5, fontWeight: 600, fontSize: '0.8rem' } }, "\u041E\u0431\u0430")))),
104751
+ 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 ||
105069
104752
  loadingImagesFromDrive ||
105070
104753
  !openaiApiKey ||
105071
104754
  (!accessToken && !refreshToken) ||
@@ -105091,16 +104774,15 @@ ${imageData.originalPrompt}
105091
104774
  openaiApiKey && (accessToken || refreshToken) && !generateProduct.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "error", sx: { mt: 1 } }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u0435 \u0422\u043E\u0432\u0430\u0440")),
105092
104775
  openaiApiKey && (accessToken || refreshToken) && generateProduct.trim() && !generateGeo.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "error", sx: { mt: 1 } }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u0435 \u0413\u0435\u043E")),
105093
104776
  openaiApiKey && (accessToken || refreshToken) && generateProduct.trim() && generateGeo.trim() && !driveFolderUrl.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "error", sx: { mt: 1 } }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 URL \u043F\u0430\u043F\u043A\u0438 Google Drive"))))),
104777
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { type: "file", ref: productFileInputRef, accept: "image/png,image/jpeg,image/jpg", style: { display: 'none' }, onChange: handleProductFileSelected }),
105094
104778
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
105095
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "success", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_37__["default"], null), onClick: handleGenerateProduct, disabled: generatingProduct || !openaiApiKey || !accessToken || !driveFolderUrl.trim(), sx: { flexGrow: 1 }, size: "large" }, generatingProduct
105096
- ? 'Generating Product...'
105097
- : (productGeneratedImage ? 'Перегенерировать Product' : 'Generate Product from Banka')),
105098
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "info", startIcon: generatingLanding ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_47__["default"], null), onClick: handleCreateLanding, disabled: generatingLanding ||
104779
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "success", startIcon: uploadingProduct ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null), onClick: handleUploadProductFile, disabled: uploadingProduct || !accessToken || !driveFolderUrl.trim(), sx: { flexGrow: 1 }, size: "large" }, uploadingProduct ? 'Загрузка...' : 'Загрузить product.png/jpg'),
104780
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "info", startIcon: generatingLanding ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_44__["default"], null), onClick: handleCreateLanding, disabled: generatingLanding ||
105099
104781
  !openaiApiKey ||
105100
104782
  !accessToken ||
105101
104783
  !driveFolderUrl.trim() ||
105102
104784
  (folderFilesInfo !== null && !folderFilesInfo.hasProduct), sx: { flexGrow: 1 }, size: "large", title: folderFilesInfo !== null && !folderFilesInfo.hasProduct
105103
- ? 'product.png/jpg не найден в папке. Сначала сгенерируйте его с помощью кнопки "Generate Product from Banka"'
104785
+ ? 'product.png/jpg не найден в папке. Загрузите его с помощью кнопки «Загрузить product.png/jpg»'
105104
104786
  : undefined }, generatingLanding ? 'Creating Landing...' : 'Создать лендинг')),
105105
104787
  (generatedImagesData.length > 0 || loadingImagesFromDrive) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2, mt: 3 } },
105106
104788
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } },
@@ -105133,168 +104815,172 @@ ${imageData.originalPrompt}
105133
104815
  gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
105134
104816
  gap: 2,
105135
104817
  mt: 1
105136
- } }, generatedImagesData.map((imageData) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { key: imageData.index, sx: { position: 'relative' } },
105137
- imageData.generating ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
105138
- width: '100%',
105139
- aspectRatio: imageAspectRatio === '2:3' ? '2 / 3' : '1 / 1',
105140
- border: (theme) => `2px dashed ${theme.palette.primary.main}`,
105141
- borderRadius: 1,
105142
- backgroundColor: (theme) => theme.palette.mode === 'dark'
105143
- ? 'rgba(25, 118, 210, 0.1)'
105144
- : 'rgba(25, 118, 210, 0.05)',
105145
- display: 'flex',
105146
- flexDirection: 'column',
105147
- alignItems: 'center',
105148
- justifyContent: 'center',
105149
- gap: 2,
105150
- p: 3
105151
- } },
105152
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 40, sx: { color: 'primary.main' } }),
105153
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'text.secondary', textAlign: 'center', fontWeight: 'bold' } }, "\u0413\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F..."),
105154
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', textAlign: 'center' } }, imageData.approach),
105155
- generatingImages && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', textAlign: 'center' } },
105156
- "\u23F1\uFE0F \u041F\u0440\u043E\u0448\u043B\u043E: ",
105157
- formatElapsedTime(elapsedTime.images))))) : imageData.imageUrl ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
105158
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "img", src: imageData.imageUrl, alt: `Generated Image ${imageData.index}`, sx: {
104818
+ } }, generatedImagesData.map((imageData) => {
104819
+ const imgRatio = imageData.aspectRatio ?? (imageAspectRatio === 'both' ? '1:1' : imageAspectRatio);
104820
+ const aspectRatioCss = imgRatio === '2:3' ? '2 / 3' : '1 / 1';
104821
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { key: imageData.index, sx: { position: 'relative' } },
104822
+ imageData.generating ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
105159
104823
  width: '100%',
105160
- height: 'auto',
105161
- display: 'block',
105162
- objectFit: 'contain',
105163
- border: (theme) => {
105164
- if (imageData.uploaded || imageData.checkStatus === 'ok') {
105165
- return `2px solid ${theme.palette.success.main}`;
105166
- }
105167
- else if (imageData.checkStatus === 'needs_rebuild') {
105168
- return `2px solid ${theme.palette.error.main}`;
105169
- }
105170
- else if (imageData.checkStatus === 'checking') {
105171
- return `2px solid ${theme.palette.warning.main}`;
105172
- }
105173
- else {
105174
- return `2px solid ${theme.palette.primary.main}`;
105175
- }
105176
- },
104824
+ aspectRatio: aspectRatioCss,
104825
+ border: (theme) => `2px dashed ${theme.palette.primary.main}`,
105177
104826
  borderRadius: 1,
105178
- backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#2a2a2a' : '#f5f5f5'
105179
- } }),
105180
- imageData.regenerating && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
105181
- position: 'absolute',
105182
- top: '50%',
105183
- left: '50%',
105184
- transform: 'translate(-50%, -50%)',
104827
+ backgroundColor: (theme) => theme.palette.mode === 'dark'
104828
+ ? 'rgba(25, 118, 210, 0.1)'
104829
+ : 'rgba(25, 118, 210, 0.05)',
105185
104830
  display: 'flex',
105186
104831
  flexDirection: 'column',
105187
104832
  alignItems: 'center',
105188
- gap: 1
104833
+ justifyContent: 'center',
104834
+ gap: 2,
104835
+ p: 3
105189
104836
  } },
105190
104837
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 40, sx: { color: 'primary.main' } }),
105191
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'white', fontWeight: 'bold', textShadow: '1px 1px 2px rgba(0,0,0,0.8)' } }, "\u041F\u0435\u0440\u0435\u0434\u0435\u043B\u043A\u0430..."))))) : imageData.failed ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
105192
- width: '100%',
105193
- aspectRatio: imageAspectRatio === '2:3' ? '2 / 3' : '1 / 1',
105194
- border: (theme) => `2px dashed ${theme.palette.error.main}`,
105195
- borderRadius: 1,
105196
- backgroundColor: (theme) => theme.palette.mode === 'dark'
105197
- ? 'rgba(244, 67, 54, 0.1)'
105198
- : '#ffebee',
105199
- display: 'flex',
105200
- flexDirection: 'column',
105201
- alignItems: 'center',
105202
- justifyContent: 'center',
105203
- gap: 2,
105204
- p: 3
105205
- } },
105206
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h4", sx: { color: 'error.main' } }, "\u274C"),
105207
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'error.main', textAlign: 'center', fontWeight: 'bold' } }, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C"),
105208
- imageData.errorMessage && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'error.main', textAlign: 'center', fontSize: '0.7rem' } }, imageData.errorMessage.length > 100
105209
- ? imageData.errorMessage.substring(0, 100) + '...'
105210
- : imageData.errorMessage)))) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
105211
- width: '100%',
105212
- aspectRatio: imageAspectRatio === '2:3' ? '2 / 3' : '1 / 1',
105213
- border: (theme) => `2px dashed ${theme.palette.divider}`,
105214
- borderRadius: 1,
105215
- backgroundColor: (theme) => theme.palette.mode === 'dark'
105216
- ? 'rgba(255,255,255,0.04)'
105217
- : '#f5f5f5',
105218
- display: 'flex',
105219
- flexDirection: 'column',
105220
- alignItems: 'center',
105221
- justifyContent: 'center',
105222
- gap: 2,
105223
- p: 3
105224
- } },
105225
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h4", sx: { color: 'text.secondary' } }, "\u23F3"),
105226
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'text.secondary', textAlign: 'center', fontWeight: 'bold' } }, "\u0412 \u043E\u0447\u0435\u0440\u0435\u0434\u0438"),
105227
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', textAlign: 'center' } }, generatingImages ? 'Ожидает генерации...' : 'Готово к генерации'),
105228
- generatingImages && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', textAlign: 'center' } },
105229
- "\u23F1\uFE0F \u041F\u0440\u043E\u0448\u043B\u043E: ",
105230
- formatElapsedTime(elapsedTime.images))))),
105231
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mt: 1, mb: 1 } },
105232
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { display: 'block', fontWeight: 'bold' } },
105233
- imageData.index,
105234
- ". ",
105235
- imageData.approach),
105236
- imageData.checkStatus === 'ok' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'success.main', display: 'block', fontWeight: 'bold' } }, "\u2705 \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430")),
105237
- imageData.checkStatus === 'needs_rebuild' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
105238
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'error.main', display: 'block', fontWeight: 'bold' } }, imageData.checkFailed ? '⚠️ Ошибка проверки' : '❌ Требует пересборки'),
105239
- imageData.checkErrors && imageData.checkErrors.length > 0 && !imageData.checkFailed && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mt: 0.5 } },
105240
- imageData.checkErrors.slice(0, 2).map((error, idx) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { key: idx, variant: "caption", sx: {
105241
- color: 'error.main',
105242
- display: 'block',
105243
- fontSize: '0.7rem',
105244
- lineHeight: 1.2
105245
- } },
105246
- "\u2022 ",
105247
- error.length > 50 ? error.substring(0, 50) + '...' : error))),
105248
- imageData.checkErrors.length > 2 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', fontSize: '0.7rem' } },
105249
- "+",
105250
- imageData.checkErrors.length - 2,
105251
- " \u0435\u0449\u0451")))),
105252
- imageData.checkFailed && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", color: "warning", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_48__["default"], null), onClick: () => handleRetryCheck(imageData), disabled: imageData.checking, sx: { mt: 0.5, fontSize: '0.7rem', py: 0.25, px: 1 } }, "\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443")))),
105253
- imageData.checkStatus === 'checking' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'warning.main', display: 'block' } }, "\uD83D\uDD0D \u041F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442\u0441\u044F...")),
105254
- imageData.checkStatus === 'pending' && !imageData.failed && !imageData.generating && imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', display: 'block' } }, "\u23F3 \u041E\u0436\u0438\u0434\u0430\u0435\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438")),
105255
- imageData.failed && !imageData.generating && !imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'error.main', display: 'block', fontWeight: 'bold' } }, "\u274C \u0413\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C")),
105256
- imageData.uploaded && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'success.main', display: 'block', mt: 0.5 } }, "\u2713 \u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E"))),
105257
- !imageData.generating && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', gap: 1, flexDirection: 'column', mt: 1 } },
105258
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { size: "small", multiline: true, minRows: 2, maxRows: 4, fullWidth: true, placeholder: "\u0423\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u044F \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u043A\u0438 (\u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E \u043F\u0440\u0438 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0430\u0445)", value: imageData.customRegeneratePrompt || '', onChange: (e) => {
105259
- setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
105260
- ? { ...img, customRegeneratePrompt: e.target.value }
105261
- : img));
105262
- }, disabled: imageData.regenerating ||
105263
- imageData.uploading ||
105264
- imageData.checkStatus === 'checking' ||
105265
- (generatingImages && !imageData.imageUrl && !imageData.failed && !imageData.generating), sx: {
105266
- '& .MuiInputBase-root': {
105267
- fontSize: '0.875rem',
105268
- backgroundColor: (theme) => imageData.checkStatus === 'needs_rebuild'
105269
- ? (theme.palette.mode === 'dark'
105270
- ? 'rgba(244, 67, 54, 0.15)'
105271
- : 'rgba(244, 67, 54, 0.05)')
105272
- : 'transparent'
105273
- }
105274
- } }),
105275
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "contained", color: imageData.failed ? 'error' : imageData.checkStatus === 'needs_rebuild' ? 'warning' : 'primary', startIcon: imageData.regenerating ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_37__["default"], null), onClick: () => handleRegenerateImage(imageData), disabled: imageData.regenerating ||
105276
- imageData.uploading ||
105277
- imageData.checkStatus === 'checking' ||
105278
- !imageData.originalPrompt ||
105279
- !imageData.productImageUrl ||
105280
- (generatingImages && !imageData.imageUrl && !imageData.failed && !imageData.generating), fullWidth: true }, imageData.regenerating
105281
- ? (imageData.failed ? 'Генерация...' : 'Переделка...')
105282
- : ((generatingImages && !imageData.imageUrl && !imageData.failed && !imageData.generating)
105283
- ? 'В очереди'
105284
- : (!imageData.imageUrl ? 'Сгенерировать' : 'Переделать'))),
105285
- imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", color: "secondary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_48__["default"], null), onClick: () => handleRegenerateImageFresh(imageData), disabled: imageData.regenerating ||
105286
- imageData.uploading ||
105287
- imageData.checkStatus === 'checking' ||
105288
- !imageData.originalPrompt ||
105289
- !imageData.productImageUrl, fullWidth: true }, "\u041F\u0435\u0440\u0435\u0434\u0435\u043B\u0430\u0442\u044C \u0437\u0430\u043D\u043E\u0432\u043E")),
105290
- !imageData.uploaded && imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_41__["default"], null), onClick: () => {
105291
- const folderId = extractFolderId(driveFolderUrl);
105292
- if (folderId) {
105293
- handleUploadImage(imageData, folderId);
105294
- }
105295
- }, disabled: imageData.uploading || imageData.regenerating || !driveFolderUrl.trim(), fullWidth: true }, imageData.uploading ? 'Загрузка...' : 'Загрузить'))))))))),
104838
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'text.secondary', textAlign: 'center', fontWeight: 'bold' } }, "\u0413\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F..."),
104839
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', textAlign: 'center' } }, imageData.approach),
104840
+ generatingImages && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', textAlign: 'center' } },
104841
+ "\u23F1\uFE0F \u041F\u0440\u043E\u0448\u043B\u043E: ",
104842
+ formatElapsedTime(elapsedTime.images))))) : imageData.imageUrl ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
104843
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "img", src: imageData.imageUrl, alt: `Generated Image ${imageData.index}`, sx: {
104844
+ width: '100%',
104845
+ height: 'auto',
104846
+ display: 'block',
104847
+ objectFit: 'contain',
104848
+ border: (theme) => {
104849
+ if (imageData.uploaded || imageData.checkStatus === 'ok') {
104850
+ return `2px solid ${theme.palette.success.main}`;
104851
+ }
104852
+ else if (imageData.checkStatus === 'needs_rebuild') {
104853
+ return `2px solid ${theme.palette.error.main}`;
104854
+ }
104855
+ else if (imageData.checkStatus === 'checking') {
104856
+ return `2px solid ${theme.palette.warning.main}`;
104857
+ }
104858
+ else {
104859
+ return `2px solid ${theme.palette.primary.main}`;
104860
+ }
104861
+ },
104862
+ borderRadius: 1,
104863
+ backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#2a2a2a' : '#f5f5f5'
104864
+ } }),
104865
+ imageData.regenerating && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
104866
+ position: 'absolute',
104867
+ top: '50%',
104868
+ left: '50%',
104869
+ transform: 'translate(-50%, -50%)',
104870
+ display: 'flex',
104871
+ flexDirection: 'column',
104872
+ alignItems: 'center',
104873
+ gap: 1
104874
+ } },
104875
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 40, sx: { color: 'primary.main' } }),
104876
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'white', fontWeight: 'bold', textShadow: '1px 1px 2px rgba(0,0,0,0.8)' } }, "\u041F\u0435\u0440\u0435\u0434\u0435\u043B\u043A\u0430..."))))) : imageData.failed ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
104877
+ width: '100%',
104878
+ aspectRatio: aspectRatioCss,
104879
+ border: (theme) => `2px dashed ${theme.palette.error.main}`,
104880
+ borderRadius: 1,
104881
+ backgroundColor: (theme) => theme.palette.mode === 'dark'
104882
+ ? 'rgba(244, 67, 54, 0.1)'
104883
+ : '#ffebee',
104884
+ display: 'flex',
104885
+ flexDirection: 'column',
104886
+ alignItems: 'center',
104887
+ justifyContent: 'center',
104888
+ gap: 2,
104889
+ p: 3
104890
+ } },
104891
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h4", sx: { color: 'error.main' } }, "\u274C"),
104892
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'error.main', textAlign: 'center', fontWeight: 'bold' } }, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C"),
104893
+ imageData.errorMessage && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'error.main', textAlign: 'center', fontSize: '0.7rem' } }, imageData.errorMessage.length > 100
104894
+ ? imageData.errorMessage.substring(0, 100) + '...'
104895
+ : imageData.errorMessage)))) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
104896
+ width: '100%',
104897
+ aspectRatio: aspectRatioCss,
104898
+ border: (theme) => `2px dashed ${theme.palette.divider}`,
104899
+ borderRadius: 1,
104900
+ backgroundColor: (theme) => theme.palette.mode === 'dark'
104901
+ ? 'rgba(255,255,255,0.04)'
104902
+ : '#f5f5f5',
104903
+ display: 'flex',
104904
+ flexDirection: 'column',
104905
+ alignItems: 'center',
104906
+ justifyContent: 'center',
104907
+ gap: 2,
104908
+ p: 3
104909
+ } },
104910
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h4", sx: { color: 'text.secondary' } }, "\u23F3"),
104911
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'text.secondary', textAlign: 'center', fontWeight: 'bold' } }, "\u0412 \u043E\u0447\u0435\u0440\u0435\u0434\u0438"),
104912
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', textAlign: 'center' } }, generatingImages ? 'Ожидает генерации...' : 'Готово к генерации'),
104913
+ generatingImages && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', textAlign: 'center' } },
104914
+ "\u23F1\uFE0F \u041F\u0440\u043E\u0448\u043B\u043E: ",
104915
+ formatElapsedTime(elapsedTime.images))))),
104916
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mt: 1, mb: 1 } },
104917
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { display: 'block', fontWeight: 'bold' } },
104918
+ imageData.index,
104919
+ ". ",
104920
+ imageData.approach),
104921
+ imageData.checkStatus === 'ok' && imageData.checkResult !== 'Проверка отключена' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'success.main', display: 'block', fontWeight: 'bold' } }, "\u2705 \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430")),
104922
+ imageData.checkStatus === 'needs_rebuild' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
104923
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'error.main', display: 'block', fontWeight: 'bold' } }, imageData.checkFailed ? '⚠️ Ошибка проверки' : '❌ Требует пересборки'),
104924
+ imageData.checkErrors && imageData.checkErrors.length > 0 && !imageData.checkFailed && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mt: 0.5 } },
104925
+ imageData.checkErrors.slice(0, 2).map((error, idx) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { key: idx, variant: "caption", sx: {
104926
+ color: 'error.main',
104927
+ display: 'block',
104928
+ fontSize: '0.7rem',
104929
+ lineHeight: 1.2
104930
+ } },
104931
+ "\u2022 ",
104932
+ error.length > 50 ? error.substring(0, 50) + '...' : error))),
104933
+ imageData.checkErrors.length > 2 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', fontSize: '0.7rem' } },
104934
+ "+",
104935
+ imageData.checkErrors.length - 2,
104936
+ " \u0435\u0449\u0451")))),
104937
+ imageData.checkFailed && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", color: "warning", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_45__["default"], null), onClick: () => handleRetryCheck(imageData), disabled: imageData.checking, sx: { mt: 0.5, fontSize: '0.7rem', py: 0.25, px: 1 } }, "\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443")))),
104938
+ imageData.checkStatus === 'checking' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'warning.main', display: 'block' } }, "\uD83D\uDD0D \u041F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442\u0441\u044F...")),
104939
+ imageData.checkStatus === 'pending' && !imageData.failed && !imageData.generating && imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', display: 'block' } }, "\u23F3 \u041E\u0436\u0438\u0434\u0430\u0435\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438")),
104940
+ imageData.failed && !imageData.generating && !imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'error.main', display: 'block', fontWeight: 'bold' } }, "\u274C \u0413\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C")),
104941
+ imageData.uploaded && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'success.main', display: 'block', mt: 0.5 } }, "\u2713 \u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E"))),
104942
+ !imageData.generating && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', gap: 1, flexDirection: 'column', mt: 1 } },
104943
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { size: "small", multiline: true, minRows: 2, maxRows: 4, fullWidth: true, placeholder: "\u0423\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u044F \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u043A\u0438 (\u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E \u043F\u0440\u0438 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0430\u0445)", value: imageData.customRegeneratePrompt || '', onChange: (e) => {
104944
+ setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
104945
+ ? { ...img, customRegeneratePrompt: e.target.value }
104946
+ : img));
104947
+ }, disabled: imageData.regenerating ||
104948
+ imageData.uploading ||
104949
+ imageData.checkStatus === 'checking' ||
104950
+ (generatingImages && !imageData.imageUrl && !imageData.failed && !imageData.generating), sx: {
104951
+ '& .MuiInputBase-root': {
104952
+ fontSize: '0.875rem',
104953
+ backgroundColor: (theme) => imageData.checkStatus === 'needs_rebuild'
104954
+ ? (theme.palette.mode === 'dark'
104955
+ ? 'rgba(244, 67, 54, 0.15)'
104956
+ : 'rgba(244, 67, 54, 0.05)')
104957
+ : 'transparent'
104958
+ }
104959
+ } }),
104960
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "contained", color: imageData.failed ? 'error' : imageData.checkStatus === 'needs_rebuild' ? 'warning' : 'primary', startIcon: imageData.regenerating ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_34__["default"], null), onClick: () => handleRegenerateImage(imageData), disabled: imageData.regenerating ||
104961
+ imageData.uploading ||
104962
+ imageData.checkStatus === 'checking' ||
104963
+ !imageData.originalPrompt ||
104964
+ !imageData.productImageUrl ||
104965
+ (generatingImages && !imageData.imageUrl && !imageData.failed && !imageData.generating), fullWidth: true }, imageData.regenerating
104966
+ ? (imageData.failed ? 'Генерация...' : 'Переделка...')
104967
+ : ((generatingImages && !imageData.imageUrl && !imageData.failed && !imageData.generating)
104968
+ ? 'В очереди'
104969
+ : (!imageData.imageUrl ? 'Сгенерировать' : 'Переделать'))),
104970
+ imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", color: "secondary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_45__["default"], null), onClick: () => handleRegenerateImageFresh(imageData), disabled: imageData.regenerating ||
104971
+ imageData.uploading ||
104972
+ imageData.checkStatus === 'checking' ||
104973
+ !imageData.originalPrompt ||
104974
+ !imageData.productImageUrl, fullWidth: true }, "\u041F\u0435\u0440\u0435\u0434\u0435\u043B\u0430\u0442\u044C \u0437\u0430\u043D\u043E\u0432\u043E")),
104975
+ !imageData.uploaded && imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null), onClick: () => {
104976
+ const folderId = extractFolderId(driveFolderUrl);
104977
+ if (folderId) {
104978
+ handleUploadImage(imageData, folderId);
104979
+ }
104980
+ }, disabled: imageData.uploading || imageData.regenerating || !driveFolderUrl.trim(), fullWidth: true }, imageData.uploading ? 'Загрузка...' : 'Загрузить'))))));
104981
+ }))),
105296
104982
  generatedImagesData.length > 0 && generatedImagesData.some(img => !img.uploaded && img.imageUrl) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mt: 2 } },
105297
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "primary", onClick: handleUploadAllImages, disabled: uploadingImages || generatingImages || !driveFolderUrl.trim(), startIcon: uploadingImages ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_41__["default"], null), fullWidth: true }, uploadingImages ? 'Загрузка...' : `Загрузить все (${generatedImagesData.filter(img => !img.uploaded && img.imageUrl).length})`))))),
104983
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "primary", onClick: handleUploadAllImages, disabled: uploadingImages || generatingImages || !driveFolderUrl.trim(), startIcon: uploadingImages ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null), fullWidth: true }, uploadingImages ? 'Загрузка...' : `Загрузить все (${generatedImagesData.filter(img => !img.uploaded && img.imageUrl).length})`))))),
105298
104984
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { sx: { my: 2 } }),
105299
104985
  (generatedTitlesData.length > 0 || generatedTextsData.length > 0 || loadingContentFromDrive) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2 } },
105300
104986
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } },
@@ -105329,7 +105015,7 @@ ${imageData.originalPrompt}
105329
105015
  const pairLabel = pairApproach ? pairApproach.name : `пара ${i + 1}`;
105330
105016
  const pairGenerating = (titleData?.generating || textData?.generating);
105331
105017
  const pairFailed = (!pairGenerating && titleData?.failed && textData?.failed);
105332
- return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_28__["default"], { key: i, variant: "outlined", sx: {
105018
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_30__["default"], { key: i, variant: "outlined", sx: {
105333
105019
  p: 2,
105334
105020
  borderColor: pairFailed
105335
105021
  ? 'error.main'
@@ -105357,7 +105043,7 @@ ${imageData.originalPrompt}
105357
105043
  "\u041F\u0430\u0440\u0430 ",
105358
105044
  i + 1),
105359
105045
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", color: "text.secondary" }, pairLabel),
105360
- translatingPairs && !pairTranslations[i] ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 13, sx: { color: 'text.disabled', ml: 0.5 } })) : pairTranslations[i] ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_25__["default"], { title: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
105046
+ translatingPairs && !pairTranslations[i] ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 13, sx: { color: 'text.disabled', ml: 0.5 } })) : pairTranslations[i] ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["default"], { title: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
105361
105047
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { fontWeight: 700, display: 'block', mb: 0.5 } }, "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A:"),
105362
105048
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { display: 'block', mb: 1 } }, pairTranslations[i].titleRu),
105363
105049
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { fontWeight: 700, display: 'block', mb: 0.5 } }, "\u0422\u0435\u043A\u0441\u0442:"),
@@ -105365,7 +105051,7 @@ ${imageData.originalPrompt}
105365
105051
  tooltip: { sx: { maxWidth: 360, fontSize: 12, lineHeight: 1.6, p: '10px 14px' } },
105366
105052
  } },
105367
105053
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { size: "small", sx: { p: 0.25, color: 'text.disabled', '&:hover': { color: 'primary.main' } } },
105368
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_44__["default"], { sx: { fontSize: 14 } })))) : null),
105054
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_41__["default"], { sx: { fontSize: 14 } })))) : null),
105369
105055
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 1.5 },
105370
105056
  titleData && (titleData.generating ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
105371
105057
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16, sx: { color: 'primary.main' } }),
@@ -105406,7 +105092,7 @@ ${imageData.originalPrompt}
105406
105092
  }))))),
105407
105093
  (landingGenerationLogs.length > 0 || generatingLanding) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2 } },
105408
105094
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } }, "\u041F\u0440\u043E\u0446\u0435\u0441\u0441 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F \u043B\u0435\u043D\u0434\u0438\u043D\u0433\u0430"),
105409
- landingGenerationLogs.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_28__["default"], { sx: {
105095
+ landingGenerationLogs.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_30__["default"], { sx: {
105410
105096
  p: 2,
105411
105097
  backgroundColor: (theme) => theme.palette.mode === 'dark'
105412
105098
  ? 'rgba(25, 118, 210, 0.1)'
@@ -105432,11 +105118,11 @@ ${imageData.originalPrompt}
105432
105118
  generatingLanding && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1, mb: 2 } },
105433
105119
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }),
105434
105120
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'text.secondary' } }, formatElapsedTime(elapsedTime.landing)))),
105435
- !generatingLanding && generatedLandingHTML && generatedLandingImageBlob && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "primary", onClick: handlePreviewLanding, startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_50__["default"], null), fullWidth: true }, "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043B\u0435\u043D\u0434\u0438\u043D\u0433\u0430")))),
105121
+ !generatingLanding && generatedLandingHTML && generatedLandingImageBlob && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "primary", onClick: handlePreviewLanding, startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_47__["default"], null), fullWidth: true }, "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043B\u0435\u043D\u0434\u0438\u043D\u0433\u0430")))),
105436
105122
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2 },
105437
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", size: "large", onClick: handleGenerate, disabled: loading || !accessToken, startIcon: loading ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_41__["default"], null), sx: { py: 1.5, fontSize: '1.1rem', flexGrow: 1 } }, loading ? 'Generating...' : 'Generate Table')),
105123
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", size: "large", onClick: handleGenerate, disabled: loading || !accessToken, startIcon: loading ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null), sx: { py: 1.5, fontSize: '1.1rem', flexGrow: 1 } }, loading ? 'Generating...' : 'Generate Table')),
105438
105124
  uploadedLink && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "success", sx: { mt: 2 }, action: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { "aria-label": "copy link", color: "inherit", size: "small", onClick: handleCopyLink, sx: { ml: 1 } },
105439
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_42__["default"], { fontSize: "small" })) },
105125
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_39__["default"], { fontSize: "small" })) },
105440
105126
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
105441
105127
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
105442
105128
  "File Uploaded!",
@@ -105446,88 +105132,7 @@ ${imageData.originalPrompt}
105446
105132
  getElectronAPI().openExternal(uploadedLink);
105447
105133
  }, style: { cursor: 'pointer', textDecoration: 'underline', color: 'inherit' } }, "Click here to open in Google Drive")),
105448
105134
  linkCopied && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'success.dark', fontWeight: 'bold' } }, "Copied!")))))))))),
105449
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_29__["default"], { open: productModalOpen, onClose: () => !generatingProduct && !uploadingProduct && setProductModalOpen(false), maxWidth: "lg", fullWidth: true },
105450
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_30__["default"], { open: generatingProduct || uploadingProduct, sx: {
105451
- position: 'absolute',
105452
- zIndex: (theme) => theme.zIndex.modal + 1,
105453
- backgroundColor: 'rgba(0, 0, 0, 0.7)',
105454
- display: 'flex',
105455
- flexDirection: 'column',
105456
- gap: 3
105457
- } },
105458
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 } },
105459
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 60, sx: { color: 'white' } }),
105460
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", sx: { color: 'white', fontWeight: 'bold' } }, formatElapsedTime(elapsedTime.product))),
105461
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_28__["default"], { sx: {
105462
- p: 3,
105463
- maxWidth: '80%',
105464
- maxHeight: '60%',
105465
- overflow: 'auto',
105466
- backgroundColor: (theme) => theme.palette.mode === 'dark'
105467
- ? 'rgba(30, 30, 30, 0.95)'
105468
- : 'rgba(255, 255, 255, 0.95)'
105469
- } },
105470
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { mb: 2, fontWeight: 'bold' } }, uploadingProduct ? 'Процесс загрузки' : 'Процесс генерации'),
105471
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { id: "product-generation-logs", sx: {
105472
- fontFamily: 'monospace',
105473
- fontSize: '0.875rem',
105474
- lineHeight: 1.6,
105475
- whiteSpace: 'pre-wrap',
105476
- wordBreak: 'break-word',
105477
- maxHeight: '400px',
105478
- overflowY: 'auto'
105479
- } }, productGenerationLogs.length === 0 ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { color: "text.secondary" }, uploadingProduct ? 'Начало загрузки...' : 'Инициализация...')) : (productGenerationLogs.map((log, idx) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { key: idx, sx: {
105480
- mb: 0.5,
105481
- color: log.includes('❌') || log.toLowerCase().includes('error') ? 'error.main' :
105482
- log.includes('⚠️') || log.toLowerCase().includes('warn') ? 'warning.main' :
105483
- 'text.primary'
105484
- } }, log))))))),
105485
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_31__["default"], null,
105486
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6" }, "Product Image Generation")),
105487
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], null,
105488
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 3, sx: { mt: 1 } },
105489
- productSourceImages.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
105490
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "subtitle1", gutterBottom: true, sx: { fontWeight: 'bold' } },
105491
- "\u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F (",
105492
- productSourceImages.length,
105493
- "):"),
105494
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
105495
- display: 'grid',
105496
- gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
105497
- gap: 2,
105498
- mt: 1
105499
- } }, productSourceImages.map((url, idx) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { key: idx, component: "img", src: convertDriveUrlToImageUrl(url), alt: `Source ${idx + 1}`, sx: {
105500
- width: '100%',
105501
- height: 'auto',
105502
- maxHeight: 300,
105503
- objectFit: 'contain',
105504
- border: (theme) => `1px solid ${theme.palette.mode === 'dark' ? '#444' : '#ddd'}`,
105505
- borderRadius: 1
105506
- }, onError: (e) => {
105507
- // Fallback: try using thumbnail URL if direct view fails
105508
- const fileIdMatch = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);
105509
- if (fileIdMatch) {
105510
- const fileId = fileIdMatch[1];
105511
- e.target.src = `https://drive.google.com/thumbnail?id=${fileId}&sz=w1000`;
105512
- }
105513
- } })))))),
105514
- productGeneratedImage && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
105515
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "subtitle1", gutterBottom: true, sx: { fontWeight: 'bold' } }, "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0434\u0443\u043A\u0442\u0430:"),
105516
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "img", src: productGeneratedImage, alt: "Generated Product", sx: {
105517
- width: '100%',
105518
- height: 'auto',
105519
- maxHeight: 400,
105520
- objectFit: 'contain',
105521
- border: (theme) => `2px solid ${theme.palette.primary.main}`,
105522
- borderRadius: 1,
105523
- mt: 1
105524
- } }))),
105525
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043A \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438", variant: "outlined", fullWidth: true, multiline: true, minRows: 2, value: productRegeneratePrompt, onChange: (e) => setProductRegeneratePrompt(e.target.value), placeholder: "\u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: \u0441\u0434\u0435\u043B\u0430\u0439 \u0444\u043E\u043D \u0431\u0435\u043B\u044B\u043C, \u0443\u0432\u0435\u043B\u0438\u0447\u044C \u043B\u043E\u0433\u043E\u0442\u0438\u043F...", helperText: "\u042D\u0442\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u0431\u0443\u0434\u0443\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u044B \u043A \u0431\u0430\u0437\u043E\u0432\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 (\u043D\u0435 \u0437\u0430\u043C\u0435\u043D\u044F\u044E\u0442 \u0435\u0433\u043E). \u041E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0441\u0442\u044B\u043C \u0434\u043B\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u0430\u0437\u043E\u0432\u043E\u0433\u043E \u043F\u0440\u043E\u043C\u043F\u0442\u0430.", disabled: generatingProduct || uploadingProduct }))),
105526
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_33__["default"], { sx: { px: 3, pb: 2 } },
105527
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { onClick: () => setProductModalOpen(false), disabled: generatingProduct || uploadingProduct }, "\u0417\u0430\u043A\u0440\u044B\u0442\u044C"),
105528
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "secondary", onClick: handleRegenerateProduct, disabled: generatingProduct || uploadingProduct || !productSourceImages.length || !productGeneratedImage, startIcon: generatingProduct ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_37__["default"], null) }, generatingProduct ? 'Перегенерация...' : 'Перегенерировать'),
105529
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "primary", onClick: handleUploadProduct, disabled: generatingProduct || uploadingProduct || !productGeneratedImage || !driveFolderUrl.trim(), startIcon: uploadingProduct ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_41__["default"], null) }, uploadingProduct ? 'Загрузка...' : 'Загрузить (product.png)'))),
105530
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_PromptManagerDialog__WEBPACK_IMPORTED_MODULE_51__["default"], { open: promptManagerOpen, onClose: () => setPromptManagerOpen(false) }))));
105135
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_PromptManagerDialog__WEBPACK_IMPORTED_MODULE_48__["default"], { open: promptManagerOpen, onClose: () => setPromptManagerOpen(false) }))));
105531
105136
  }
105532
105137
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (App);
105533
105138
 
@@ -105962,12 +105567,15 @@ function PromptManagerDialog({ open, onClose }) {
105962
105567
  const [viewModes, setViewModes] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({});
105963
105568
  // selectedApproaches: array of indices from PAIR_APPROACH_POOL, ordered
105964
105569
  const [selectedApproaches, setSelectedApproaches] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([0, 1, 2]);
105570
+ // imageApproachCounts: по одному на каждый подход (CREO_APPROACHES), каждый 0–4
105571
+ const [imageApproachCounts, setImageApproachCounts] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => Array(_prompts__WEBPACK_IMPORTED_MODULE_34__.CREO_APPROACHES.length).fill(1));
105965
105572
  // Загрузить оверрайды при открытии
105966
105573
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
105967
105574
  if (open) {
105968
105575
  const loaded = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_36__.loadPromptOverrides)();
105969
105576
  setOverrides(loaded);
105970
105577
  setSelectedApproaches((0,_promptOverrides__WEBPACK_IMPORTED_MODULE_36__.getSelectedPairApproaches)());
105578
+ setImageApproachCounts((0,_promptOverrides__WEBPACK_IMPORTED_MODULE_36__.getImageApproachCounts)());
105971
105579
  setHasChanges(false);
105972
105580
  }
105973
105581
  }, [open]);
@@ -105992,6 +105600,17 @@ function PromptManagerDialog({ open, onClose }) {
105992
105600
  return next;
105993
105601
  });
105994
105602
  };
105603
+ const handleImageApproachCountChange = (idx, count) => {
105604
+ const clamped = Math.max(0, Math.min(4, count));
105605
+ setImageApproachCounts(prev => {
105606
+ const next = [...prev];
105607
+ if (idx >= 0 && idx < next.length)
105608
+ next[idx] = clamped;
105609
+ setOverrides(o => ({ ...o, imageApproachCounts: next }));
105610
+ setHasChanges(true);
105611
+ return next;
105612
+ });
105613
+ };
105995
105614
  const handleToggleOverride = (promptName, enabled) => {
105996
105615
  setOverrides(prev => {
105997
105616
  const prevOverride = prev[promptName];
@@ -106169,14 +105788,14 @@ function PromptManagerDialog({ open, onClose }) {
106169
105788
  hasChanges && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_7__["default"], { label: "\u0415\u0441\u0442\u044C \u043D\u0435\u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043D\u044B\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F", color: "warning", size: "small" })))),
106170
105789
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], null,
106171
105790
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { value: tabValue, onChange: handleTabChange, sx: { borderBottom: 1, borderColor: 'divider' } },
106172
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { label: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0438 \u0438 \u0442\u0435\u043A\u0441\u0442\u044B" }),
105791
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { label: "\u041F\u043E\u0434\u0445\u043E\u0434\u044B" }),
106173
105792
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { label: "\u0418\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F" }),
106174
105793
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { label: "\u0412\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u044F" }),
106175
105794
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { label: "\u041B\u0435\u043D\u0434\u0438\u043D\u0433" })),
106176
105795
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(TabPanel, { value: tabValue, index: 0 },
106177
105796
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_1__["default"], { sx: { mb: 3, p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 } },
106178
105797
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { direction: "row", spacing: 2, alignItems: "center", sx: { mb: 1.5 } },
106179
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_3__["default"], { variant: "subtitle1", sx: { fontWeight: 600 } }, "\u041F\u043E\u0434\u0445\u043E\u0434\u044B \u0434\u043B\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u043F\u0430\u0440"),
105798
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_3__["default"], { variant: "subtitle1", sx: { fontWeight: 600 } }, "\u041F\u043E\u0434\u0445\u043E\u0434\u044B \u0434\u043B\u044F \u0442\u0435\u043A\u0441\u0442\u043E\u0432 (\u043F\u0430\u0440\u044B \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A + \u0442\u0435\u043A\u0441\u0442)"),
106180
105799
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_7__["default"], { label: `Выбрано: ${selectedApproaches.length}`, color: "primary", size: "small" }),
106181
105800
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_3__["default"], { variant: "caption", color: "text.secondary" }, "(\u043C\u0438\u043D\u0438\u043C\u0443\u043C 2, \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C 10)")),
106182
105801
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_1__["default"], { sx: { overflowX: 'auto' } },
@@ -106204,11 +105823,34 @@ function PromptManagerDialog({ open, onClose }) {
106204
105823
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", { style: { padding: '4px 8px', color: '#777' } }, p.titleApproach),
106205
105824
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", { style: { padding: '4px 8px', color: '#777' } }, p.textApproach.split('\n')[0])));
106206
105825
  }))))),
105826
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_1__["default"], { sx: { mb: 3, p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 } },
105827
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { direction: "row", spacing: 2, alignItems: "center", sx: { mb: 1.5 } },
105828
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_3__["default"], { variant: "subtitle1", sx: { fontWeight: 600 } }, "\u041F\u043E\u0434\u0445\u043E\u0434\u044B \u0434\u043B\u044F \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439"),
105829
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_7__["default"], { label: `Всего: ${imageApproachCounts.reduce((a, b) => a + b, 0)}`, color: "primary", size: "small" }),
105830
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_3__["default"], { variant: "caption", color: "text.secondary" }, "(0\u20134 \u043D\u0430 \u043A\u0430\u0436\u0434\u044B\u0439 \u043F\u043E\u0434\u0445\u043E\u0434)")),
105831
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_1__["default"], { sx: { overflowX: 'auto' } },
105832
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("table", { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12 } },
105833
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("thead", null,
105834
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tr", null,
105835
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("th", { style: { textAlign: 'center', padding: '4px 8px', borderBottom: '1px solid #ccc', width: 70 } }, "\u041A\u043E\u043B-\u0432\u043E"),
105836
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("th", { style: { textAlign: 'left', padding: '4px 8px', borderBottom: '1px solid #ccc', whiteSpace: 'nowrap', width: 180 } }, "\u041F\u043E\u0434\u0445\u043E\u0434"),
105837
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("th", { style: { textAlign: 'left', padding: '4px 8px', borderBottom: '1px solid #ccc' } }, "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435"))),
105838
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tbody", null, _prompts__WEBPACK_IMPORTED_MODULE_34__.CREO_APPROACHES.map((approach, i) => {
105839
+ const count = imageApproachCounts[i] ?? 0;
105840
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tr", { key: i, style: { opacity: count > 0 ? 1 : 0.6 } },
105841
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", { style: { textAlign: 'center', padding: '2px 8px', verticalAlign: 'middle' } },
105842
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { type: "number", size: "small", value: count, onChange: (e) => {
105843
+ const v = parseInt(e.target.value, 10);
105844
+ handleImageApproachCountChange(i, isNaN(v) ? 0 : v);
105845
+ }, inputProps: { min: 0, max: 4, step: 1 }, sx: { width: 56, '& .MuiInputBase-input': { textAlign: 'center', py: 0.5 } } })),
105846
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", { style: { padding: '4px 8px', fontWeight: 500, whiteSpace: 'nowrap' } }, approach.name),
105847
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", { style: { padding: '4px 8px', color: '#777' } }, approach.prompt.split('\n')[0])));
105848
+ }))))),
106207
105849
  renderPromptEditor('getPairsSystemPrompt', 'Системный промпт (пары заголовок + текст)', 'Единый системный промпт, генерирующий все пары одним запросом. Переменные: ${geo}, ${count}, ${n}'),
106208
105850
  renderPromptEditor('getPairsUserPrompt', 'Пользовательский промпт (пары)', 'Пользовательский промпт для генерации пар. Переменные: ${product}, ${geo}, ${additionalInfo}, ${count}, ${n}')),
106209
105851
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(TabPanel, { value: tabValue, index: 1 },
106210
105852
  renderPromptEditor('getImageGenerationBasePrompt', 'Базовый промпт для изображений', 'Базовый промпт, используемый для всех подходов'),
106211
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_3__["default"], { variant: "h6", sx: { mt: 3, mb: 2 } }, "\u041F\u043E\u0434\u0445\u043E\u0434\u044B \u0434\u043B\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u043A\u0440\u0435\u0430\u0442\u0438\u0432\u043E\u0432"),
105853
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_3__["default"], { variant: "h6", sx: { mt: 3, mb: 2 } }, "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u043F\u043E\u0434\u0445\u043E\u0434\u043E\u0432 \u0434\u043B\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u043A\u0440\u0435\u0430\u0442\u0438\u0432\u043E\u0432"),
106212
105854
  _prompts__WEBPACK_IMPORTED_MODULE_34__.CREO_APPROACHES.map((approach) => {
106213
105855
  const override = overrides.creoApproaches?.[approach.name];
106214
105856
  const enabled = override?.enabled || false;
@@ -106478,6 +106120,7 @@ const MODELS = {
106478
106120
  __webpack_require__.r(__webpack_exports__);
106479
106121
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
106480
106122
  /* harmony export */ getCreoApproachOverride: () => (/* binding */ getCreoApproachOverride),
106123
+ /* harmony export */ getImageApproachCounts: () => (/* binding */ getImageApproachCounts),
106481
106124
  /* harmony export */ getPairsCount: () => (/* binding */ getPairsCount),
106482
106125
  /* harmony export */ getPromptOverride: () => (/* binding */ getPromptOverride),
106483
106126
  /* harmony export */ getSelectedPairApproaches: () => (/* binding */ getSelectedPairApproaches),
@@ -106525,6 +106168,33 @@ function getSelectedPairApproaches() {
106525
106168
  function getPairsCount() {
106526
106169
  return getSelectedPairApproaches().length;
106527
106170
  }
106171
+ /** Количество подходов для изображений. Должно совпадать с CREO_APPROACHES.length в prompts.ts */
106172
+ const IMAGE_APPROACH_COUNT = 10;
106173
+ /**
106174
+ * Получить количество изображений по каждому подходу (N элементов, 0–4).
106175
+ * По умолчанию — по 1 на каждый подход.
106176
+ */
106177
+ function getImageApproachCounts() {
106178
+ const overrides = loadPromptOverrides();
106179
+ const defaultCounts = Array(IMAGE_APPROACH_COUNT).fill(1);
106180
+ if (overrides.imageApproachCounts && overrides.imageApproachCounts.length > 0) {
106181
+ const stored = overrides.imageApproachCounts.map(c => Math.max(0, Math.min(4, c)));
106182
+ // backward compat: если было 8, дополняем новыми подходами
106183
+ while (stored.length < IMAGE_APPROACH_COUNT)
106184
+ stored.push(1);
106185
+ return stored.slice(0, IMAGE_APPROACH_COUNT);
106186
+ }
106187
+ // backward compat: selectedImageApproaches -> counts (1 for selected, 0 for not)
106188
+ if (overrides.selectedImageApproaches && overrides.selectedImageApproaches.length >= 1) {
106189
+ const counts = Array(IMAGE_APPROACH_COUNT).fill(0);
106190
+ for (const i of overrides.selectedImageApproaches) {
106191
+ if (i >= 0 && i < IMAGE_APPROACH_COUNT)
106192
+ counts[i] = 1;
106193
+ }
106194
+ return counts;
106195
+ }
106196
+ return defaultCounts;
106197
+ }
106528
106198
  /**
106529
106199
  * Загрузить оверрайды из localStorage
106530
106200
  */
@@ -106687,7 +106357,8 @@ __webpack_require__.r(__webpack_exports__);
106687
106357
  /* harmony export */ getTextsSystemPrompt: () => (/* binding */ getTextsSystemPrompt),
106688
106358
  /* harmony export */ getTitlesSystemPrompt: () => (/* binding */ getTitlesSystemPrompt),
106689
106359
  /* harmony export */ getUserPrompt: () => (/* binding */ getUserPrompt),
106690
- /* harmony export */ getValidationPrompt: () => (/* binding */ getValidationPrompt)
106360
+ /* harmony export */ getValidationPrompt: () => (/* binding */ getValidationPrompt),
106361
+ /* harmony export */ pickRandomBullets: () => (/* binding */ pickRandomBullets)
106691
106362
  /* harmony export */ });
106692
106363
  /* harmony import */ var _promptOverrides__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./promptOverrides */ "./src/promptOverrides.ts");
106693
106364
  /**
@@ -106695,6 +106366,56 @@ __webpack_require__.r(__webpack_exports__);
106695
106366
  * Все промпты на русском языке для консистентности
106696
106367
  */
106697
106368
 
106369
+ /** Пул шаблонов буллетов по категориям (на русском — модель переводит на язык GEO) */
106370
+ const BULLET_BY_CATEGORY = {
106371
+ action: [
106372
+ 'Снижает дискомфорт', 'Снижает вздутие', 'Снижает боль', 'Быстрое облегчение',
106373
+ 'Комфорт каждый день', 'Лёгкость движений', 'Меньше симптомов', 'Без боли',
106374
+ 'Дни без дискомфорта', 'Спокойный желудок', 'Спокойные ночи', 'Видимый результат'
106375
+ ],
106376
+ timeframe: [
106377
+ 'Эффект за 7–14 дней', 'Эффект за 2 недели', 'Быстрый результат', 'Облегчение с 1 дня',
106378
+ 'Эффект за 7 дней', 'В 3 раза быстрее', 'С первого дня', 'В первые дни'
106379
+ ],
106380
+ socialProof: [
106381
+ '9 из 10 рекомендуют', '9 из 10 мужчин рекомендуют', '93% подтверждают',
106382
+ '8 из 10 довольны', 'Тысячи довольных'
106383
+ ],
106384
+ formula: [
106385
+ 'Натуральная формула', 'Натуральные ингредиенты', 'Без химии', 'Отборный состав',
106386
+ 'Дерматологически протестировано'
106387
+ ],
106388
+ qualityOfLife: [
106389
+ 'Активный комфорт', 'Дни без забот', 'Больше энергии', 'Здоровый сон', 'Лёгкая жизнь'
106390
+ ]
106391
+ };
106392
+ /** Какие категории для какого подхода (по 1 из каждой категории, порядок случайный) */
106393
+ const APPROACH_BULLET_CATEGORIES = {
106394
+ 'Эксперт / Авторитет': ['action', 'timeframe', 'socialProof'],
106395
+ 'Lifestyle / Момент приёма': ['timeframe', 'socialProof', 'action'], // срок + цифра + действие
106396
+ 'Эмоция / Портрет': ['qualityOfLife', 'action', 'timeframe'], // качество жизни + действие + срок
106397
+ 'Визуализация проблемы': ['action', 'timeframe', 'socialProof'],
106398
+ 'Power / Сила решения': ['action', 'timeframe', 'socialProof'], // действие + скорость + цифра
106399
+ 'Минимализм / Clean Big Text': ['action', 'timeframe', 'formula'],
106400
+ 'Врач / Эксперт': ['action', 'timeframe', 'socialProof']
106401
+ };
106402
+ function pickOne(arr) {
106403
+ return arr[Math.floor(Math.random() * arr.length)];
106404
+ }
106405
+ function shuffle(arr) {
106406
+ return [...arr].sort(() => Math.random() - 0.5);
106407
+ }
106408
+ /** Выбрать 3 случайных буллета по правилам подхода (1 из каждой категории, порядок случайный) */
106409
+ function pickRandomBullets(approachName) {
106410
+ const categories = APPROACH_BULLET_CATEGORIES[approachName];
106411
+ if (!categories || categories.length < 3) {
106412
+ // fallback: все категории
106413
+ const all = Object.values(BULLET_BY_CATEGORY).flat();
106414
+ return shuffle(all).slice(0, 3);
106415
+ }
106416
+ const bullets = categories.map(cat => pickOne(BULLET_BY_CATEGORY[cat] || []));
106417
+ return shuffle(bullets);
106418
+ }
106698
106419
  // Debug logging — пишет в терминал Electron (или в console как fallback)
106699
106420
  function _debugLog(...args) {
106700
106421
  const api = typeof window !== 'undefined' ? window.electronAPI : null;
@@ -106826,16 +106547,13 @@ function getTitlesSystemPrompt(geo, noOverride, count = 3) {
106826
106547
  - НЕ упоминай название продукта в заголовке — оно уже на креативе. Заголовок = только боль + решение/триггер
106827
106548
 
106828
106549
  Технические требования:
106829
- - Каждый заголовок должен предпочтительно быть до 55 символов. При необходимости для разнообразия смысла допускается до 60 символов
106830
- - Каждый заголовок максимум 6-7 слов. Если длиннее сократи, сохраняя смысл
106550
+ - Каждый заголовок может быть 1–4 строки. Допускается до 12 слов (при многострочности)
106551
+ - Каждый заголовок предпочтительно до 55 символов в строке. При необходимости для разнообразия допускается до 60 символов в строке
106831
106552
  - ЗАПРЕЩЕНО использовать двоеточие (:) в заголовках. Используй тире (—) или переформулируй заголовок без двоеточия. Если в заголовке есть двоеточие — это критическая ошибка, перепиши заголовок
106832
106553
  - Используй 1-2 эмодзи естественно внутри заголовка, не только в конце. Эмодзи должны заменять или визуально отмечать смысл (эмоция, действие, предупреждение), а не украшать или заполнять пространство. Максимум 1-2 на строку
106833
106554
  - Избегай эмодзи ⚠️ и 🚨 — они ассоциируются с опасностью. Для привлечения внимания используй 👉, ✅, 🌿, ⏳
106834
- - КРИТИЧНО: Каждый заголовок должен содержать хотя бы одно ясное ключевое слово проблемы, релевантное категории продукта (например, для суставов: боль/скованность/подвижность; для пищеварения: дискомфорт/тяжесть; для сна: бессонница/усталость примеры только для формата, адаптируй под категорию). Если ключевое слово проблемы отсутствует — перепиши заголовок
106555
+ - КРИТИЧНО: Каждый заголовок ОБЯЗАН содержать ключевое слово проблемы ЯВНО. Примеры по категориям: простата простатит, простата; похудение лишний вес, похудение; суставы боль в суставах, колени, скованность; пищеварение дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Если ключевое слово отсутствует — перепиши заголовок
106835
106556
  - Используй знания лучших практик рекламного копирайтинга
106836
- - Избегай только жёстких медицинских гарантий («лечит», «вылечивает», «гарантируем»). Мягкие обещания результата разрешены («Nopți fără treziri», «Stomac liniștit», «Efect rapid»). Разрешены: сроки («în 14 zile»), цифры соц. доказательства («9 din 10 bărbați»), мягкие результаты. Запрещены: «100% гарантия», «навсегда», «излечение».
106837
- - Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
106838
- - Если в категории продукта есть специфические медицинские термины или диагнозы — используй их осторожно, не в каждом заголовке. Максимум ${maxMedTerms} заголовок из ${n} может содержать прямое упоминание таких терминов. Предпочитай мягкие формулировки, описывающие симптомы и дискомфорт, а не диагнозы. Если все ${n} заголовков содержат прямые медицинские термины — перепиши все кроме ${maxMedTerms} с мягкими альтернативами, фокусируясь на ощущениях и симптомах
106839
106557
  - После создания проверь их правильность и разнообразие
106840
106558
  - Верни только заголовки, по одному на строку, без нумерации или буллетов
106841
106559
  - Заголовки должны быть на языке ${geo}
@@ -106863,8 +106581,8 @@ function getTextsSystemPrompt(geo, noOverride, count = 3) {
106863
106581
  .map((p, i) => `- Текст ${i + 1} → [${p.name}]. ${p.textApproach}`)
106864
106582
  .join('\n');
106865
106583
  const deadlineNote = n >= 2
106866
- ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO). Формулируй как ожидание/видимый результат, НЕ как гарантию. НЕЛЬЗЯ использовать «гарантированно/100%/навсегда».`
106867
- : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO). Формулируй как ожидание/видимый результат.`;
106584
+ ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO).`
106585
+ : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO).`;
106868
106586
  return `Ты — эксперт-копирайтер, специализирующийся на рекламе в Meta Ads (Facebook/Instagram). Твоя задача — создать ровно ${n} идеальных продающих рекламных текста для Meta Ads на языке ${geo}.
106869
106587
 
106870
106588
  ВАЖНО — СИСТЕМА ПАРЫ (заголовок + текст):
@@ -106894,24 +106612,18 @@ ${deadlineNote}
106894
106612
  - Избегай академических, синтетических или звучащих как ИИ слов и фраз
106895
106613
  - Избегай искусственного или неестественного формулирования, которое реальные рекламодатели не использовали бы
106896
106614
  - Если фраза звучит синтетически или академически — перепиши её
106897
- - Избегай медицинской терминологии, которую обычный человек не использует в разговоре («secreția biliară», «diureza», «peristaltică»). Пиши так, как человек описал бы проблему другу
106898
-
106899
106615
  Требования к содержанию:
106900
106616
  - КРИТИЧНО: Держи каждый текст кратким — цель примерно 150-280 символов на текст. Тексты в стиле отзыва могут использовать до 300 символов при необходимости для естественного повествования. Будь эффектным, но коротким
106901
106617
  - Если текст длиннее 280 символов (или 300 для стиля отзыва) — сожми и перепиши, сохраняя смысл
106902
106618
  - КРИТИЧНО: Каждый текст должен содержать хотя бы одно ясное ключевое слово проблемы, релевантное категории продукта (например, для суставов: боль/скованность/подвижность; для пищеварения: дискомфорт/тяжесть; для сна: бессонница/усталость — примеры только для формата, адаптируй под категорию). Если ключевое слово проблемы отсутствует — перепиши текст
106903
106619
  - КРИТИЧНО: Каждый текст должен заканчиваться сильным и явным призывом к действию (кликни, попробуй сейчас, закажи сегодня, узнай больше и т.д.)
106904
106620
  - Избегай слабых окончаний или информационного тона. Слабые или нейтральные окончания не допускаются
106905
- - CTA должен быть “нажимным”: допускаются формулировки «не откладывай», «последний шанс», «только сегодня», «забери -50%» (всё строго на языке GEO). Но избегай слов «гарантированно», «вылечит», «100%», «навсегда»
106621
+ - CTA должен быть “нажимным”: допускаются формулировки «не откладывай», «последний шанс», «только сегодня», «забери -50%» (всё строго на языке GEO)
106906
106622
  - В каждом тексте строго следуй заданному формату старта согласно его подходу (см. распределение выше)
106907
106623
  - Если ингредиенты предоставлены в дополнительной информации, упоминай их ТОЛЬКО если они естественно подходят выбранному рекламному подходу. Например: подходы авторитета/экспертизы выигрывают от деталей ингредиентов, в то время как подходы срочности/эмоционального триггера могут не нуждаться в них. Упоминай только релевантные ингредиенты, не обязательно все из них. Если ингредиенты не подходят подходу или не предоставлены — полностью пропусти их упоминание и не изобретай и не упоминай никакие ингредиенты
106908
106624
  - Добавляй только существенные поддерживающие детали, которые усиливают убеждение или ясность, а не заполнители или общие утверждения. Будь кратким и прямым
106909
106625
  - Используй 1-4 эмодзи естественно внутри текста, не только в конце. Эмодзи должны заменять или визуально отмечать смысл (эмоция, действие, предупреждение), а не украшать или заполнять пространство. Максимум 1-2 на строку
106910
106626
  - Избегай эмодзи ⚠️ и 🚨 — они ассоциируются с опасностью. Для привлечения внимания используй 👉, ✅, 🌿, ⏳
106911
- - Разрешены формулировки: «reduce frecvența», «elimină disconfortul», «rezultat vizibil în X zile», «9 din 10 observă diferența». Запрещены только: «лечит/vindecă», «гарантируем 100%», «навсегда/definitiv».
106912
- - Ты можешь усиливать эффект и результаты, но без прямых медицинских обещаний
106913
- - Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
106914
- - Если в категории продукта есть специфические медицинские термины или диагнозы — используй их осторожно, не в каждом тексте. Максимум ${maxMedTerms} текст из ${n} может содержать прямое упоминание таких терминов. Остальные тексты фокусируй на симптомах, дискомфорте и ощущениях пользователя, релевантных категории продукта. Избегай эвфемизмов и искусственных замен — используй естественные формулировки
106915
106627
  - Избегай прямых упоминаний интимных или деликатных деталей, связанных с проблемой. Используй мягкие формулировки, описывающие дискомфорт и симптомы, а не физиологические процессы
106916
106628
  - Избегай повторения полного названия продукта более 2 раз в одном тексте. Используй местоимения или более короткие ссылки при необходимости
106917
106629
 
@@ -106974,8 +106686,8 @@ function getPairsSystemPrompt(geo, noOverride, count = 3, selectedIndices) {
106974
106686
  .map((p, i) => `Пара ${i + 1} → [${p.name}]:\n Заголовок: ${p.titleApproach}\n Текст: ${p.textApproach}`)
106975
106687
  .join('\n');
106976
106688
  const deadlineNote = n >= 2
106977
- ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}). Формулируй как ожидание/видимый результат, НЕ как гарантию.`
106978
- : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}). Формулируй как ожидание/видимый результат.`;
106689
+ ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}).`
106690
+ : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}).`;
106979
106691
  return `Ты — эксперт-копирайтер, специализирующийся на рекламе в Meta Ads (Facebook/Instagram). Создай ровно ${n} пар «заголовок + текст» для рекламы на языке ${geo}.
106980
106692
 
106981
106693
  ФОРМАТ ОТВЕТА (строго соблюдай, без лишних символов):
@@ -107001,7 +106713,6 @@ ${distributionLines}
107001
106713
  - 1–2 эмодзи естественно внутри (не только в конце); избегай ⚠️ и 🚨
107002
106714
  - Хотя бы одно ключевое слово проблемы, релевантное продукту
107003
106715
  - НЕ упоминай название продукта — заголовок = боль + триггер
107004
- - Максимум ${maxMedTerms} из ${n} заголовков могут содержать прямые медицинские термины
107005
106716
  - Звучит как живая рекламная фраза, а не строка ключевых слов
107006
106717
 
107007
106718
  ТРЕБОВАНИЯ К ТЕКСТУ:
@@ -107010,7 +106721,6 @@ ${distributionLines}
107010
106721
  - Сильный явный CTA в конце («кликни», «попробуй сейчас», «закажи», «не жди»)
107011
106722
  - Короткие рубленые фразы, императивы, FOMO допустим${hasTestimonial ? '\n- Testimonial: строго от первого лица, начинается с «Имя, возраст:», конкретный результат с таймингом' : ''}
107012
106723
  ${deadlineNote}
107013
- - Избегай «гарантированно», «вылечит», «100%», «навсегда»
107014
106724
 
107015
106725
  ОБЩИЕ ТРЕБОВАНИЯ:
107016
106726
  - Язык: ${geo} — все тексты строго на этом языке
@@ -107084,6 +106794,7 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
107084
106794
  // Resolve no-bullets conditions in code — don't leave "if approach = X" for the LLM
107085
106795
  const noBulletsApproachNames = CREO_APPROACHES.filter(a => a.noBullets).map(a => a.name);
107086
106796
  const isNoBullets = noBulletsApproachNames.includes(approachName?.trim() ?? '');
106797
+ const isScreenshotReviews = (approachName?.trim() ?? '') === 'Скрин отзывов';
107087
106798
  // ШАГ 0 — hint for BULLETS depends on approach
107088
106799
  const step0BulletsHint = isNoBullets
107089
106800
  ? `BULLETS: [] (для данного подхода буллиты запрещены — ожидается 0)`
@@ -107096,36 +106807,37 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
107096
106807
  : `ШАГ 2 — BULLETS (критично):
107097
106808
  - РОВНО 3 буллета. Если буллетов не 3 — ОШИБКА.
107098
106809
  - Каждый буллет должен быть читабельным (хорошо читается на телефоне, не микрошрифт)
107099
- - Каждый буллет: 2–4 слова, без запятых
106810
+ - Буллеты: крупные, с иконками/галочками (✓), на контрастных подложках. Буллеты НЕ на упаковке/банке.
107100
106811
  - Формат бенефита РАЗРЕШЁН: «Чувствуешь себя легче», «Больше энергии», «Без дискомфорта», «Лёгкость движений» — это бенефиты, НЕ предложения
107101
- - ПРЕДЛОЖЕНИЕ (ошибка) = полная грамматическая конструкция с подлежащим + сказуемым + дополнением, например «Продукт быстро снижает дискомфорт». Короткие бенефиты из 2-4 слов — не считать предложениями
106812
+ - ПРЕДЛОЖЕНИЕ (ошибка) = полная грамматическая конструкция с подлежащим + сказуемым + дополнением, например «Продукт быстро снижает дискомфорт». Короткие бенефиты — не считать предложениями
107102
106813
  - Буллиты с глаголом во 2-м лице («Чувствуешь...», «Получаешь...») или безличные («Больше энергии», «Без дискомфорта») — НЕ ошибка
107103
- - Буллеты = свойства, характеристики ИЛИ мягкие бенефиты. Разрешено: «Reduce disconfortul», «Mai puține treziri», «Efect în 14 zile», цифры соц. доказательства. Запрещено: жёсткие гарантии («лечит», «вылечивает», «гарантируем», «навсегда»).
107104
- - Буллеты не на упаковке
107105
- - Буллиты должны быть расположены вертикально (столбиком). Если буллиты идут горизонтально в одну строку ОШИБКА: плохая читаемость на мобиле`;
107106
- // ШАГ 3 — TEXT LIMIT: PVP has no badges allowed, normal allows urgency/trust
107107
- const step3TextLimit = isNoBullets
107108
- ? `ШАГ 3 — TEXT LIMIT (критично):
107109
- - Общее число слов в HOOK/HEADLINE <= 5. Если большеошибка.
107110
- - OTHER_TEXT должен быть ПУСТЫМ (0 элементов). Никаких дополнительных бейджей, подписей, ярлыков, trust/urgency — НИЧЕГО.
107111
- - Любой дополнительный текст (бейджи/подписи/пояснения/urgency/trust) ОШИБКА: слишком много текста для punch‑креатива.
106814
+ - Буллеты = свойства, характеристики ИЛИ бенефиты. Разрешено: «Reduce disconfortul», «Mai puține treziri», «Efect în 14 zile», цифры соц. доказательства.
106815
+ - Буллиты должны быть расположены вертикально (столбиком). Если буллиты идут горизонтально в одну строку — ОШИБКА: плохая читаемость на мобиле
106816
+ - Цена визуально ОТЛИЧНА от буллитов (цена компактный блок без иконок; буллетыс иконками/галочками). Если цена выглядит как буллет — ОШИБКА.`;
106817
+ // ШАГ 3 — TEXT LIMIT: PVP has no badges allowed, normal allows urgency/trust (без проверок по длине/количеству слов)
106818
+ const step3TextLimit = isScreenshotReviews
106819
+ ? `ШАГ 3 — OTHER_TEXT (критично):
106820
+ - Для «Скрин отзывов» блок отзывов (аватарки, имена, возраст, 5 звёзд, текст отзывов) это НЕ ошибка, это основной контент.
106821
+ - Допускаются также trust‑печати (0–3 шт). URGENCY не рекомендуется, но не ошибка.
106822
+ - На креативе допустимы: HOOK, блок отзывов, цена, скидка, CTA, опционально trust‑печати.`
106823
+ : isNoBullets
106824
+ ? `ШАГ 3 — OTHER_TEXT (критично):
106825
+ - OTHER_TEXT должен быть ПУСТЫМ (0 элементов). Никаких дополнительных бейджей, подписей, ярлыков, trust‑печатей/urgency — НИЧЕГО.
106826
+ - Любой дополнительный текст (бейджи/подписи/пояснения/urgency/trust‑печати) — ОШИБКА: слишком много текста для punch‑креатива.
107112
106827
  - На креативе допустимы ТОЛЬКО: HOOK, цена, скидка, кнопка CTA.`
107113
- : `ШАГ 3 — TEXT LIMIT (критично):
107114
- - Общее число слов в HOOK/HEADLINE + BULLETS <= 16. Если больше — ошибка.
106828
+ : `ШАГ 3 — OTHER_TEXT (критично):
107115
106829
  - По умолчанию любой другой рекламный текст (бейджи/подписи/пояснения) запрещён.
107116
- - НО допускаются (не обязательно) дополнительные маленькие бейджи ВНЕ упаковки — это НЕ ошибка, если ВСЕ элементы OTHER_TEXT подпадают под разрешённые типы:
107117
- A) URGENCY (0–1 шт): короткий бейдж срочности/дефицита/лимита времени.
107118
- B) TRUST (0–3 шт): бейджи доверия/сервиса (качество/доставка/поддержка/проверено/рекомендуют).
106830
+ - НО допускаются (не обязательно) дополнительные бейджи ВНЕ упаковки — это НЕ ошибка, если ВСЕ элементы OTHER_TEXT подпадают под разрешённые типы:
106831
+ A) URGENCY (0–1 шт): бейдж срочности/дефицита — только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» и подобные — непонятно что означает.
106832
+ B) TRUST (0–3 шт, для «Минимализм / Clean Big Text» — макс 1): печати цветные, яркие, очень похожие на печать FDA. Подчёркивают: натуральность состава, премиальность, безопасность. Допустимо: качество, натуральные ингредиенты, экологичность, контроль качества. Печати — размер как минимум как у буллитов, крупные. Даже одна печать — крупная.
107119
106833
  Правила для каждого элемента OTHER_TEXT:
107120
- * короткий: 1–3 слова ИЛИ число+слово (например «24h»)
107121
106834
  * строго на языке ${geo} (без английских слов)
107122
106835
  * без цены/скидки/процентов (кроме обязательного поля DISCOUNT), без доменов/ссылок
107123
- * без диагнозов и без жёстких медицинских обещаний
107124
- * TRUST‑бейджи должны быть про доверие/сервис (качество/доставка/поддержка/проверено/рекомендуют), не про «лечит/результаты». Запрещено: «рекомендовано врачами», «клинически доказано», «одобрено Минздравом» и любые фразы, которые требуют доказательств/авторитетов
106836
+ * TRUST‑печати про натуральность/премиальность/безопасность (не про доставку/поддержку)
107125
106837
  Примеры стиля (адаптируй к языку GEO, не требуются):
107126
- - URGENCY: «Ostatnie sztuki», «Tylko dziś», «Koniec dziś», «Tylko 24h»
107127
- - TRUST: «Wysoka jakość», «Szybka dostawa», «Wsparcie klienta», «Sprawdzone», «Polecane»
107128
- - Если OTHER_TEXT содержит что-то вне этих типов, или URGENCY > 1, или TRUST > 3 — ОШИБКА.`;
106838
+ - URGENCY: «Ostatnie sztuki», «Tylko dziś», «Koniec dziś» (НЕ «24h» — непонятно)
106839
+ - TRUST: «Naturalna formuła», «Wysoka jakość» (для PL); «Ingrediente naturale», «Calitate» (для RO). ЗАПРЕЩЕНО: «NATURAL», «QUALITY», «100% NATURAL» — английские слова.
106840
+ - Если OTHER_TEXT содержит что-то вне этих типов, или URGENCY > 1, или TRUST > 3 (для Минимализма TRUST > 1) — ОШИБКА.`;
107129
106841
  return `Ты — СТРОГИЙ валидатор рекламных креативов (1:1). Не улучшай и не предлагай идеи — только проверка и вердикт.
107130
106842
  Продукт: ${product}. GEO/язык: ${geo}.${approachLine}
107131
106843
 
@@ -107142,35 +106854,29 @@ DISCOUNT: "..." (если нет — "нет")
107142
106854
  CTA: "..." (если нет — "нет")
107143
106855
  OTHER_TEXT: ["..."] (любой другой рекламный текст на макете ВНЕ упаковки и ВНЕ элементов выше; название/бренд на упаковке НЕ включай)
107144
106856
  (если можешь, для ясности — не обязательно):
107145
- URGENCY_BADGE: "..." (короткий бейдж срочности, если есть)
107146
- TRUST_BADGES: ["..."] (бейджи доверия/сервиса, если есть)
106857
+ URGENCY_BADGE: "..." (бейдж срочности, если есть; «24h» — ОШИБКА, непонятно что означает)
106858
+ TRUST_BADGES: ["..."] (печати цветные, яркие, в стиле FDA; про натуральность/премиальность/безопасность; размер как у буллитов — крупные, если есть)
107147
106859
 
107148
106860
  ШАГ 1 — HOOK/HEADLINE (критично):
107149
- - Должен быть в верхнем блоке и читабелен (хорошо читается на телефоне)
107150
- - РОВНО 35 слов
107151
- - Допускается 1–2 строки (это НЕ ошибка). Ошибка только если текст нечитабелен или есть разрыв/перенос внутри слова.
107152
- - Смысл: релевантно проблеме/выгоде продукта. Допускаются и боль/дискомфорт, и обещание/выгода (например «Könnyebb napok»). НЕ считай ошибкой, если по смыслу это обещание результата/выгоды.
107153
- - Мягкие обещания результата («Nopți liniștite», «Stomac fără griji») — не ошибка. Ошибка только если есть медицинская гарантия или диагноз.
107154
- - Желательно: ключевое слово боли/проблемы в формулировке (как часть смысла), но отсутствие ключевого слова само по себе НЕ является ошибкой. Примеры для ориентира: ${keywords.join(', ')} (можно синонимы).
106861
+ - Должен быть ВЫШЕ всех элементов и читабелен (хорошо читается на телефоне)
106862
+ - Допускается 14 строки (это НЕ ошибка). Ошибка только если текст нечитабелен или есть разрыв/перенос внутри слова.
106863
+ - Смысл: релевантно проблеме/выгоде продукта. Допускаются и боль/дискомфорт, и обещание/выгода (например «Könnyebb napok»). НЕ считай ошибкой формулировки обещаний результата или клеймы.
106864
+ - ОБЯЗАТЕЛЬНО: ключевое слово проблемы явно в формулировке. Примеры по категориям: простата простатит, простата; похудение лишний вес, похудение; суставы → боль, колени, скованность; пищеварение → дискомфорт, вздутие; сон → бессонница, усталость. Отсутствие ключевого слова — ОШИБКА. Дополнительно для ориентира: ${keywords.join(', ')} (можно синонимы).
107155
106865
  - Проверь вторую часть заголовка. Если она состоит только из абстрактных слов («Mai ușor», «Mai bine», «Soluția», «Ajutor») без конкретного бенефита — отметь: РЕКОМЕНДАЦИЯ: заголовок можно усилить конкретикой
107156
106866
  - Если заголовок слишком общий/абстрактный («Mai ușor», «Mai bine» без конкретики) — отметь как РЕКОМЕНДАЦИЯ к улучшению (не ошибка, но слабо)
107157
106867
  - РЕКОМЕНДАЦИЯ (CTR): верхний текст лучше делать как HOOK — ОЧЕНЬ крупный, жирный, ВСЕ БУКВЫ ПРОПИСНЫЕ, на яркой контрастной плашке. Если это не так — не ошибка, но отметь рекомендацией
107158
106868
 
107159
106869
  ${step2Bullets}
107160
106870
 
107161
- ШАГ 2.5 — CLAIMS CHECK (критично):
107162
- - Мягкие клеймы РАЗРЕШЕНЫ: «помогает», «поддерживает», «способствует», «может снизить», «выводит токсины», «устраняет дискомфорт», «очищает организм», сроки как ожидание («через 2 недели», «за 7-14 дней»), цифры без гарантий («9 из 10 рекомендуют»)
107163
- - ЗАПРЕЩЕНЫ только: «лечит», «вылечивает», «гарантируем», «100%», «навсегда», «полностью избавляет», диагностика заболеваний
107164
- - «Выводит/устраняет токсины» для детокс-продуктов — это мягкий клейм, НЕ медицинская гарантия
107165
- - Если найден запрещённый клейм — ошибка. Мягкие клеймы — не ошибка.
107166
- - Специфические медицинские термины или диагнозы — разрешены, но не должны повторяться чрезмерно. Если в HOOK/HEADLINE + BULLETS один и тот же специфический термин встречается более 1 раза — РЕКОМЕНДАЦИЯ: разнообразить формулировки, использовать синонимы или фокус на симптомах вместо повторения термина
106871
+ ШАГ 2.5 — CLAIMS CHECK:
106872
+ - Клеймы по результату (жёсткие/абсолютные или мягкие) НЕ проверяются и НЕ запрещаются. Не блокируй за формулировки обещаний результата.
107167
106873
 
107168
106874
  ${step3TextLimit}
107169
106875
 
107170
106876
  ШАГ 4 — CTA > PRICE (критично):
107171
- - CTA: 1–2 слова на языке ${geo}, хорошо заметная кнопка (позиция НЕ важна: можно вправо/влево/по центру). Главное — CTA читабельна и выглядит как кнопка.
107172
- - Цена видна и читабельна
107173
- - Скидка НЕ обязательна. Если скидка есть, она должна быть "-50%" (возможны варианты символа минуса: -, или −), не на упаковке
106877
+ - CTA: на языке ${geo}, хорошо заметная кнопка (позиция НЕ важна: можно вправо/влево/по центру). Главное — CTA читабельна и выглядит как кнопка.
106878
+ - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (до скидки) + новая. Если только одна цена — ОШИБКА. Старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Тонкое/незаметное зачёркивание — ОШИБКА.
106879
+ - Скидка ОБЯЗАТЕЛЬНА: «-50%» отдельным видимым бейджем (процент должен быть явно читаем, не только в цене). Ярко, заметно. Строго НЕ на упаковке/банке.
107174
106880
  - Не считать ошибкой, если скидка/цена конкурируют с CTA по акценту — это допустимо, пока CTA остаётся хорошо заметной и читабельной.
107175
106881
  - Если скидка указана и она не равна -50% — ошибка.
107176
106882
 
@@ -107181,13 +106887,18 @@ ${step3TextLimit}
107181
106887
  ШАГ 6 — КОМПОЗИЦИЯ:
107182
106888
  - Если креатив без человека (lifestyle/clean), проверь наличие контекста использования (кухня, стол, тумбочка, и т.д.). Продукт на полностью пустом/белом фоне без контекста — РЕКОМЕНДАЦИЯ к улучшению (не критичная ошибка, но слабый визуал)
107183
106889
  - КОНТРАСТ ПЛАШЕК: если хотя бы одна текстовая плашка (HOOK, буллеты, CTA или цена) визуально сливается с фоном и текст плохо читается — это ОШИБКА.
106890
+ - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА. Цена не должна выглядеть как буллет.
106891
+ - TRUST‑печати: если печати мелкие или текст нечитабелен на телефоне — ОШИБКА (размер как минимум как у буллитов).
106892
+ - СКИДКА «-50%»: ОБЯЗАТЕЛЬНО отдельным видимым бейджем (процент явно читаем). Если скидка не отображается или только в цене без отдельного «-50%» — ОШИБКА. Ярко, заметно, НЕ на упаковке.
106893
+ - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА.
106894
+ - Бейджи срочности: «24h» и подобные неясные формулировки — ОШИБКА (непонятно что означает). Ясные («только сегодня», «последние штуки») — ок.
107184
106895
 
107185
106896
  ФИНАЛ:
107186
106897
  Выведи строго:
107187
106898
  СТАТУС: OK / НУЖНА ПЕРЕСБОРКА
107188
106899
  Затем список ошибок, каждая строка начинается с "ОШИБКА:" (кратко, по делу). Если ошибок нет — напиши "ОШИБКА: нет".
107189
106900
  Затем список рекомендаций (если есть), каждая строка начинается с "РЕКОМЕНДАЦИЯ:" (кратко, по делу). Если рекомендаций нет — напиши "РЕКОМЕНДАЦИЯ: нет".
107190
- Не блокируй за мягкие бенефиты, сроки или цифры — только за жёсткие гарантии и медицинские клеймы.`;
106901
+ Не блокируй за клеймы по результату или формулировки обещаний.`;
107191
106902
  }
107192
106903
  /**
107193
106904
  * Базовый промпт для генерации изображений креативов
@@ -107205,10 +106916,12 @@ function getImageGenerationBasePrompt(generateGeo, generatePrice, generateCurren
107205
106916
  }
107206
106917
  _debugLog('⚠️ Using DEFAULT getImageGenerationBasePrompt');
107207
106918
  }
107208
- // Подходы без буллитов — вычисляем динамически из массива
107209
- const noBulletsNames = CREO_APPROACHES.filter(a => a.noBullets).map(a => a.name);
106919
+ // Подходы без буллитов — из подходов с count > 0
106920
+ const counts = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_0__.getImageApproachCounts)();
106921
+ const selectedApproaches = CREO_APPROACHES.filter((_, i) => counts[i] > 0);
106922
+ const noBulletsNames = selectedApproaches.filter(a => a.noBullets).map(a => a.name);
107210
106923
  const noBulletsCond = noBulletsNames.map(n => `"${n}"`).join(' или ');
107211
- const totalApproaches = CREO_APPROACHES.length;
106924
+ const totalApproaches = counts.reduce((a, b) => a + b, 0);
107212
106925
  const ratio = aspectRatio || '1:1';
107213
106926
  const ratioLabel = ratio === '1:1' ? '1:1 (квадрат, 1024×1024 px)' : '2:3 (портрет, 1024×1536 px)';
107214
106927
  const ratioShape = ratio === '1:1' ? 'квадратным' : 'вертикальным 2:3 (портрет)';
@@ -107236,20 +106949,24 @@ CRITICAL RULES (это ВАЛИДАЦИЯ, не рекомендации; есл
107236
106949
  - Цена/скидка = триггер "сейчас"
107237
106950
  - CTA = финальный шаг к действию
107238
106951
 
106952
+ ❗ ЦЕНА (КРИТИЧНО — НЕ ПРОПУСКАЙ):
106953
+ - ОБЯЗАТЕЛЬНО ДВЕ цены: старая (до скидки) + новая (${generatePrice} ${generateCurrency}). Одна цена = ОШИБКА.
106954
+ - Старая = 2× новой (при -50%). Новая — выразительно, крупно. Старая — зачёркнута.
106955
+ - Зачёркивание: ТОЛСТАЯ линия (не тонкая!), контрастная, хорошо видимая. Тонкая/незаметная линия = ОШИБКА.
106956
+
107239
106957
  ЯЗЫК + ТОН:
107240
- - ВСЕ слова строго на языке ${generateGeo} (HOOK/буллеты/CTA/скидка/опциональный бейдж срочности/опциональные trust‑бейджи). Английские слова запрещены. Исключений нет.
107241
- - Тон: убедительный, но честный. Лёгкая эмоция и срочность — ок. Запрещено: ложные обещания, диагностика заболеваний, слова «лечит/вылечивает/гарантируем».
106958
+ - ВСЕ слова строго на языке ${generateGeo} (HOOK/буллеты/CTA/скидка/опциональный бейдж срочности/опциональные trust‑печати). Английские слова запрещены. Исключений нет.
106959
+ - Тон: убедительный, но честный. Лёгкая эмоция и срочность — ок.
107242
106960
 
107243
106961
  HOOK / HEADLINE (строгое правило):
107244
- - ровно 35 слов
107245
- - строго 1 строка; без переноса/разбиения слов; без подзаголовка
107246
- - заголовок должен описывать боль пользователя на языке ${generateGeo}; ключевое слово проблемы часть боли (не отдельный ярлык/бейдж)
106962
+ - 14 строки; до 12 слов. Без переноса внутри слова
106963
+ - ОБЯЗАТЕЛЬНО содержит ключевое слово проблемы ЯВНО. Примеры по категориям: простата → простатит, простата; похудение → лишний вес, похудение; суставы → боль в суставах, колени, скованность; пищеварение → дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Без ключевого слова — ОШИБКА
106964
+ - HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). Самый крупный текстовый элемент на креативе
107247
106965
  - HOOK должен быть ВИЗУАЛЬНО “тяжёлым”: ОЧЕНЬ крупный, ТОЛСТЫЙ/жирный шрифт, ВСЕ БУКВЫ ПРОПИСНЫЕ (CAPS), на яркой контрастной плашке/подложке. Это верхний главный “thumb‑stop” элемент
107248
- - Можно (не обязательно) включить СРОК прямо в HOOK (например «7 dni», «14 dni») как триггер, но без жёсткой гарантии. Лучше формулировать как ожидание/вопрос (например с «?»), не как медицинское обещание
107249
- - Заголовок должен быть РЕЗКИМ и призывным: короткие рубленые фразы, вопросы и предупреждения допустимы. Можно использовать обращение на «ты» (в языке GEO: «Masz…», «Twój…») и вопросительный знак — но без диагнозов и без жёстких медицинских гарантий
107250
- - заголовок может содержать мягкое обещание облегчения, но не медицинскую гарантию. ПРАВИЛЬНО: «Nopți fără treziri? Posibil». НЕ ТАК: «Vindecă prostata definitiv».
106966
+ - Можно (не обязательно) включить СРОК прямо в HOOK (например «7 dni», «14 dni») как триггер
106967
+ - Заголовок должен быть РЕЗКИМ и призывным: короткие рубленые фразы, вопросы и предупреждения допустимы. Можно использовать обращение на «ты» (в языке GEO: «Masz…», «Twój…») и вопросительный знак
107251
106968
  - заголовок НЕ должен быть абстрактным слоганом/лозунгом или метафорой без конкретной боли. Допускается «thumb‑stop» стиль (вызов/вопрос/предупреждение), если это конкретно и релевантно категории
107252
- - если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт.
106969
+ - если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт. HOOK всегда остаётся выше всех остальных элементов
107253
106970
  - Не повторяй один и тот же заголовок в разных подходах: для текущего подхода придумай новый, уникальный заголовок. Варьируй: угол боли, формулировку, акцент (проблема vs облегчение)
107254
106971
  - Вторая часть заголовка = конкретное улучшение, не абстрактное слово. ПРАВИЛЬНО: «Zile fără dureri», «Mișcare ușoară», «Confort activ» (для суставов); «Zile fără disconfort», «Nopți liniștite» (для сна) — адаптируй под категорию. НЕ ТАК: «Mai ușor», «Mai bine», «Soluția»
107255
106972
  - Специфические медицинские термины или диагнозы разрешены в заголовке, но не обязательны. Варьируй между прямыми формулировками (если релевантно категории) и мягкими формулировками, фокусирующимися на симптомах и дискомфорте, для разных креативов. Не используй один и тот же термин во всех креативах
@@ -107258,73 +106975,89 @@ HOOK / HEADLINE (строгое правило):
107258
106975
  - ТАК (пищеварение): «Puffadás gond? Könnyebb napok»
107259
106976
  - ТАК (суставы): «Ízületi fájdalom? Mozogj könnyebben»
107260
106977
  - ТАК (сон): «Álmatlan éjszakák? Pihenj végre»
107261
- Если заголовок > 5 слов, похож на предложение, распадается на 2 смысловых блока или звучит как лозунг — ОШИБКА → пересоздай вариант.
106978
+ - ТАК (простата): «Prostată? Alinare rapidă»
106979
+ - ТАК (похудение): «Greutate în plus? Fără diete extreme»
106980
+ ❗ Если заголовок > 12 слов, похож на длинное предложение или звучит как лозунг без ключевого слова проблемы — ОШИБКА → пересоздай вариант.
107262
106981
 
107263
- BULLETS (жёсткое правило):
106982
+ BULLETS (жёсткое правило, ровно 3):
107264
106983
  - По умолчанию: ровно 3 буллета
107265
106984
  - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллиты ЗАПРЕЩЕНЫ (0 буллитов). Не добавляй буллет‑блок вообще
107266
106985
  - каждый 2–3 слова (макс 4); без запятых; буллет НЕ должен выглядеть как предложение
107267
- - буллеты = свойства ИЛИ ощущаемые преимущества (комфорт, лёгкость, спокойствие). Запрещено: диагнозы, жёсткие медицинские гарантии («лечит», «вылечивает», «избавляет навсегда»). Разрешено: мягкие результаты («Reduce disconfortul», «Mai puține simptome») примеры формата, адаптируй под категорию, сроки без гарантий («Efect în 7-14 zile», «Rezultat vizibil rapid»), цифры как соц. доказательство («9 din 10 bărbați recomandă»).
107268
- - Делай буллеты более активными и конкретными: глаголы действия + сроки + цифры. Примеры формата: «Reduce X», «Ulga od 1. dnia», «Efekt w 7 dni», «93% potwierdza», «3× szybciej» (адаптируй под язык GEO и категорию)
106986
+ - ВИЗУАЛ БУЛЛЕТОВ: крупные, с иконками/галочками (), на контрастных подложках. Буллеты ВНЕ упаковки/банкине на продукте.
107269
106987
  - Буллиты должны быть расположены ВЕРТИКАЛЬНО (столбиком), не горизонтально в одну строку. Минимальный размер шрифта буллитов — чтобы читались на экране телефона без зума
107270
- - Не повторяй один и тот же набор буллитов во всех подходах: для текущего подхода используй свой набор и акценты. Варианты: свойства (Formulă naturală), бенефиты (Reduce disconfortul), сроки (Efect în 7-14 zile), соц. доказательство (9 din 10 recomandă)
107271
- ПРАВИЛЬНО (пример формата адаптируй под категорию продукта): «Confort zilnic», «Fără dureri», «Reduce disconfortul», «Mișcare ușoară», «Efect în 2 săptămâni».
107272
- ЗАПРЕЩЕНО: «Vindecă prostata definitiv», «Garantat în 3 zile», «Лечит простату».
106988
+
106989
+ ВЫБОР БУЛЛЕТОВ (КРИТИЧНО): Выбери СЛУЧАЙНЫЕ 3 буллета из пула ниже в ПРОИЗВОЛЬНОМ порядке. Адаптируй формулировки под язык ${generateGeo} и категорию продукта. НЕ используй один и тот же набор для разных подходов каждый креатив должен иметь УНИКАЛЬНУЮ комбинацию из 3 буллетов.
106990
+
106991
+ ПУЛ ВАРИАНТОВ (выбери 3 случайных, порядок произвольный; адаптируй под GEO и категорию):
106992
+ • Действие/бенефит: Reduce disconfortul, Reduce balonarea, Reduce durerea, Ulga rapidă, Confort zilnic, Mișcare ușoară, Mai puține simptome, Fără dureri, Zile fără disconfort, Stomac liniștit, Nopți liniștite, Rezultat vizibil
106993
+ • Срок/скорость: Efect în 7-14 zile, Efect în 2 săptămâni, Rezultat rapid, Ulga od 1. dnia, Efekt w 7 dni, 3× szybciej, De la prima zi, În primele zile
106994
+ • Соц. доказательство: 9 din 10 recomandă, 9 din 10 bărbați recomandă, 93% potwierdza, 8 din 10 mulțumiți, Mii de clienți mulțumiți
106995
+ • Формула/состав: Formulă naturală, Ingrediente naturale, Fără chimicale, Compoziție selectată, Testat dermatologic
106996
+ • Качество жизни: Confort activ, Zile fără griji, Mai multă energie, Somn odihnitor, Viață mai ușoară
106997
+
107273
106998
  ❗ Если хоть один буллет > 4 слов или похож на предложение/обещание — ОШИБКА → пересоздай.
107274
106999
 
107275
107000
  TEXT LIMIT:
107276
107001
  - кроме: HOOK, 3 буллетов, цены, скидки, кнопки — ЛЮБОЙ другой текст запрещён (ярлыки/подписи/дисклеймеры/пояснения)
107277
- - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → кроме HOOK, цены, скидки, CTA — НИКАКОГО другого текста. Буллиты запрещены. Other_text/urgency/trust-бейджи запрещены
107278
- - ДОПУСКАЕТСЯ (не обязательно) ОДИН небольшой бейдж срочности/дефицита/лимита времени: 1–3 слова ИЛИ число+слово (например «24h»), строго на языке ${generateGeo}, без цены/скидки/процентов, без доменов/ссылок, без диагнозов и жёстких обещаний. Бейдж должен быть в углу кадра (не в центре), контрастный, но заметно меньше CTA и не конкурирует с CTA по вниманию
107279
- - ДОПУСКАЕТСЯ (не обязательно) 1–3 TRUST‑бейджа (печати/стампы доверия) на языке ${generateGeo}: качество/доставка/поддержка/проверено/рекомендуют. Каждый бейдж короткий (1–3 слова), читабельный на телефоне, без английских слов, без цен/скидок/процентов, без доменов/ссылок. Размещай их компактно (обычно в нижней части/по низу), меньше CTA и не конкурируют с CTA. Запрещено: «рекомендовано врачами», «клинически доказано», «одобрено Минздравом» и любые фразы, требующие доказательств
107280
- - HOOK + буллеты <= 16 слов суммарно; если больше — ОШИБКА → пересоздай
107281
- - Если хочешь добавить ощущение срочности — делай это через формулировки в HOOK/BULLETS, через реквизит/иконки, ИЛИ (не обязательно) через один короткий текстовый бейдж срочности. Примеры стиля (адаптируй к языку GEO, не обязаны): «Ostatnie sztuki», «Tylko dziś», «Koniec dziś», «Tylko 24h»
107002
+ - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → кроме HOOK, цены, скидки, CTA — НИКАКОГО другого текста. Буллиты запрещены. Other_text/urgency/trust‑печати запрещены
107003
+ - ДОПУСКАЕТСЯ (не обязательно) ОДИН бейдж срочности: только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» непонятно что означает.
107004
+ - ДОПУСКАЕТСЯ (не обязательно) 1–3 trust‑печати: печати цветные, яркие, в стиле FDA. Текст СТРОГО на языке ${generateGeo} запрещены английские слова («NATURAL», «QUALITY» и т.д.). Примеры для RO: «naturale», «Ingrediente naturale»; для PL: «naturalna». Размер как минимум как у буллитов. Мелкие печати ОШИБКА.
107005
+ - HOOK + буллеты <= 24 слова суммарно (HOOK до 12, буллеты до 12); если больше — ОШИБКА → пересоздай
107006
+ - Если хочешь добавить ощущение срочности — делай это через формулировки в HOOK/BULLETS, через реквизит/иконки, ИЛИ (не обязательно) через один короткий бейдж срочности. Только ясные формулировки («Ostatnie sztuki», «Tylko dziś», «Koniec dziś»). ЗАПРЕЩЕНО «24h» — непонятно что означает.
107282
107007
 
107283
107008
  CTA > PRICE:
107284
107009
  - кнопка CTA (1–2 слова на языке ${generateGeo}) — главный якорь нижнего блока (самый контрастный элемент внизу)
107285
107010
  - цена и скидка видны, но слабее CTA; скидка «-50%» ОБЯЗАТЕЛЬНА и не должна конкурировать с кнопкой
107011
+ - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА. Цена не должна выглядеть как буллет.
107286
107012
  ❗ Если цена/скидка/бейдж по контрасту или размеру равны или сильнее кнопки — ОШИБКА → ослабь цену/скидку или усили CTA и пересоздай.
107287
107013
  ❗ ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → цена и «-50%» могут быть более крупными и заметными, но CTA всё равно должен оставаться очень заметным и выглядеть как кнопка (не теряется на фоне)
107288
107014
 
107289
107015
  ПОКАЗЫВАЙ ТОЛЬКО ЭТИ ТЕКСТОВЫЕ ЭЛЕМЕНТЫ:
107290
- - HOOK (1 строка; CAPS; жирный; на яркой контрастной подложке)
107291
- - 3 буллета
107292
- - Цена: ${generatePrice} ${generateCurrency} (обязательно, не на упаковке)
107293
- - Скидка: -50% (обязательно, отдельным бейджем, не на упаковке; визуально слабее CTA)
107016
+ - HOOK (1–4 строки; выше всех элементов; самый крупный; CAPS; жирный; на яркой контрастной подложке; ключевое слово проблемы явно)
107017
+ - 3 буллета (крупные, с иконками/галочками ✓, на контрастных подложках, НЕ на банке/упаковке)
107018
+ - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (2×${generatePrice} ${generateCurrency}) зачёркнута ТОЛСТОЙ контрастной линией + новая ${generatePrice} ${generateCurrency} выразительно. Одна цена = ОШИБКА. Тонкая линия зачёркивания = ОШИБКА. Без слов. Не на упаковке.
107019
+ - Скидка: «-50 ОБЯЗАТЕЛЬНО отдельным видимым бейджем (не только в цене!). Процент скидки должен быть явно читаем. Ярко, заметно, строго НЕ на банке/упаковке, визуально слабее CTA
107294
107020
  - Кнопка CTA (1-2 слова)
107295
- - Опционально: 1 короткий бейдж срочности (1–3 слова ИЛИ число+слово, строго на языке ${generateGeo}; без цены/скидки/процентов). Бейдж в углу кадра (не в центре), контрастный, но меньше CTA. НЕ обязателен
107296
- - Опционально: 1–3 TRUST‑бейджа (печати доверия) на языке ${generateGeo} (качество/доставка/поддержка), короткие и читабельные. НЕ обязательны
107297
- - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → показывай только: HOOK + PRICE + DISCOUNT + CTA. Буллиты/urgency/trust запрещены
107021
+ - Опционально: 1 короткий бейдж срочности (ясные формулировки: «только сегодня», «последние штуки»; ЗАПРЕЩЕНО «24h»). Бейдж в углу кадра, контрастный, но меньше CTA. НЕ обязателен
107022
+ - Опционально: 1–3 trust‑печати (цветные, яркие, в стиле FDA; натуральность, премиальность, безопасность). Текст СТРОГО на языке ${generateGeo} никакого английского. Для RO: «naturale», «Ingrediente naturale», «Calitate»; для PL: «naturalna», «naturalne»; НЕ «NATURAL», «QUALITY». Размер как минимум как у буллитов. НЕ обязательны
107023
+ - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → показывай только: HOOK + PRICE + DISCOUNT + CTA. Буллиты/urgency/trust‑печати запрещены
107298
107024
 
107299
107025
  ВИЗУАЛ:
107300
- - Иерархия: HOOK > продукт > CTA > цена > буллеты
107026
+ - Иерархия: HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). HOOK — самый крупный текстовый элемент
107027
+ - Буллеты: крупные, с иконками/галочками (✓), на контрастных подложках. Строго ВНЕ упаковки/банки.
107028
+ - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание = ОШИБКА. Визуально ОТЛИЧНА от буллитов.
107029
+ - Скидка «-50%»: ярко, заметно, на контрастной подложке. Строго ВНЕ упаковки/банки.
107301
107030
  - Текст на контрастных плашках, читабельно на телефоне. Допускается мультяшная/комиксовая стилистика текста (не обязательно)
107302
107031
  - КОНТРАСТ ПЛАШЕК: каждая текстовая плашка (HOOK, буллеты, CTA, цена) должна иметь высокий контраст с фоном — светлый текст на тёмной подложке ИЛИ тёмный текст на светлой. Плашка, сливающаяся с фоном = ОШИБКА → добавь подложку или измени цвет фона/плашки.
107303
107032
  - АНГЛИЙСКИЙ НА УПАКОВКЕ: если на этикетке продукта есть английский текст (например название/описание на упаковке) — не приближай её настолько, чтобы эти надписи были читабельны как отдельный контент. Упаковка = визуальный объект; мелкий этикеточный текст должен быть нечитабелен или едва различим.
107304
107033
  - Без ссылок/доменов и мелкого текста
107305
107034
  - LIFESTYLE/CLEAN: продукт в контексте использования. Для пищеварения — кухня, обеденный стол, рядом с едой/чашкой. Для сна/простаты — спальня, тумбочка, стакан воды. Для суставов — стол/тумбочка рядом с эластичным бинтом, ортезом или активным фоном (кроссовки, лестница). Контекст = момент приёма. Просто продукт на белом фоне — НЕ допускается
107306
107035
  - Цвета и композиция должны быть «thumb‑stop»: высокий контраст, яркий акцент на продукте (луч света/подсветка/обводка/глоу), избегай бледных пастельных сцен
107307
- - ЛЮДИ: человек допускается ТОЛЬКО для подхода «Эмоция / Портрет». Для всех остальных подходов — БЕЗ человека.
107308
- - Если есть человек (только в «Эмоция / Портрет»): прямой взгляд в камеру (как обращение лично к зрителю), эмоция облегчения/надежды (без стоковой улыбки). Возраст человека ДОЛЖЕН соответствовать категории и целевой аудитории продукта: по умолчанию 50–60, но для категорий, где аудитория обычно моложе (например похудение/фитнес) допускается 35–55. Этнически соответствует GEO/рынку
107036
+ - ЛЮДИ: человек допускается ТОЛЬКО для подходов «Эмоция / Портрет» и «Врач / Эксперт». Для всех остальных — БЕЗ человека.
107037
+ - Если «Эмоция / Портрет»: прямой взгляд в камеру, эмоция облегчения/надежды. Возраст 50–60 (похудение/фитнес 35–55). Этнически соответствует GEO/рынку.
107038
+ - Если «Врач / Эксперт»: женщина-врач 40–55 лет. Внешность и этничность СТРОГО соответствуют GEO/рынку (локал — врач выглядит как местный специалист).
107309
107039
 
107310
107040
  ANTI-TEMPLATE DIVERSITY (КРИТИЧНО):
107311
107041
  - Ты сейчас создаёшь ОДИН креатив для текущего подхода. В проекте будет серия из ${totalApproaches} креативов, поэтому у каждого подхода должна быть своя узнаваемая композиция.
107312
107042
  - Для ЭТОГО креатива НЕ используй универсальный шаблон. Обязательно вариируй: (1) крупность/кроп (close-up vs medium vs flat-lay), (2) угол камеры (прямо vs сверху vs диагональ), (3) расположение блоков HOOK/BULLETS/CTA/PRICE/DISCOUNT, (4) свет/фон (но без лишнего текста).
107313
107043
  - Ориентируйся на строку «🎯 ПОДХОД: ...» и примени соответствующую раскладку ниже (только одну, соответствующую текущему подходу):
107314
- * 🎯 ПОДХОД: Эксперт / Авторитет → БЕЗ человека. Профессиональный контекст с реквизитом экспертизы вокруг продукта. Продукт в центре. HOOK сверху справа, BULLETS справа ниже, CTA снизу справа, PRICE/DISCOUNT рядом с CTA (слабее CTA). Trust‑бейджи (если есть) — компактно по низу.
107315
- * 🎯 ПОДХОД: Lifestyle / Момент приёма → FLAT-LAY/СВЕРХУ или 3/4 сверху на столе/тумбочке. Продукт в центре, реквизит "момент приёма" вокруг. HOOK сверху по центру, BULLETS сбоку (вертикально, шрифт достаточно крупный для чтения на телефоне без зума), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Urgency‑бейдж (если есть) — в углу.
107316
- * 🎯 ПОДХОД: Эмоция / Портрет → ЭТО ЕДИНСТВЕННЫЙ ПОДХОД С ЧЕЛОВЕКОМ В СЕРИИ. ОЧЕНЬ крупный портрет лица (thumb‑stop), взгляд в камеру — лицо занимает верхние 2/3 кадра и доминирует. Продукт в руке или у подбородка. HOOK сверху по центру. BULLETS, CTA, PRICE/DISCOUNT — строго в нижней трети, НЕ перекрывают лицо и не конкурируют с ним по размеру.
107317
- * 🎯 ПОДХОД: Визуализация проблемыИнфографика/схема: слева проблема (иконка/схема зоны тела, релевантной продукту), справа решение + продукт. HOOK сверху по центру, BULLETS справа или снизу (вертикально), CTA снизу справа, PRICE/DISCOUNT возле CTA. Красный акцент только на проблеме.
107318
- * 🎯 ПОДХОД: Power / Сила решения БЕЗ человека. ДИНАМИЧНЫЙ комикс‑кадр, диагональная композиция. Продукт как “герой” + power‑иконки (щит/молния/бёрст). HOOK сверху слева на яркой плашке, BULLETS слева ниже, CTA снизу справа, PRICE/DISCOUNT возле CTA.
107319
- * 🎯 ПОДХОД: Минимализм / Clean Big Text Белый/градиентный фон, минимум элементов. ОГРОМНЫЙ HOOK занимает верх/центр, продукт крупно (центр/право), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Trust‑бейджи (если есть) — очень маленькие и аккуратные.
107320
- * 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов колено; для пищеварения желудок), с красным свечением. Продукт крупно рядом. HOOK 3–4 слова МАКС, огромный CAPS. БЕЗ буллитов. PRICE + «-50%» крупно и заметно. CTA контрастной кнопкой
107321
- * 🎯 ПОДХОД: Любительский Примитивизм БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху крупный, грубый bold/рукописный, CAPS, 3–4 слова. Крупные надписи цены/скидки вокруг продукта. CTA плоская кнопка без градиентов. БЕЗ буллитов, бейджей, теней, иконок.
107044
+ * 🎯 ПОДХОД: Эксперт / Авторитет → БЕЗ человека. Профессиональный контекст с реквизитом экспертизы вокруг продукта. Продукт в центре. HOOK сверху справа, BULLETS справа ниже, CTA снизу справа, PRICE/DISCOUNT рядом с CTA (слабее CTA). Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
107045
+ * 🎯 ПОДХОД: Lifestyle / Момент приёма → FLAT-LAY/СВЕРХУ или 3/4 сверху на столе/тумбочке. Продукт в центре, реквизит "момент приёма" вокруг. HOOK сверху по центру, BULLETS сбоку (вертикально, шрифт достаточно крупный для чтения на телефоне без зума), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Urgency‑бейдж (если есть) — в углу, только ясные формулировки, НЕ «24h». Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
107046
+ * 🎯 ПОДХОД: Эмоция / Портрет → Один из двух подходов с человеком. ОЧЕНЬ крупный портрет лица (thumb‑stop), взгляд в камеру — лицо занимает верхние 2/3 кадра. Продукт в руке или у подбородка. HOOK сверху по центру. BULLETS, CTA, PRICE/DISCOUNT, trust‑печати — в нижней трети.
107047
+ * 🎯 ПОДХОД: Врач / Эксперт Женщина-врач (локал по GEO), продукт в руках или рядом. Профессиональный фон. HOOK сверху, BULLETS сбоку/снизу, CTA и PRICE/DISCOUNT внизу. Врач и продукт главные элементы.
107048
+ * 🎯 ПОДХОД: Скрин отзывов Имитация скриншота: блок отзывов (аватарки, имена, возраст, 5 звёзд, текст на языке GEO). HOOK ОБЯЗАТЕЛЕН выше отзывов или поверх, крупно. Продукт виден. CTA, PRICE, DISCOUNT внизу.
107049
+ * 🎯 ПОДХОД: Визуализация проблемы Инфографика/схема: слева проблема (иконка/схема зоны тела, релевантной продукту), справа решение + продукт. HOOK сверху по центру, BULLETS справа или снизу (вертикально), CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне). Красный акцент только на проблеме.
107050
+ * 🎯 ПОДХОД: Power / Сила решения → БЕЗ человека. ДИНАМИЧНЫЙ комикс‑кадр, диагональная композиция. Продукт как “герой” + power‑иконки (щит/молния/бёрст). HOOK сверху слева на яркой плашке, BULLETS слева ниже, CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
107051
+ * 🎯 ПОДХОД: Минимализм / Clean Big Text Белый/градиентный фон, минимум элементов. ОГРОМНЫЙ HOOK занимает верх/центр, продукт крупно (центр/право), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Trust‑печати (если есть)размер как минимум как у буллитов, крупные и читабельные.
107052
+ * 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK 1–4 строки, до 12 слов, огромный CAPS, выше всех. БЕЗ буллитов. PRICE + «-50%» крупно и заметно. CTA контрастной кнопкой
107053
+ * 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — 1–4 строки, до 12 слов, крупный, грубый bold/рукописный, CAPS, выше всех. Крупные надписи цены/скидки вокруг продукта. CTA — плоская кнопка без градиентов. БЕЗ буллитов, бейджей, теней, иконок.
107322
107054
 
107323
107055
  AUTO-CHECK (перед финалом):
107324
- - HOOK: 35 слов, 1 строка, CAPS, жирный на контрастной плашке, боль + направление облегчения (не лозунг), язык ${generateGeo} без английских слов
107056
+ - HOOK: 14 строки, до 12 слов, CAPS, жирный на контрастной плашке, выше всех элементов, ключевое слово проблемы явно, язык ${generateGeo} без английских слов
107325
107057
  - Буллеты: по умолчанию ровно 3, каждый <= 4 слов, без запятых, не предложения. ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллетов 0 (запрещены)
107326
- - Нет лишнего текста кроме: HOOK, буллетов, цены, скидки, кнопки (+ опционально 1 короткий бейдж срочности + опционально 1–3 trust‑бейджа)
107058
+ - Нет лишнего текста кроме: HOOK, буллетов, цены, скидки, кнопки (+ опционально 1 бейдж срочности + опционально 1–3 trust‑печати)
107327
107059
  - Есть скидка «-50%» (вне упаковки) и она слабее CTA
107060
+ - Цена: ОБЯЗАТЕЛЬНО ДВЕ (старая + новая). Старая зачёркнута ТОЛСТОЙ контрастной линией. Одна цена = ОШИБКА. Тонкая/незаметная линия зачёркивания = ОШИБКА
107328
107061
  - CTA визуально сильнее цены/скидки
107329
107062
  - Контраст: каждая текстовая плашка читабельна на телефоне — ни одна не сливается с фоном
107330
107063
  Если хоть один пункт нарушен — пересоздай креатив.
@@ -107332,59 +107065,70 @@ AUTO-CHECK (перед финалом):
107332
107065
  ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай, не вызывай tools. Сразу генерируй визуальный креатив как картинку.`;
107333
107066
  }
107334
107067
  /**
107335
- * Получить подходы с учетом оверрайдов
107068
+ * Получить подходы с учетом количества и оверрайдов.
107069
+ * Каждый подход повторяется N раз (0–4), где N = imageApproachCounts[i].
107336
107070
  */
107337
107071
  function getCreoApproaches() {
107338
- return CREO_APPROACHES.map(approach => {
107072
+ const counts = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_0__.getImageApproachCounts)();
107073
+ const result = [];
107074
+ for (let i = 0; i < CREO_APPROACHES.length && i < counts.length; i++) {
107075
+ const approach = CREO_APPROACHES[i];
107076
+ if (!approach)
107077
+ continue;
107339
107078
  const override = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_0__.getCreoApproachOverride)(approach.name);
107340
- if (override?.enabled) {
107341
- _debugLog(`✅ Using CUSTOM approach: ${approach.name}`, `hasPrompt=${!!override.customPrompt}`, `hasHeadline=${!!override.customHeadlineAngle}`, `hasBullets=${!!override.customBulletsFocus}`);
107342
- return {
107079
+ const resolved = override?.enabled
107080
+ ? {
107343
107081
  ...approach,
107344
107082
  prompt: override.customPrompt || approach.prompt,
107345
107083
  headlineAngle: override.customHeadlineAngle || approach.headlineAngle,
107346
107084
  bulletsFocus: override.customBulletsFocus || approach.bulletsFocus
107347
- };
107085
+ }
107086
+ : approach;
107087
+ if (override?.enabled) {
107088
+ _debugLog(`✅ Using CUSTOM approach: ${approach.name}`, `hasPrompt=${!!override.customPrompt}`, `hasHeadline=${!!override.customHeadlineAngle}`, `hasBullets=${!!override.customBulletsFocus}`);
107348
107089
  }
107349
- return approach;
107350
- });
107090
+ for (let k = 0; k < counts[i]; k++) {
107091
+ result.push(resolved);
107092
+ }
107093
+ }
107094
+ return result;
107351
107095
  }
107352
107096
  const CREO_APPROACHES = [
107353
107097
  {
107354
107098
  name: 'Эксперт / Авторитет',
107355
- prompt: `ЭКСПЕРТ / АВТОРИТЕТ (без человека): стиль “рекомендация эксперта”, но БЕЗ человека. Профессиональный контекст — аптечные полки на фоне / медицинский планшет / стетоскоп / рецептурный блокнот как реквизит рядом с продуктом. Продукт в центре как “рекомендованное решение”. Чистый профессиональный фон, высокий контраст. Trust‑бейджи: выбери 1–3 варианта из (качество/проверено/рекомендуют/доставка/поддержка), маленькие и аккуратныеусиливают авторитет.`,
107356
- headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 35 слов, 1 строка, CAPS.`,
107357
- bulletsFocus: `БУЛЛИТЫ: “доверие + действие + конкретика”. Минимум 1 буллет с действием на главную боль (глагол), минимум 1 со сроком/скоростью, плюс 1 с цифрой/соц.доказательством (если уместно). Всё строго на языке GEO и релевантно категории. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры (например «Naturalna formuła» сама по себе).`
107099
+ prompt: `ЭКСПЕРТ / АВТОРИТЕТ (без человека): стиль “рекомендация эксперта”, но БЕЗ человека. Профессиональный контекст — аптечные полки на фоне / медицинский планшет / стетоскоп / рецептурный блокнот как реквизит рядом с продуктом. Продукт в центре как “рекомендованное решение”. Чистый профессиональный фон, высокий контраст. Trust‑печати (1–3 шт): цветные, яркие, очень похожие на печать FDA; подчёркивают натуральность состава, премиальность, безопасность. Размер как минимум как у буллитов крупные, читабельны на телефоне. Даже одна печать — крупная.`,
107100
+ headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107101
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор для этого подхода не повторяй комбинации из других креативов.`
107358
107102
  },
107359
107103
  {
107360
107104
  name: 'Lifestyle / Момент приёма',
107361
- prompt: `LIFESTYLE / МОМЕНТ ПРИЁМА: продукт в контексте использования (кухня/стол/спальня/тумбочка). Без человека, но с "историей" (стакан воды, чай, тарелка, будильник/часы/календарь). Срочность: реквизит (часы/таймер‑иконка) и/или опциональный urgency‑бейдж (1–3 слова) в углу кадра, контрастный, но меньше CTA. Высокий контраст, яркий акцент на продукте. Опционально: 1–3 trust‑бейджа по низу. ВАЖНО: буллиты располагаются вертикально и всегда читабельны на экране телефона без зума — даже если уходят вбок, минимальный размер шрифта буллитов не уменьшается. Если буллиты не вмещаются сбоку с нужным шрифтом — переставь их вниз.`,
107362
- headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 35 слов, 1 строка, CAPS.`,
107363
- bulletsFocus: `БУЛЛИТЫ: активные глаголы + сроки + цифры. Минимум 1 буллет со сроком/скоростью, минимум 1 с цифрой/соц.доказательством. Всё 2–4 слова, без запятых, строго на языке GEO и релевантно категории. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
107105
+ prompt: `LIFESTYLE / МОМЕНТ ПРИЁМА: продукт в контексте использования (кухня/стол/спальня/тумбочка). Без человека, но с "историей" (стакан воды, чай, тарелка, будильник/часы/календарь). Срочность: реквизит (часы/таймер‑иконка) и/или опциональный urgency‑бейдж (1–3 слова) в углу кадра только ясные формулировки («только сегодня», «последние штуки»), ЗАПРЕЩЕНО «24h». Высокий контраст, яркий акцент на продукте. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ВАЖНО: буллиты располагаются вертикально и всегда читабельны на экране телефона без зума — даже если уходят вбок, минимальный размер шрифта буллитов не уменьшается. Если буллиты не вмещаются сбоку с нужным шрифтом — переставь их вниз.`,
107106
+ headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107107
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (срок + цифра + действие/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107364
107108
  },
107365
107109
  {
107366
107110
  name: 'Эмоция / Портрет',
107367
- prompt: `ЭМОЦИЯ / ПОРТРЕТ: ЭТО ЕДИНСТВЕННЫЙ ПОДХОД С ЧЕЛОВЕКОМ В СЕРИИ. Используй максимально: ОЧЕНЬ крупный план лица (thumb‑stop), прямой взгляд в камеру, сильная эмоция облегчения/надежды (без широкой стоковой улыбки). Возраст человека подбери под категорию и ЦА продукта: по умолчанию 50–60, но для категорий с более молодой аудиторией (например похудение/фитнес) допускается 35–55. Продукт в руке на уровне лица или рядом, хорошо виден. Свет "тень → свет" на лице допустим. Минимум отвлекающих деталей, высокий контраст. Опционально: 1–3 trust‑бейджа по низу. ИЕРАРХИЯ: лицо — главный и доминирующий визуальный элемент (верхние 2/3 кадра). Все текстовые блоки (HOOK исключение — сверху) уходят в нижнюю треть. Буллиты, цена, CTA НЕ перекрывают лицо и НЕ конкурируют с ним по визуальному весу — они заметно меньше и ниже.`,
107368
- headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение». 35 слов, 1 строка, CAPS. Тон максимально личный и прямой.`,
107369
- bulletsFocus: `БУЛЛИТЫ: про ощущение и качество жизни, но в формате действий/результата + срок/скорость + (если уместно) цифра. Всё строго на GEO. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
107111
+ prompt: `ЭМОЦИЯ / ПОРТРЕТ: один из двух подходов с человеком в серии. Используй максимально: ОЧЕНЬ крупный план лица (thumb‑stop), прямой взгляд в камеру, сильная эмоция облегчения/надежды (без широкой стоковой улыбки). Возраст человека подбери под категорию и ЦА продукта: по умолчанию 50–60, но для категорий с более молодой аудиторией (например похудение/фитнес) допускается 35–55. Продукт в руке на уровне лица или рядом, хорошо виден. Свет "тень → свет" на лице допустим. Минимум отвлекающих деталей, высокий контраст. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ИЕРАРХИЯ: лицо — главный и доминирующий визуальный элемент (верхние 2/3 кадра). Все текстовые блоки (HOOK исключение — сверху) уходят в нижнюю треть. Буллиты, цена, CTA НЕ перекрывают лицо и НЕ конкурируют с ним по визуальному весу — они заметно меньше и ниже.`,
107112
+ headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение». 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно. Тон максимально личный и прямой.`,
107113
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (качество жизни + действие + срок/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107370
107114
  },
107371
107115
  {
107372
107116
  name: 'Визуализация проблемы',
107373
- prompt: `ВИЗУАЛИЗАЦИЯ ПРОБЛЕМЫ (без человека): схема/иконка проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав/позвоночник; для пищеварения — желудок; для простаты — силуэт мужчины) + мягкий красный акцент на этой зоне (не шок‑контент, без графики). Рядом продукт как решение. Можно добавить простую стрелку/переход “проблема → облегчение” как графику (без лишнего текста). Чистый фон, высокая читабельность. Опционально: 1–3 trust‑бейджа по низу. БЕЗ человека.`,
107374
- headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 35 слов, 1 строка, CAPS, вопросительный знак допустим.`,
107375
- bulletsFocus: `БУЛЛИТЫ: механика/результат в активной форме + срок/скорость + конкретика. Без диагнозов и гарантий. Всё строго на GEO. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
107117
+ prompt: `ВИЗУАЛИЗАЦИЯ ПРОБЛЕМЫ (без человека): схема/иконка проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав/позвоночник; для пищеварения — желудок; для простаты — силуэт мужчины) + мягкий красный акцент на этой зоне (не шок‑контент, без графики). Рядом продукт как решение. Можно добавить простую стрелку/переход “проблема → облегчение” как графику (без лишнего текста). Чистый фон, высокая читабельность. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека.`,
107118
+ headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно, вопросительный знак допустим.`,
107119
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107376
107120
  },
107377
107121
  {
107378
107122
  name: 'Power / Сила решения',
107379
- prompt: `POWER / СИЛА РЕШЕНИЯ (без человека): метафоры силы и победы над проблемой: щит, молния, энергия, взрыв‑бёрст, мощные стикеры/иконки. Продукт как “герой” в центре, высокая энергия, контрастные агрессивные цвета. Можно комикс/anti‑design подачу, но всё должно быть читабельно. Без лишнего текста. Опционально: 1–3 trust‑бейджа по низу. БЕЗ человека. Только продукт + power‑иконки.`,
107380
- headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 35 слов, 1 строка, CAPS.`,
107381
- bulletsFocus: `БУЛЛИТЫ: активные глаголы + скорость/срок + конкретика + (если уместно) цифра. Без гарантий. Всё строго на GEO. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
107123
+ prompt: `POWER / СИЛА РЕШЕНИЯ (без человека): метафоры силы и победы над проблемой: щит, молния, энергия, взрыв‑бёрст, мощные стикеры/иконки. Продукт как “герой” в центре, высокая энергия, контрастные агрессивные цвета. Можно комикс/anti‑design подачу, но всё должно быть читабельно. Без лишнего текста. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека. Только продукт + power‑иконки.`,
107124
+ headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107125
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + скорость + цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107382
107126
  },
107383
107127
  {
107384
107128
  name: 'Минимализм / Clean Big Text',
107385
- prompt: `МИНИМАЛИЗМ / CLEAN BIG TEXT (без человека): белый или мягкий градиентный фон, премиальное ощущение. ОГРОМНЫЙ HOOK как главный элемент (CAPS, жирный, на контрастной плашке). Продукт крупно, минимум реквизита, минимум шума. Тени/премиальные материалы допустимы, но без лишнего текста. БЕЗ человека. Urgency и trust‑бейджи в этом подходе НЕ рекомендуются — они нарушают чистоту и ощущение премиальности. Добавляй бейджи только если это критически необходимо, не более 1 и только самый маленький.`,
107386
- headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 35 слов, 1 строка, CAPS.`,
107387
- bulletsFocus: `БУЛЛИТЫ: максимально коротко, но конкретно: действие + срок/скорость + цифра/доказательство (если уместно). Никаких абстрактных буллитов без конкретики. Всё строго на GEO.`
107129
+ prompt: `МИНИМАЛИЗМ / CLEAN BIG TEXT (без человека): белый или мягкий градиентный фон, премиальное ощущение. ОГРОМНЫЙ HOOK как главный элемент (CAPS, жирный, на контрастной плашке). Продукт крупно, минимум реквизита, минимум шума. Тени/премиальные материалы допустимы, но без лишнего текста. БЕЗ человека. Urgency и trust‑печати в этом подходе НЕ рекомендуются — они нарушают чистоту и ощущение премиальности. Добавляй печати только если это критически необходимо, не более 1, но крупную размер как у буллитов.`,
107130
+ headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107131
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + формула/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107388
107132
  },
107389
107133
  {
107390
107134
  name: 'Problem Visual Punch',
@@ -107393,14 +107137,14 @@ const CREO_APPROACHES = [
107393
107137
  - Фон: яркий сплошной цвет (красный/оранжевый) или агрессивный градиент
107394
107138
  - Центр: КРУПНАЯ иконка/схема проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав; для простаты — силуэт мужчины; для пищеварения — желудок), с красным свечением
107395
107139
  - Продукт: крупно рядом
107396
- - HOOK: 3–4 слова МАКСИМУМ, огромный, CAPS
107140
+ - HOOK: 1–4 строки, до 12 слов, огромный, CAPS, ключевое слово проблемы явно
107397
107141
  - БЕЗ буллитов
107398
107142
  - Цена + скидка: крупно, заметно
107399
107143
  - CTA: контрастная яркая кнопка
107400
107144
 
107401
107145
  Цель: считывание за 1 секунду. Минимум текста, максимум визуального удара.
107402
- БЕЗ человека. Никаких trust/urgency бейджей — только HOOK + PRICE + -50% + CTA.`,
107403
- headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 3–4 слова, CAPS.`,
107146
+ БЕЗ человека. Никаких trust‑печатей/urgency бейджей — только HOOK + PRICE + -50% + CTA.`,
107147
+ headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107404
107148
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK и крупных надписях.`
107405
107149
  },
107406
107150
  {
@@ -107414,9 +107158,34 @@ const CREO_APPROACHES = [
107414
107158
  - Элементы выглядят «вырезанными и приклеенными»: неровные края, отсутствие выравнивания по сетке
107415
107159
  - Крупные текстовые надписи рядом с продуктом: цена, скидка; допустимо одно короткое слово-восклицание («РАБОТАЕТ!», «ПРОВЕРЕНО!») на языке GEO
107416
107160
  - Кнопка CTA: плоская, без градиентов, контрастный сплошной цвет
107417
- - НИКАКИХ буллитов, печатей, trust-бейджей, urgency-бейджей, иконок — только продукт и текст`,
107418
- headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 3–4 слова, CAPS, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
107161
+ - НИКАКИХ буллитов, trust‑печатей, urgency‑бейджей, иконок — только продукт и текст`,
107162
+ headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
107419
107163
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Всё — в HOOK и крупных надписях вокруг продукта (цена, скидка, одно слово-восклицание).`
107164
+ },
107165
+ {
107166
+ name: 'Врач / Эксперт',
107167
+ prompt: `ВРАЧ / ЭКСПЕРТ (с человеком): женщина-врач как авторитет, рекомендующий продукт. Один из двух подходов с человеком в серии.
107168
+ - Врач: женщина 40–55 лет, профессиональный вид (медицинский халат, планшет/стетоскоп/рецептурный блокнот). Уверенная, располагающая поза.
107169
+ - ЛОКАЛИЗАЦИЯ (КРИТИЧНО): врач НЕ generic — явно угадывается регион/GEO. Черты лица, тип кожи, причёска — характерные для целевого рынка. RO → румынский тип; PL → польский; HU → венгерский; ES → испанский/латино; IT → итальянский. Врач = местный специалист, не стоковое универсальное лицо.
107170
+ - Продукт: в руках врача или на столе/планшете рядом, хорошо виден.
107171
+ - Фон: профессиональный (кабинет, аптечные полки, медицинский контекст). Высокий контраст, читабельность.
107172
+ - Опционально: 1–3 trust‑печати по низу (цветные, яркие, в стиле FDA). Размер как минимум как у буллитов.
107173
+ - ИЕРАРХИЯ: врач + продукт — главные элементы. HOOK сверху, BULLETS сбоку или снизу, CTA и PRICE/DISCOUNT в нижнем блоке.`,
107174
+ headlineAngle: `HOOK: угол «врач/специалист рекомендует» или «эксперты знают». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107175
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
107176
+ },
107177
+ {
107178
+ name: 'Скрин отзывов',
107179
+ noBullets: true,
107180
+ prompt: `СКРИН ОТЗЫВОВ: имитация реалистичного скриншота с положительными благодарными отзывами и оценками 5 звёзд.
107181
+ - Визуал: реалистичный стиль — как скриншот приложения/сайта отзывов (App Store, Google Play, или страница отзывов). Высокое качество, читабельный текст.
107182
+ - Отзывы: 2–4 отзыва с аватарками, именами и возрастом (формат «Имя, 45 лет»), ОБЯЗАТЕЛЬНО 5 звёзд (★★★★★) в каждом, короткий текст благодарности. Текст отзывов СТРОГО на языке GEO — никакого английского. Релевантен категории продукта.
107183
+ - HOOK ОБЯЗАТЕЛЕН: крупно, выше блока отзывов или поверх, CAPS, на яркой подложке. Ключевое слово проблемы явно.
107184
+ - Продукт: виден в кадре (рядом со скрином или интегрирован в композицию).
107185
+ - Цена, скидка «-50%», CTA — в нижней части. БЕЗ буллитов — отзывы заменяют их.
107186
+ - Стиль: не мультяшный, не комикс — максимально реалистичный скрин.`,
107187
+ headlineAngle: `HOOK: угол «тысячи довольны» / «9 из 10 рекомендуют» / «реальный результат». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107188
+ bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK, отзывах (с 5 звёздами), цене и CTA.`
107420
107189
  }
107421
107190
  ];
107422
107191