docs-combiner 0.1.10 → 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
@@ -99622,33 +99622,28 @@ __webpack_require__.r(__webpack_exports__);
99622
99622
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButtonGroup/ToggleButtonGroup.js");
99623
99623
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButton/ToggleButton.js");
99624
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/Dialog/Dialog.js");
99626
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Backdrop/Backdrop.js");
99627
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/DialogTitle/DialogTitle.js");
99628
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/DialogContent/DialogContent.js");
99629
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/DialogActions/DialogActions.js");
99630
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/styles/createTheme.js");
99631
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/styles/ThemeProvider.js");
99632
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/AccountBalance.js");
99633
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/AutoAwesome.js");
99634
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Brightness4.js");
99635
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Brightness7.js");
99636
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/CheckCircle.js");
99637
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/CloudDownload.js");
99638
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/ContentCopy.js");
99639
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/ExpandMore.js");
99640
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/InfoOutlined.js");
99641
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Login.js");
99642
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Logout.js");
99643
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/NoteAdd.js");
99644
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Replay.js");
99645
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Settings.js");
99646
- /* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Visibility.js");
99647
- /* harmony import */ var _PromptManagerDialog__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(/*! ./PromptManagerDialog */ "./src/PromptManagerDialog.tsx");
99648
- /* harmony import */ var _promptOverrides__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(/*! ./promptOverrides */ "./src/promptOverrides.ts");
99649
- /* harmony import */ var xlsx__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(/*! xlsx */ "./node_modules/xlsx/xlsx.mjs");
99650
- /* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(/*! jszip */ "./node_modules/jszip/dist/jszip.min.js");
99651
- /* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_56___default = /*#__PURE__*/__webpack_require__.n(jszip__WEBPACK_IMPORTED_MODULE_56__);
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__);
99652
99647
 
99653
99648
 
99654
99649
 
@@ -99724,7 +99719,6 @@ function App() {
99724
99719
  const [pairTranslations, setPairTranslations] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({});
99725
99720
  const [translatingPairs, setTranslatingPairs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99726
99721
  const [driveFolderUrl, setDriveFolderUrl] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99727
- const [brand, setBrand] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99728
99722
  const [link, setLink] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99729
99723
  const [openaiApiKey, setOpenaiApiKey] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99730
99724
  const [openRouterBalance, setOpenRouterBalance] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
@@ -99876,28 +99870,66 @@ function App() {
99876
99870
  const { price, currency } = parsePriceAndCurrency(value);
99877
99871
  return price.trim() !== '' && currency.trim() !== '';
99878
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]);
99879
99917
  const [generating, setGenerating] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99880
99918
  const [generatingImages, setGeneratingImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99881
99919
  const [imagesGenerationLogs, setImagesGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99882
99920
  const [generatedImagesData, setGeneratedImagesData] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99883
99921
  const [checkingImages, setCheckingImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99884
99922
  const [uploadingImages, setUploadingImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99885
- const [generatingProduct, setGeneratingProduct] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99886
99923
  const [uploadingProduct, setUploadingProduct] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99887
- const [productModalOpen, setProductModalOpen] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99888
- const [productSourceImages, setProductSourceImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99889
- const [productGeneratedImage, setProductGeneratedImage] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99890
- const [productRegeneratePrompt, setProductRegeneratePrompt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
99891
- const [productGenerationLogs, setProductGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99892
99924
  const [folderFilesInfo, setFolderFilesInfo] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99925
+ const productFileInputRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null);
99893
99926
  const [checkingFolderFiles, setCheckingFolderFiles] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99894
99927
  const permissionCheckedFoldersRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(new Set());
99895
99928
  const folderCheckRunningRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(false);
99896
- const [productProcessStartTime, setProductProcessStartTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99897
99929
  const [imagesProcessStartTime, setImagesProcessStartTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99898
99930
  const [contentProcessStartTime, setContentProcessStartTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
99899
99931
  const [contentGenerationLogs, setContentGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
99900
- 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 });
99901
99933
  const [uiNow, setUiNow] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => Date.now());
99902
99934
  const [generatingLanding, setGeneratingLanding] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99903
99935
  const [landingGenerationLogs, setLandingGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
@@ -99964,7 +99996,7 @@ function App() {
99964
99996
  }
99965
99997
  };
99966
99998
  // Create theme based on mode
99967
- const theme = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => (0,_mui_material__WEBPACK_IMPORTED_MODULE_36__["default"])({
99999
+ const theme = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => (0,_mui_material__WEBPACK_IMPORTED_MODULE_31__["default"])({
99968
100000
  palette: {
99969
100001
  mode: darkMode ? 'dark' : 'light',
99970
100002
  ...(darkMode
@@ -100056,7 +100088,7 @@ function App() {
100056
100088
  };
100057
100089
  loadKey();
100058
100090
  // Load prompt overrides from Electron config
100059
- (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_54__.loadOverridesFromElectron)();
100091
+ (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.loadOverridesFromElectron)();
100060
100092
  }, []);
100061
100093
  // Save form fields to localStorage whenever they change
100062
100094
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
@@ -100112,7 +100144,6 @@ function App() {
100112
100144
  setGeneratedImagesData([]);
100113
100145
  setTitles('');
100114
100146
  setTexts(['']);
100115
- setBrand('');
100116
100147
  setLink('');
100117
100148
  return;
100118
100149
  }
@@ -100128,7 +100159,6 @@ function App() {
100128
100159
  setGeneratedImagesData([]);
100129
100160
  setTitles('');
100130
100161
  setTexts(['']);
100131
- setBrand('');
100132
100162
  setLink('');
100133
100163
  return;
100134
100164
  }
@@ -100139,7 +100169,6 @@ function App() {
100139
100169
  setGeneratedImagesData([]);
100140
100170
  setTitles('');
100141
100171
  setTexts(['']);
100142
- setBrand('');
100143
100172
  setLink('');
100144
100173
  setLoadingContentFromDrive(true);
100145
100174
  setLoadingImagesFromDrive(true);
@@ -100179,12 +100208,6 @@ function App() {
100179
100208
  }, [titles]); // Only depend on titles, not generatedTitlesData to avoid loops
100180
100209
  // Auto-scroll logs when new logs are added
100181
100210
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
100182
- if (productGenerationLogs.length > 0 && (generatingProduct || uploadingProduct)) {
100183
- const logContainer = document.getElementById('product-generation-logs');
100184
- if (logContainer) {
100185
- logContainer.scrollTop = logContainer.scrollHeight;
100186
- }
100187
- }
100188
100211
  if (imagesGenerationLogs.length > 0 && (generatingImages || uploadingImages)) {
100189
100212
  const logContainer = document.getElementById('images-generation-logs');
100190
100213
  if (logContainer) {
@@ -100197,30 +100220,7 @@ function App() {
100197
100220
  logContainer.scrollTop = logContainer.scrollHeight;
100198
100221
  }
100199
100222
  }
100200
- }, [productGenerationLogs, generatingProduct, uploadingProduct, imagesGenerationLogs, generatingImages, uploadingImages, contentGenerationLogs, generating]);
100201
- // Timer for product generation
100202
- (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
100203
- let interval = null;
100204
- if (generatingProduct || uploadingProduct) {
100205
- interval = setInterval(() => {
100206
- setElapsedTime(prev => {
100207
- if (productProcessStartTime) {
100208
- const elapsed = Math.floor((Date.now() - productProcessStartTime) / 1000);
100209
- return { ...prev, product: elapsed };
100210
- }
100211
- return prev;
100212
- });
100213
- }, 1000);
100214
- }
100215
- else {
100216
- setProductProcessStartTime(null);
100217
- setElapsedTime(prev => ({ ...prev, product: 0 }));
100218
- }
100219
- return () => {
100220
- if (interval)
100221
- clearInterval(interval);
100222
- };
100223
- }, [generatingProduct, uploadingProduct, productProcessStartTime]);
100223
+ }, [imagesGenerationLogs, generatingImages, uploadingImages, contentGenerationLogs, generating]);
100224
100224
  // Timer for images generation
100225
100225
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
100226
100226
  let interval = null;
@@ -100441,19 +100441,12 @@ function App() {
100441
100441
  return;
100442
100442
  if (response.ok) {
100443
100443
  const data = await response.json();
100444
- const bankaFiles = data.files.filter((f) => {
100445
- const name = f.name?.toLowerCase() || '';
100446
- return name.startsWith('banka') && (name.endsWith('.png') || name.endsWith('.jpg') || name.endsWith('.jpeg'));
100447
- });
100448
100444
  const hasProduct = data.files.some((f) => {
100449
100445
  const name = f.name?.toLowerCase() || '';
100450
100446
  return name === 'product.png' || name === 'product.jpg';
100451
100447
  });
100452
100448
  if (!cancelled) {
100453
- setFolderFilesInfo({
100454
- bankaCount: bankaFiles.length,
100455
- hasProduct
100456
- });
100449
+ setFolderFilesInfo({ hasProduct });
100457
100450
  // If product not found, poll every 10 seconds until found
100458
100451
  if (!hasProduct) {
100459
100452
  pollTimeoutId = setTimeout(() => {
@@ -101540,7 +101533,7 @@ function App() {
101540
101533
  setTexts(['']);
101541
101534
  setPairTranslations({});
101542
101535
  // Read pairs count from settings (3–10, default 3)
101543
- const pairsCountInit = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_54__.getPairsCount)();
101536
+ const pairsCountInit = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.getPairsCount)();
101544
101537
  // Initialize placeholders
101545
101538
  const initialTitles = Array.from({ length: pairsCountInit }, (_, index) => ({
101546
101539
  index: index + 1,
@@ -101569,7 +101562,7 @@ function App() {
101569
101562
  }
101570
101563
  // Generate all pairs (title + text) in a single request
101571
101564
  addLog(formatLogMessage('log', '📋 Generating title+text pairs in a single request...'));
101572
- const selectedIndices = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_54__.getSelectedPairApproaches)();
101565
+ const selectedIndices = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.getSelectedPairApproaches)();
101573
101566
  setLastUsedApproachIndices(selectedIndices);
101574
101567
  const pairsCount = selectedIndices.length;
101575
101568
  addLog(formatLogMessage('log', `⚙️ Generating ${pairsCount} pairs (approaches: ${selectedIndices.join(', ')})`));
@@ -102202,362 +102195,52 @@ function App() {
102202
102195
  errors: finalErrors
102203
102196
  };
102204
102197
  };
102205
- const generateProductFromBanka = async (bankaImageUrls, additionalPrompt = '', addLog, currentProductImageUrl) => {
102206
- if (!openaiApiKey) {
102207
- const errorMsg = 'OpenRouter API key is not set';
102208
- logToTerminal('error', errorMsg);
102209
- if (addLog)
102210
- addLog(formatLogMessage('error', errorMsg));
102211
- throw new Error(errorMsg);
102212
- }
102213
- const logMsg = (level, ...args) => {
102214
- logToTerminal(level, ...args);
102215
- if (addLog)
102216
- addLog(formatLogMessage(level, ...args));
102217
- };
102218
- const isRegeneration = !!currentProductImageUrl;
102219
- logMsg('log', `=== Starting product image ${isRegeneration ? 'regeneration' : 'generation'} from source images ===`);
102220
- logMsg('log', '📸 Source images count:', bankaImageUrls.length);
102221
- if (isRegeneration) {
102222
- logMsg('log', '🔄 Regenerating with current product image as reference');
102223
- }
102224
- // Ensure we have a valid access token before proceeding
102225
- const validToken = await getValidAccessToken();
102226
- if (!validToken) {
102227
- const errorMsg = 'Not logged in to Google Drive or token expired';
102228
- logMsg('error', errorMsg);
102229
- throw new Error(errorMsg);
102230
- }
102231
- logMsg('log', '✅ Valid access token confirmed');
102232
- // Base prompt for product generation
102233
- let basePrompt = `Прикрепил фото упаковки продукта. Сгенерируй пожалуйста её упрощенное изображение для креативов для рекламы в Facebook.
102234
- ВАЖНО: Сохрани точно такую же форму упаковки, как на исходном фото (если это упаковка таблеток - оставь упаковку таблеток, если банка - оставь банку, если тюбик - оставь тюбик).
102235
- Убери мелкий текст. Нужна только сама упаковка продукта, не коробка. Оставь логотип, название, примерно тот же тон.`;
102236
- // If regenerating, update prompt to mention current image
102237
- if (isRegeneration) {
102238
- basePrompt = `${basePrompt}
102239
-
102240
- 🚨 ПЕРЕГЕНЕРАЦИЯ: Прикреплены два типа референсов:
102241
- 1. Исходные фото упаковки продукта (используй их как основу для формы и деталей)
102242
- 2. Текущий вариант сгенерированного продукта (НЕПРАВИЛЬНЫЙ, требует ОБЯЗАТЕЛЬНЫХ изменений согласно требованиям ниже)
102243
-
102244
- ═══════════════════════════════════════════════════════════════
102245
- 🔥 ОБЯЗАТЕЛЬНЫЕ ТРЕБОВАНИЯ ПОЛЬЗОВАТЕЛЯ (ПРИМЕНИ ВСЕ БЕЗ ИСКЛЮЧЕНИЯ):
102246
- ═══════════════════════════════════════════════════════════════`;
102247
- }
102248
- const fullPrompt = additionalPrompt.trim()
102249
- ? `${basePrompt}${isRegeneration ? '' : '\nДополнительные требования: '}${additionalPrompt.split('\n').filter(line => line.trim()).map(line => `• ${line.trim()}`).join('\n')}${isRegeneration ? '\n\n⚠️ ВАЖНО: ОБЯЗАТЕЛЬНО примени ВСЕ требования выше в новом варианте изображения. НЕ копируй предыдущий вариант - создай НОВОЕ изображение с учетом всех требований.' : ''}`
102250
- : basePrompt;
102251
- logMsg('log', '📝 Prompt:', fullPrompt);
102252
- // Convert Drive URLs to direct image URLs for API
102253
- // For Google Drive, convert view URLs to direct download URLs
102254
- // Note: Token is refreshed above to ensure files are accessible
102255
- const convertImageUrl = (url) => {
102256
- const fileIdMatch = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);
102257
- if (fileIdMatch) {
102258
- const fileId = fileIdMatch[1];
102259
- return `https://drive.google.com/uc?export=view&id=${fileId}`;
102260
- }
102261
- return url;
102262
- };
102263
- const sourceImageUrls = bankaImageUrls.map(convertImageUrl);
102264
- // Build image URLs array: current product image first (if regenerating), then source images
102265
- const imageUrls = [];
102266
- if (currentProductImageUrl) {
102267
- imageUrls.push(convertImageUrl(currentProductImageUrl));
102268
- }
102269
- imageUrls.push(...sourceImageUrls);
102270
- logMsg('log', '🖼️ Total reference images:', imageUrls.length, isRegeneration ? '(current product + source images)' : '(source images only)');
102271
- const requestBody = {
102272
- model: _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.imageGeneration,
102273
- messages: [
102274
- {
102275
- role: 'user',
102276
- content: [
102277
- { type: 'text', text: fullPrompt },
102278
- ...imageUrls.map(url => ({ type: 'image_url', image_url: { url } }))
102279
- ]
102280
- }
102281
- ],
102282
- modalities: ['image', 'text'],
102283
- };
102284
- logMsg('log', '📦 Request body prepared with', imageUrls.length, 'source images');
102285
- logMsg('log', '🚀 Sending request to OpenRouter API...');
102286
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
102287
- method: 'POST',
102288
- headers: {
102289
- 'Content-Type': 'application/json',
102290
- 'Authorization': `Bearer ${openaiApiKey}`,
102291
- 'HTTP-Referer': window.location.origin || 'https://docs-combiner.app',
102292
- 'X-Title': 'Docs Combiner'
102293
- },
102294
- body: JSON.stringify(requestBody)
102295
- });
102296
- const responseText = await response.text();
102297
- logMsg('log', '📊 Response status:', response.status, response.statusText);
102298
- if (!response.ok) {
102299
- let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
102300
- try {
102301
- const error = JSON.parse(responseText);
102302
- errorMessage = error.error?.message || error.message || errorMessage;
102303
- logMsg('error', '🔍 Parsed error:', errorMessage);
102304
- }
102305
- catch (e) {
102306
- logMsg('error', '❌ Failed to parse error response as JSON');
102307
- }
102308
- throw new Error(errorMessage);
102309
- }
102310
- logMsg('log', '📥 Parsing response...');
102311
- let data;
102312
- try {
102313
- data = JSON.parse(responseText);
102314
- logMsg('log', '✅ JSON parsed successfully');
102315
- }
102316
- catch (e) {
102317
- logMsg('error', '❌ Failed to parse response as JSON');
102318
- throw new Error(`Invalid JSON response: ${responseText.substring(0, 200)}`);
102319
- }
102320
- if (!data.choices || !data.choices[0] || !data.choices[0].message) {
102321
- logMsg('error', '❌ Invalid response structure');
102322
- throw new Error(`Invalid response structure. Expected choices[0].message`);
102323
- }
102324
- const message = data.choices[0].message;
102325
- if (!message.images || !message.images[0] || !message.images[0].image_url) {
102326
- logMsg('error', '❌ No images in response');
102327
- throw new Error(`No images found in response`);
102198
+ const handleUploadProductFile = () => {
102199
+ if (!driveFolderUrl.trim()) {
102200
+ alert('Укажите папку Google Drive');
102201
+ return;
102328
102202
  }
102329
- const imageUrl = message.images[0].image_url.url;
102330
- logMsg('log', '✅ Product image generated successfully');
102331
- logMsg('log', '=== End product image generation ===');
102332
- // Update balance after successful API call
102333
- fetchOpenRouterBalance();
102334
- return imageUrl;
102203
+ productFileInputRef.current?.click();
102335
102204
  };
102336
- const handleGenerateProduct = async () => {
102205
+ const handleProductFileSelected = async (e) => {
102206
+ const file = e.target.files?.[0];
102207
+ if (!file)
102208
+ return;
102209
+ e.target.value = '';
102337
102210
  const validToken = await getValidAccessToken();
102338
102211
  if (!validToken) {
102339
- alert('Please log in with Google first');
102340
- return;
102341
- }
102342
- if (!driveFolderUrl.trim()) {
102343
- alert('Please fill in Google Drive Folder URL');
102212
+ alert('Войдите в Google Drive');
102344
102213
  return;
102345
102214
  }
102346
- try {
102347
- const folderId = extractFolderId(driveFolderUrl);
102348
- if (!folderId) {
102349
- throw new Error('Invalid Google Drive Folder URL');
102350
- }
102351
- // Check if product.png exists in the folder
102352
- logToTerminal('log', '🔍 Checking for product.png in folder...');
102353
- const existingProductImage = await fetchProductImage(folderId);
102354
- if (existingProductImage) {
102355
- // Product.png exists - open regeneration modal without generating
102356
- logToTerminal('log', '✅ product.png found in folder, opening regeneration modal');
102357
- // Load product image as blob and convert to data URL for display
102358
- if (!productGeneratedImage) {
102359
- try {
102360
- logToTerminal('log', '📥 Downloading product.png for display...');
102361
- let token = await getValidAccessToken();
102362
- if (!token) {
102363
- throw new Error('Not logged in to Google Drive');
102364
- }
102365
- let imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${existingProductImage.id}?alt=media`, {
102366
- headers: {
102367
- 'Authorization': `Bearer ${token}`
102368
- }
102369
- });
102370
- // Handle 401 - try to refresh token and retry
102371
- if (imageResponse.status === 401 && refreshToken) {
102372
- logToTerminal('log', '🔄 Got 401, refreshing token...');
102373
- const newToken = await refreshAccessToken(refreshToken);
102374
- if (newToken) {
102375
- token = newToken;
102376
- logToTerminal('log', '✅ Token refreshed, retrying download...');
102377
- imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${existingProductImage.id}?alt=media`, {
102378
- headers: {
102379
- 'Authorization': `Bearer ${newToken}`
102380
- }
102381
- });
102382
- }
102383
- }
102384
- if (!imageResponse.ok) {
102385
- throw new Error(`Failed to download product image: ${imageResponse.status} ${imageResponse.statusText}`);
102386
- }
102387
- const blob = await imageResponse.blob();
102388
- // Convert blob to data URL using Promise
102389
- const dataUrl = await new Promise((resolve, reject) => {
102390
- const reader = new FileReader();
102391
- reader.onloadend = () => resolve(reader.result);
102392
- reader.onerror = reject;
102393
- reader.readAsDataURL(blob);
102394
- });
102395
- setProductGeneratedImage(dataUrl);
102396
- logToTerminal('log', '✅ Product image loaded and displayed');
102397
- }
102398
- catch (err) {
102399
- logToTerminal('error', '❌ Failed to load product image:', err.message);
102400
- // Fallback to direct URL if blob download fails
102401
- const productImageUrl = `https://drive.google.com/uc?export=view&id=${existingProductImage.id}`;
102402
- setProductGeneratedImage(productImageUrl);
102403
- }
102404
- }
102405
- // If we have source images, just open modal
102406
- if (productSourceImages.length > 0) {
102407
- logToTerminal('log', '✅ Source images available, opening modal');
102408
- setProductModalOpen(true);
102409
- return;
102410
- }
102411
- // If no source images, load them without generating
102412
- logToTerminal('log', '📥 Loading source images without generating');
102413
- setProductModalOpen(true);
102414
- setProductGenerationLogs([]);
102415
- const addLog = (msg) => {
102416
- setProductGenerationLogs(prev => [...prev, msg]);
102417
- };
102418
- try {
102419
- addLog(formatLogMessage('log', '🔍 Загрузка исходных изображений...'));
102420
- logToTerminal('log', '🔍 Loading source images...');
102421
- const bankaImages = await fetchBankaImages(folderId);
102422
- if (bankaImages.length === 0) {
102423
- addLog(formatLogMessage('warn', '⚠️ Исходные изображения не найдены, но есть product.png'));
102424
- logToTerminal('warn', '⚠️ No source images found, but product.png exists');
102425
- }
102426
- else {
102427
- addLog(formatLogMessage('log', `✅ Загружено ${bankaImages.length} исходных изображений`));
102428
- logToTerminal('log', `✅ Loaded ${bankaImages.length} source image(s)`);
102429
- setProductSourceImages(bankaImages.map(img => img.url));
102430
- }
102431
- }
102432
- catch (err) {
102433
- const errorMsg = 'Error loading source images: ' + err.message;
102434
- addLog(formatLogMessage('error', errorMsg));
102435
- logToTerminal('error', '❌ Error loading source images:', err);
102436
- // Don't alert, just log - user can still regenerate with existing product image
102437
- }
102438
- return; // IMPORTANT: Return here to prevent generation
102439
- }
102440
- // Product.png doesn't exist - proceed with full generation
102441
- logToTerminal('log', '🎨 product.png not found, starting full generation');
102442
- }
102443
- catch (err) {
102444
- // If check fails, proceed with generation (better to try than to block)
102445
- logToTerminal('warn', '⚠️ Failed to check for product.png, proceeding with generation:', err.message);
102446
- }
102447
- // Full generation flow - only when product image doesn't exist
102448
- setGeneratingProduct(true);
102449
- setProductGenerationLogs([]);
102450
- setProductProcessStartTime(Date.now());
102451
- setProductModalOpen(true);
102452
- const addLog = (msg) => {
102453
- setProductGenerationLogs(prev => [...prev, msg]);
102454
- };
102455
- try {
102456
- const folderId = extractFolderId(driveFolderUrl);
102457
- if (!folderId) {
102458
- throw new Error('Invalid Google Drive Folder URL');
102459
- }
102460
- addLog(formatLogMessage('log', '🔍 Searching for source images (banka*.png/jpg)...'));
102461
- logToTerminal('log', '🔍 Searching for banka images...');
102462
- const bankaImages = await fetchBankaImages(folderId);
102463
- if (bankaImages.length === 0) {
102464
- addLog(formatLogMessage('error', 'No banka*.png/jpg files found in the folder'));
102465
- alert('No banka*.png/jpg files found in the folder');
102466
- return;
102467
- }
102468
- addLog(formatLogMessage('log', `✅ Found ${bankaImages.length} source image(s)`));
102469
- logToTerminal('log', `✅ Found ${bankaImages.length} banka image(s)`);
102470
- setProductSourceImages(bankaImages.map(img => img.url));
102471
- // Generate product image
102472
- addLog(formatLogMessage('log', '🎨 Generating product image...'));
102473
- logToTerminal('log', '🎨 Generating product image...');
102474
- const generatedImageUrl = await generateProductFromBanka(bankaImages.map(img => img.url), '', addLog);
102475
- setProductGeneratedImage(generatedImageUrl);
102476
- }
102477
- catch (err) {
102478
- const errorMsg = 'Error generating product: ' + err.message;
102479
- addLog(formatLogMessage('error', errorMsg));
102480
- logToTerminal('error', '❌ Error generating product:', err);
102481
- alert(errorMsg);
102482
- }
102483
- finally {
102484
- setGeneratingProduct(false);
102485
- }
102486
- };
102487
- const handleRegenerateProduct = async () => {
102488
- if (!productSourceImages.length || !productGeneratedImage)
102215
+ const folderId = extractFolderId(driveFolderUrl);
102216
+ if (!folderId) {
102217
+ alert('Некорректная ссылка на папку Google Drive');
102489
102218
  return;
102490
- setGeneratingProduct(true);
102491
- setProductGenerationLogs([]);
102492
- if (!productProcessStartTime) {
102493
- setProductProcessStartTime(Date.now());
102494
- }
102495
- const addLog = (msg) => {
102496
- setProductGenerationLogs(prev => [...prev, msg]);
102497
- };
102498
- try {
102499
- if (productRegeneratePrompt.trim()) {
102500
- addLog(formatLogMessage('log', '🔄 Переделка изображения продукта с пользовательскими требованиями:'));
102501
- productRegeneratePrompt.split('\n').filter(line => line.trim()).forEach((line, idx) => {
102502
- addLog(formatLogMessage('log', ` ${idx + 1}. ${line.trim()}`));
102503
- });
102504
- }
102505
- else {
102506
- addLog(formatLogMessage('log', '🔄 Переделка изображения продукта (без дополнительных требований)'));
102507
- }
102508
- logToTerminal('log', '🔄 Regenerating product image with additional prompt:', productRegeneratePrompt);
102509
- // Pass current product image as reference for regeneration
102510
- const generatedImageUrl = await generateProductFromBanka(productSourceImages, productRegeneratePrompt, addLog, productGeneratedImage || undefined);
102511
- setProductGeneratedImage(generatedImageUrl);
102512
- setProductRegeneratePrompt(''); // Clear prompt after regeneration
102513
- }
102514
- catch (err) {
102515
- const errorMsg = 'Error regenerating product: ' + err.message;
102516
- addLog(formatLogMessage('error', errorMsg));
102517
- logToTerminal('error', '❌ Error regenerating product:', err);
102518
- alert(errorMsg);
102519
- }
102520
- finally {
102521
- setGeneratingProduct(false);
102522
102219
  }
102523
- };
102524
- const handleUploadProduct = async () => {
102525
- if (!productGeneratedImage || !driveFolderUrl.trim()) {
102526
- 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');
102527
102223
  return;
102528
102224
  }
102529
102225
  setUploadingProduct(true);
102530
- setProductGenerationLogs([]);
102531
- if (!productProcessStartTime) {
102532
- setProductProcessStartTime(Date.now());
102533
- }
102534
- const addLog = (msg) => {
102535
- setProductGenerationLogs(prev => [...prev, msg]);
102536
- };
102226
+ const addLog = () => { };
102537
102227
  try {
102538
- const folderId = extractFolderId(driveFolderUrl);
102539
- if (!folderId) {
102540
- throw new Error('Invalid Google Drive Folder URL');
102541
- }
102542
- addLog(formatLogMessage('log', '📤 Uploading product.png...'));
102543
- logToTerminal('log', '📤 Uploading product.png...');
102544
- await uploadImageToDrive(productGeneratedImage, 'product.png', folderId, addLog);
102545
- addLog(formatLogMessage('log', ' product.png uploaded successfully'));
102546
- logToTerminal('log', '✅ product.png uploaded successfully');
102547
- // Update folder files info
102548
- if (folderFilesInfo) {
102549
- setFolderFilesInfo({
102550
- ...folderFilesInfo,
102551
- hasProduct: true
102552
- });
102553
- }
102554
- alert('product.png uploaded successfully!');
102555
- 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 успешно загружен в папку!');
102556
102240
  }
102557
102241
  catch (err) {
102558
- const errorMsg = 'Error uploading product: ' + err.message;
102559
- addLog(formatLogMessage('error', errorMsg));
102560
- logToTerminal('error', '❌ Error uploading product:', err);
102242
+ const errorMsg = 'Ошибка загрузки: ' + err.message;
102243
+ logToTerminal('error', '❌', errorMsg);
102561
102244
  alert(errorMsg);
102562
102245
  }
102563
102246
  finally {
@@ -102801,7 +102484,7 @@ function App() {
102801
102484
  addLog(formatLogMessage(level, ...args));
102802
102485
  };
102803
102486
  logMsg('log', '📦 Creating ZIP archive with HTML and product image...');
102804
- const zip = new (jszip__WEBPACK_IMPORTED_MODULE_56___default())();
102487
+ const zip = new (jszip__WEBPACK_IMPORTED_MODULE_51___default())();
102805
102488
  zip.file('index.html', htmlContent);
102806
102489
  zip.file('product.png', productImageBlob);
102807
102490
  logMsg('log', '✅ Files added to archive: index.html, product.png');
@@ -103141,7 +102824,13 @@ function App() {
103141
102824
  : '';
103142
102825
  const imagePrompts = tasks.map(t => {
103143
102826
  const basePromptStructure = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getImageGenerationBasePrompt)(generateGeo, generatePrice, currencyForPrompt, undefined, t.ratio);
103144
- return `🏷️ ПРОДУКТ: ${generateProduct}${additionalInfoLine}\n\n${basePromptStructure}🏷️ ПРОДУКТ (повторение для ясности): ${generateProduct}${additionalInfoLine}\n🎯 ПОДХОД: ${t.approach.name}\n\n${t.approach.prompt}\n\n${t.approach.headlineAngle}\n\n${t.approach.bulletsFocus}\n\nТекст — строго следуй правилам этого подхода (HOOK обязателен; буллиты добавляй только если они разрешены для подхода).`;
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 обязателен; буллиты добавляй только если они разрешены для подхода).`;
103145
102834
  });
103146
102835
  // Initialize placeholders for all images
103147
102836
  const initialPlaceholders = tasks.map((t, index) => ({
@@ -104006,41 +103695,13 @@ ${imageData.originalPrompt}
104006
103695
  throw new Error(err.error?.message || 'Failed to fetch Drive files');
104007
103696
  }
104008
103697
  const data = await response.json();
104009
- // Filter out banka* files and product.png
103698
+ // Filter out product.png/jpg (used for product reference, not creative images)
104010
103699
  const filteredFiles = data.files.filter((f) => {
104011
103700
  const name = f.name?.toLowerCase() || '';
104012
- return !name.startsWith('banka') && name !== 'product.png' && name !== 'product.jpg';
103701
+ return name !== 'product.png' && name !== 'product.jpg';
104013
103702
  });
104014
103703
  return filteredFiles.map((f) => f.id ? `https://drive.google.com/file/d/${f.id}/view?usp=sharing` : '');
104015
103704
  };
104016
- const fetchBankaImages = async (folderId) => {
104017
- const validToken = await getValidAccessToken();
104018
- if (!validToken)
104019
- throw new Error('Not logged in');
104020
- const q = `'${folderId}' in parents and trashed = false and mimeType contains 'image/'`;
104021
- const fields = 'files(id, name)';
104022
- const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent(fields)}`;
104023
- const response = await fetch(url, {
104024
- headers: {
104025
- 'Authorization': `Bearer ${validToken}`
104026
- }
104027
- });
104028
- if (!response.ok) {
104029
- const err = await response.json();
104030
- throw new Error(err.error?.message || 'Failed to fetch Drive files');
104031
- }
104032
- const data = await response.json();
104033
- // Filter only banka* files (case insensitive)
104034
- const bankaFiles = data.files.filter((f) => {
104035
- const name = f.name?.toLowerCase() || '';
104036
- return name.startsWith('banka') && (name.endsWith('.png') || name.endsWith('.jpg') || name.endsWith('.jpeg'));
104037
- });
104038
- return bankaFiles.map((f) => ({
104039
- id: f.id,
104040
- name: f.name,
104041
- url: `https://drive.google.com/file/d/${f.id}/view?usp=sharing`
104042
- }));
104043
- };
104044
103705
  const fetchProductImage = async (folderId) => {
104045
103706
  const validToken = await getValidAccessToken();
104046
103707
  if (!validToken)
@@ -104417,11 +104078,7 @@ ${imageData.originalPrompt}
104417
104078
  logToTerminal('log', '[Load Content] Loaded generatePriceWithCurrency:', settings.generatePriceWithCurrency);
104418
104079
  }
104419
104080
  }
104420
- // Load brand and link
104421
- if (loadedData.brand !== undefined) {
104422
- setBrand(loadedData.brand || '');
104423
- logToTerminal('log', '[Load Content] Loaded brand:', loadedData.brand || '(empty)');
104424
- }
104081
+ // Load link (brand is auto-generated from product, geo, price)
104425
104082
  if (loadedData.link !== undefined) {
104426
104083
  setLink(loadedData.link || '');
104427
104084
  logToTerminal('log', '[Load Content] Loaded link:', loadedData.link || '(empty)');
@@ -104739,8 +104396,8 @@ ${imageData.originalPrompt}
104739
104396
  }
104740
104397
  setGeneratedData(rows);
104741
104398
  // Create workbook
104742
- const wb = xlsx__WEBPACK_IMPORTED_MODULE_55__.utils.book_new();
104743
- const ws = xlsx__WEBPACK_IMPORTED_MODULE_55__.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);
104744
104401
  // Set column widths (approximate pixel width / 7)
104745
104402
  ws['!cols'] = [
104746
104403
  { wch: 20 }, // id
@@ -104753,10 +104410,10 @@ ${imageData.originalPrompt}
104753
104410
  { wch: 40 }, // image_link
104754
104411
  { wch: 20 } // brand
104755
104412
  ];
104756
- xlsx__WEBPACK_IMPORTED_MODULE_55__.utils.book_append_sheet(wb, ws, "Products");
104413
+ xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_append_sheet(wb, ws, "Products");
104757
104414
  // Generate buffer
104758
- const wbout = xlsx__WEBPACK_IMPORTED_MODULE_55__.write(wb, { bookType: 'xlsx', type: 'array' });
104759
- // Upload to Drive
104415
+ const wbout = xlsx__WEBPACK_IMPORTED_MODULE_50__.write(wb, { bookType: 'xlsx', type: 'array' });
104416
+ // Upload to Drive (имя файла по бренду)
104760
104417
  const dateStr = new Date().toISOString().split('T')[0];
104761
104418
  const fileName = `${brand}-${dateStr}.xlsx`;
104762
104419
  const result = await uploadFileToDrive(wbout, fileName, folderId || undefined);
@@ -104877,13 +104534,13 @@ ${imageData.originalPrompt}
104877
104534
  setTestLoading(true);
104878
104535
  try {
104879
104536
  // Create simple test workbook with structure
104880
- const wb = xlsx__WEBPACK_IMPORTED_MODULE_55__.utils.book_new();
104537
+ const wb = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_new();
104881
104538
  const rows = [
104882
104539
  INSTRUCTION_ROW,
104883
104540
  ['id', 'title', 'description', 'availability', 'condition', 'price', 'link', 'image_link', 'brand'],
104884
104541
  ['test1', 'Test Title', 'Test Description', 'in stock', 'new', '10.00 USD', 'http://test.com', 'http://test.com/img.jpg', 'TestBrand']
104885
104542
  ];
104886
- const ws = xlsx__WEBPACK_IMPORTED_MODULE_55__.utils.aoa_to_sheet(rows);
104543
+ const ws = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.aoa_to_sheet(rows);
104887
104544
  // Set column widths
104888
104545
  ws['!cols'] = [
104889
104546
  { wch: 20 }, // id
@@ -104896,8 +104553,8 @@ ${imageData.originalPrompt}
104896
104553
  { wch: 40 }, // image_link
104897
104554
  { wch: 20 } // brand
104898
104555
  ];
104899
- xlsx__WEBPACK_IMPORTED_MODULE_55__.utils.book_append_sheet(wb, ws, "Test");
104900
- const wbout = xlsx__WEBPACK_IMPORTED_MODULE_55__.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' });
104901
104558
  // Try to extract folder ID if available, otherwise upload to root
104902
104559
  const folderId = driveFolderUrl ? extractFolderId(driveFolderUrl) : undefined;
104903
104560
  const result = await uploadFileToDrive(wbout, 'test_table.xlsx', folderId || undefined);
@@ -104931,7 +104588,7 @@ ${imageData.originalPrompt}
104931
104588
  };
104932
104589
  // Show lock screen if not unlocked
104933
104590
  if (!unlocked) {
104934
- return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_37__["default"], { theme: theme },
104591
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { theme: theme },
104935
104592
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_4__["default"], null),
104936
104593
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { onClick: handleSecretClick, sx: {
104937
104594
  width: '100vw',
@@ -104971,24 +104628,24 @@ ${imageData.originalPrompt}
104971
104628
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("br", null),
104972
104629
  "Please contact system administrator"))));
104973
104630
  }
104974
- return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_37__["default"], { theme: theme },
104631
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { theme: theme },
104975
104632
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_4__["default"], null),
104976
104633
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_7__["default"], { maxWidth: "lg", sx: { py: 4 } },
104977
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 } },
104978
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"),
104979
104636
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
104980
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 } },
104981
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_51__["default"], null)),
104982
- 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_41__["default"], null) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_40__["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)))),
104983
104640
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_9__["default"], { variant: "outlined", sx: { mb: 4 } },
104984
104641
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_10__["default"], null,
104985
104642
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 3 },
104986
104643
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
104987
104644
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true }, "Google Drive Authentication"),
104988
104645
  accessToken ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_12__["default"], null,
104989
- 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_45__["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) },
104990
104647
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { sx: { display: 'flex', alignItems: 'center', color: 'success.main' } },
104991
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_42__["default"], { sx: { mr: 1 } }),
104648
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_37__["default"], { sx: { mr: 1 } }),
104992
104649
  " Logged In (Credentials Hidden)")),
104993
104650
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_14__["default"], null,
104994
104651
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 2 },
@@ -104999,7 +104656,7 @@ ${imageData.originalPrompt}
104999
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)" }),
105000
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) }))),
105001
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 } },
105002
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["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' }),
105003
104660
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", color: openRouterAccountBalance !== null ? 'text.primary' : 'text.secondary' },
105004
104661
  "\u0411\u0430\u043B\u0430\u043D\u0441: ",
105005
104662
  openRouterBalanceLoading ? 'Loading...' : (openRouterAccountBalance !== null ? `$${openRouterAccountBalance.toFixed(2)}` : 'N/A')),
@@ -105008,8 +104665,8 @@ ${imageData.originalPrompt}
105008
104665
  "\u041B\u0438\u043C\u0438\u0442 \u043A\u043B\u044E\u0447\u0430: ",
105009
104666
  openRouterBalanceLoading ? 'Loading...' : (openRouterBalance === -1 ? 'Без лимита' : (openRouterBalance !== null ? `${openRouterBalance.toFixed(4)}` : 'N/A'))))),
105010
104667
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, alignItems: "center", sx: { mt: 2 } },
105011
- 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_42__["default"], null) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_47__["default"], null)), sx: { flexGrow: 1 } }, authLoading ? 'Logging in...' : (accessToken ? 'Logged In' : 'Login with Google')),
105012
- 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_48__["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")))),
105013
104670
  !accessToken && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
105014
104671
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], null),
105015
104672
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
@@ -105025,16 +104682,13 @@ ${imageData.originalPrompt}
105025
104682
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 14 }),
105026
104683
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0444\u0430\u0439\u043B\u043E\u0432...")))),
105027
104684
  !checkingFolderFiles && folderFilesInfo && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], null,
105028
- 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' } },
105029
- "\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: ",
105030
- 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"))),
105031
- 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"))))),
105032
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"))))),
105033
104687
  !driveFolderUrl.trim() ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "info", sx: { mt: 2 } },
105034
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"),
105035
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,
105036
104690
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: { xs: 'column', sm: 'row' }, spacing: 2 },
105037
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Brand (Short ID)", variant: "outlined", sx: { flex: '0 0 160px', minWidth: 140 }, value: brand, onChange: (e) => setBrand(e.target.value), placeholder: "e.g. NIKE" }),
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" }),
105038
104692
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { flex: 1, minWidth: 0 } },
105039
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/" }))),
105040
104694
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { sx: { my: 2 } }),
@@ -105067,7 +104721,7 @@ ${imageData.originalPrompt}
105067
104721
  : 'Выбрана модель: ' + (validationModels.find(m => m.id === selectedValidationModel)?.name || selectedValidationModel))),
105068
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 } })))),
105069
104723
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
105070
- 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_39__["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'),
105071
104725
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 1, alignItems: "center", sx: { flexGrow: 1 } },
105072
104726
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["default"], { title: imageAspectRatio === '1:1'
105073
104727
  ? '1:1 — квадрат (1024×1024 px)'
@@ -105094,7 +104748,7 @@ ${imageData.originalPrompt}
105094
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"),
105095
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"),
105096
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")))),
105097
- 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_39__["default"], null), onClick: handleGenerateImages, disabled: generatingImages ||
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 ||
105098
104752
  loadingImagesFromDrive ||
105099
104753
  !openaiApiKey ||
105100
104754
  (!accessToken && !refreshToken) ||
@@ -105120,16 +104774,15 @@ ${imageData.originalPrompt}
105120
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")),
105121
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")),
105122
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 }),
105123
104778
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
105124
- 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_39__["default"], null), onClick: handleGenerateProduct, disabled: generatingProduct || !openaiApiKey || !accessToken || !driveFolderUrl.trim(), sx: { flexGrow: 1 }, size: "large" }, generatingProduct
105125
- ? 'Generating Product...'
105126
- : (productGeneratedImage ? 'Перегенерировать Product' : 'Generate Product from Banka')),
105127
- 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_49__["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 ||
105128
104781
  !openaiApiKey ||
105129
104782
  !accessToken ||
105130
104783
  !driveFolderUrl.trim() ||
105131
104784
  (folderFilesInfo !== null && !folderFilesInfo.hasProduct), sx: { flexGrow: 1 }, size: "large", title: folderFilesInfo !== null && !folderFilesInfo.hasProduct
105132
- ? 'product.png/jpg не найден в папке. Сначала сгенерируйте его с помощью кнопки "Generate Product from Banka"'
104785
+ ? 'product.png/jpg не найден в папке. Загрузите его с помощью кнопки «Загрузить product.png/jpg»'
105133
104786
  : undefined }, generatingLanding ? 'Creating Landing...' : 'Создать лендинг')),
105134
104787
  (generatedImagesData.length > 0 || loadingImagesFromDrive) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2, mt: 3 } },
105135
104788
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } },
@@ -105281,7 +104934,7 @@ ${imageData.originalPrompt}
105281
104934
  "+",
105282
104935
  imageData.checkErrors.length - 2,
105283
104936
  " \u0435\u0449\u0451")))),
105284
- 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_50__["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")))),
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")))),
105285
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...")),
105286
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")),
105287
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")),
@@ -105304,7 +104957,7 @@ ${imageData.originalPrompt}
105304
104957
  : 'transparent'
105305
104958
  }
105306
104959
  } }),
105307
- 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_39__["default"], null), onClick: () => handleRegenerateImage(imageData), disabled: imageData.regenerating ||
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 ||
105308
104961
  imageData.uploading ||
105309
104962
  imageData.checkStatus === 'checking' ||
105310
104963
  !imageData.originalPrompt ||
@@ -105314,12 +104967,12 @@ ${imageData.originalPrompt}
105314
104967
  : ((generatingImages && !imageData.imageUrl && !imageData.failed && !imageData.generating)
105315
104968
  ? 'В очереди'
105316
104969
  : (!imageData.imageUrl ? 'Сгенерировать' : 'Переделать'))),
105317
- 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_50__["default"], null), onClick: () => handleRegenerateImageFresh(imageData), disabled: imageData.regenerating ||
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 ||
105318
104971
  imageData.uploading ||
105319
104972
  imageData.checkStatus === 'checking' ||
105320
104973
  !imageData.originalPrompt ||
105321
104974
  !imageData.productImageUrl, fullWidth: true }, "\u041F\u0435\u0440\u0435\u0434\u0435\u043B\u0430\u0442\u044C \u0437\u0430\u043D\u043E\u0432\u043E")),
105322
- !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_43__["default"], null), onClick: () => {
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: () => {
105323
104976
  const folderId = extractFolderId(driveFolderUrl);
105324
104977
  if (folderId) {
105325
104978
  handleUploadImage(imageData, folderId);
@@ -105327,7 +104980,7 @@ ${imageData.originalPrompt}
105327
104980
  }, disabled: imageData.uploading || imageData.regenerating || !driveFolderUrl.trim(), fullWidth: true }, imageData.uploading ? 'Загрузка...' : 'Загрузить'))))));
105328
104981
  }))),
105329
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 } },
105330
- 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_43__["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})`))))),
105331
104984
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { sx: { my: 2 } }),
105332
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 } },
105333
104986
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } },
@@ -105398,7 +105051,7 @@ ${imageData.originalPrompt}
105398
105051
  tooltip: { sx: { maxWidth: 360, fontSize: 12, lineHeight: 1.6, p: '10px 14px' } },
105399
105052
  } },
105400
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' } } },
105401
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_46__["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),
105402
105055
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 1.5 },
105403
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 } },
105404
105057
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16, sx: { color: 'primary.main' } }),
@@ -105465,11 +105118,11 @@ ${imageData.originalPrompt}
105465
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 } },
105466
105119
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }),
105467
105120
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'text.secondary' } }, formatElapsedTime(elapsedTime.landing)))),
105468
- !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_52__["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")))),
105469
105122
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2 },
105470
- 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_43__["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')),
105471
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 } },
105472
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_44__["default"], { fontSize: "small" })) },
105125
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_39__["default"], { fontSize: "small" })) },
105473
105126
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
105474
105127
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
105475
105128
  "File Uploaded!",
@@ -105479,88 +105132,7 @@ ${imageData.originalPrompt}
105479
105132
  getElectronAPI().openExternal(uploadedLink);
105480
105133
  }, style: { cursor: 'pointer', textDecoration: 'underline', color: 'inherit' } }, "Click here to open in Google Drive")),
105481
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!")))))))))),
105482
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_31__["default"], { open: productModalOpen, onClose: () => !generatingProduct && !uploadingProduct && setProductModalOpen(false), maxWidth: "lg", fullWidth: true },
105483
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { open: generatingProduct || uploadingProduct, sx: {
105484
- position: 'absolute',
105485
- zIndex: (theme) => theme.zIndex.modal + 1,
105486
- backgroundColor: 'rgba(0, 0, 0, 0.7)',
105487
- display: 'flex',
105488
- flexDirection: 'column',
105489
- gap: 3
105490
- } },
105491
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 } },
105492
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 60, sx: { color: 'white' } }),
105493
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", sx: { color: 'white', fontWeight: 'bold' } }, formatElapsedTime(elapsedTime.product))),
105494
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_30__["default"], { sx: {
105495
- p: 3,
105496
- maxWidth: '80%',
105497
- maxHeight: '60%',
105498
- overflow: 'auto',
105499
- backgroundColor: (theme) => theme.palette.mode === 'dark'
105500
- ? 'rgba(30, 30, 30, 0.95)'
105501
- : 'rgba(255, 255, 255, 0.95)'
105502
- } },
105503
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { mb: 2, fontWeight: 'bold' } }, uploadingProduct ? 'Процесс загрузки' : 'Процесс генерации'),
105504
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { id: "product-generation-logs", sx: {
105505
- fontFamily: 'monospace',
105506
- fontSize: '0.875rem',
105507
- lineHeight: 1.6,
105508
- whiteSpace: 'pre-wrap',
105509
- wordBreak: 'break-word',
105510
- maxHeight: '400px',
105511
- overflowY: 'auto'
105512
- } }, 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: {
105513
- mb: 0.5,
105514
- color: log.includes('❌') || log.toLowerCase().includes('error') ? 'error.main' :
105515
- log.includes('⚠️') || log.toLowerCase().includes('warn') ? 'warning.main' :
105516
- 'text.primary'
105517
- } }, log))))))),
105518
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_33__["default"], null,
105519
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6" }, "Product Image Generation")),
105520
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_34__["default"], null,
105521
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 3, sx: { mt: 1 } },
105522
- productSourceImages.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
105523
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "subtitle1", gutterBottom: true, sx: { fontWeight: 'bold' } },
105524
- "\u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F (",
105525
- productSourceImages.length,
105526
- "):"),
105527
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
105528
- display: 'grid',
105529
- gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
105530
- gap: 2,
105531
- mt: 1
105532
- } }, 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: {
105533
- width: '100%',
105534
- height: 'auto',
105535
- maxHeight: 300,
105536
- objectFit: 'contain',
105537
- border: (theme) => `1px solid ${theme.palette.mode === 'dark' ? '#444' : '#ddd'}`,
105538
- borderRadius: 1
105539
- }, onError: (e) => {
105540
- // Fallback: try using thumbnail URL if direct view fails
105541
- const fileIdMatch = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);
105542
- if (fileIdMatch) {
105543
- const fileId = fileIdMatch[1];
105544
- e.target.src = `https://drive.google.com/thumbnail?id=${fileId}&sz=w1000`;
105545
- }
105546
- } })))))),
105547
- productGeneratedImage && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
105548
- 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:"),
105549
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "img", src: productGeneratedImage, alt: "Generated Product", sx: {
105550
- width: '100%',
105551
- height: 'auto',
105552
- maxHeight: 400,
105553
- objectFit: 'contain',
105554
- border: (theme) => `2px solid ${theme.palette.primary.main}`,
105555
- borderRadius: 1,
105556
- mt: 1
105557
- } }))),
105558
- 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 }))),
105559
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_35__["default"], { sx: { px: 3, pb: 2 } },
105560
- 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"),
105561
- 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_39__["default"], null) }, generatingProduct ? 'Перегенерация...' : 'Перегенерировать'),
105562
- 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_43__["default"], null) }, uploadingProduct ? 'Загрузка...' : 'Загрузить (product.png)'))),
105563
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_PromptManagerDialog__WEBPACK_IMPORTED_MODULE_53__["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) }))));
105564
105136
  }
105565
105137
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (App);
105566
105138
 
@@ -105995,8 +105567,8 @@ function PromptManagerDialog({ open, onClose }) {
105995
105567
  const [viewModes, setViewModes] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({});
105996
105568
  // selectedApproaches: array of indices from PAIR_APPROACH_POOL, ordered
105997
105569
  const [selectedApproaches, setSelectedApproaches] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([0, 1, 2]);
105998
- // imageApproachCounts: 8 элементов, каждый 0–4
105999
- const [imageApproachCounts, setImageApproachCounts] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([1, 1, 1, 1, 1, 1, 1, 1]);
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));
106000
105572
  // Загрузить оверрайды при открытии
106001
105573
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
106002
105574
  if (open) {
@@ -106596,25 +106168,32 @@ function getSelectedPairApproaches() {
106596
106168
  function getPairsCount() {
106597
106169
  return getSelectedPairApproaches().length;
106598
106170
  }
106171
+ /** Количество подходов для изображений. Должно совпадать с CREO_APPROACHES.length в prompts.ts */
106172
+ const IMAGE_APPROACH_COUNT = 10;
106599
106173
  /**
106600
- * Получить количество изображений по каждому подходу (8 элементов, 0–4).
106174
+ * Получить количество изображений по каждому подходу (N элементов, 0–4).
106601
106175
  * По умолчанию — по 1 на каждый подход.
106602
106176
  */
106603
106177
  function getImageApproachCounts() {
106604
106178
  const overrides = loadPromptOverrides();
106605
- if (overrides.imageApproachCounts && overrides.imageApproachCounts.length === 8) {
106606
- return overrides.imageApproachCounts.map(c => Math.max(0, Math.min(4, c)));
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);
106607
106186
  }
106608
106187
  // backward compat: selectedImageApproaches -> counts (1 for selected, 0 for not)
106609
106188
  if (overrides.selectedImageApproaches && overrides.selectedImageApproaches.length >= 1) {
106610
- const counts = Array(8).fill(0);
106189
+ const counts = Array(IMAGE_APPROACH_COUNT).fill(0);
106611
106190
  for (const i of overrides.selectedImageApproaches) {
106612
- if (i >= 0 && i < 8)
106191
+ if (i >= 0 && i < IMAGE_APPROACH_COUNT)
106613
106192
  counts[i] = 1;
106614
106193
  }
106615
106194
  return counts;
106616
106195
  }
106617
- return [1, 1, 1, 1, 1, 1, 1, 1];
106196
+ return defaultCounts;
106618
106197
  }
106619
106198
  /**
106620
106199
  * Загрузить оверрайды из localStorage
@@ -106778,7 +106357,8 @@ __webpack_require__.r(__webpack_exports__);
106778
106357
  /* harmony export */ getTextsSystemPrompt: () => (/* binding */ getTextsSystemPrompt),
106779
106358
  /* harmony export */ getTitlesSystemPrompt: () => (/* binding */ getTitlesSystemPrompt),
106780
106359
  /* harmony export */ getUserPrompt: () => (/* binding */ getUserPrompt),
106781
- /* harmony export */ getValidationPrompt: () => (/* binding */ getValidationPrompt)
106360
+ /* harmony export */ getValidationPrompt: () => (/* binding */ getValidationPrompt),
106361
+ /* harmony export */ pickRandomBullets: () => (/* binding */ pickRandomBullets)
106782
106362
  /* harmony export */ });
106783
106363
  /* harmony import */ var _promptOverrides__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./promptOverrides */ "./src/promptOverrides.ts");
106784
106364
  /**
@@ -106786,6 +106366,56 @@ __webpack_require__.r(__webpack_exports__);
106786
106366
  * Все промпты на русском языке для консистентности
106787
106367
  */
106788
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
+ }
106789
106419
  // Debug logging — пишет в терминал Electron (или в console как fallback)
106790
106420
  function _debugLog(...args) {
106791
106421
  const api = typeof window !== 'undefined' ? window.electronAPI : null;
@@ -106917,16 +106547,13 @@ function getTitlesSystemPrompt(geo, noOverride, count = 3) {
106917
106547
  - НЕ упоминай название продукта в заголовке — оно уже на креативе. Заголовок = только боль + решение/триггер
106918
106548
 
106919
106549
  Технические требования:
106920
- - Каждый заголовок должен предпочтительно быть до 55 символов. При необходимости для разнообразия смысла допускается до 60 символов
106921
- - Каждый заголовок максимум 6-7 слов. Если длиннее сократи, сохраняя смысл
106550
+ - Каждый заголовок может быть 1–4 строки. Допускается до 12 слов (при многострочности)
106551
+ - Каждый заголовок предпочтительно до 55 символов в строке. При необходимости для разнообразия допускается до 60 символов в строке
106922
106552
  - ЗАПРЕЩЕНО использовать двоеточие (:) в заголовках. Используй тире (—) или переформулируй заголовок без двоеточия. Если в заголовке есть двоеточие — это критическая ошибка, перепиши заголовок
106923
106553
  - Используй 1-2 эмодзи естественно внутри заголовка, не только в конце. Эмодзи должны заменять или визуально отмечать смысл (эмоция, действие, предупреждение), а не украшать или заполнять пространство. Максимум 1-2 на строку
106924
106554
  - Избегай эмодзи ⚠️ и 🚨 — они ассоциируются с опасностью. Для привлечения внимания используй 👉, ✅, 🌿, ⏳
106925
- - КРИТИЧНО: Каждый заголовок должен содержать хотя бы одно ясное ключевое слово проблемы, релевантное категории продукта (например, для суставов: боль/скованность/подвижность; для пищеварения: дискомфорт/тяжесть; для сна: бессонница/усталость примеры только для формата, адаптируй под категорию). Если ключевое слово проблемы отсутствует — перепиши заголовок
106555
+ - КРИТИЧНО: Каждый заголовок ОБЯЗАН содержать ключевое слово проблемы ЯВНО. Примеры по категориям: простата простатит, простата; похудение лишний вес, похудение; суставы боль в суставах, колени, скованность; пищеварение дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Если ключевое слово отсутствует — перепиши заголовок
106926
106556
  - Используй знания лучших практик рекламного копирайтинга
106927
- - Избегай только жёстких медицинских гарантий («лечит», «вылечивает», «гарантируем»). Мягкие обещания результата разрешены («Nopți fără treziri», «Stomac liniștit», «Efect rapid»). Разрешены: сроки («în 14 zile»), цифры соц. доказательства («9 din 10 bărbați»), мягкие результаты. Запрещены: «100% гарантия», «навсегда», «излечение».
106928
- - Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
106929
- - Если в категории продукта есть специфические медицинские термины или диагнозы — используй их осторожно, не в каждом заголовке. Максимум ${maxMedTerms} заголовок из ${n} может содержать прямое упоминание таких терминов. Предпочитай мягкие формулировки, описывающие симптомы и дискомфорт, а не диагнозы. Если все ${n} заголовков содержат прямые медицинские термины — перепиши все кроме ${maxMedTerms} с мягкими альтернативами, фокусируясь на ощущениях и симптомах
106930
106557
  - После создания проверь их правильность и разнообразие
106931
106558
  - Верни только заголовки, по одному на строку, без нумерации или буллетов
106932
106559
  - Заголовки должны быть на языке ${geo}
@@ -106954,8 +106581,8 @@ function getTextsSystemPrompt(geo, noOverride, count = 3) {
106954
106581
  .map((p, i) => `- Текст ${i + 1} → [${p.name}]. ${p.textApproach}`)
106955
106582
  .join('\n');
106956
106583
  const deadlineNote = n >= 2
106957
- ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO). Формулируй как ожидание/видимый результат, НЕ как гарантию. НЕЛЬЗЯ использовать «гарантированно/100%/навсегда».`
106958
- : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO). Формулируй как ожидание/видимый результат.`;
106584
+ ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO).`
106585
+ : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO).`;
106959
106586
  return `Ты — эксперт-копирайтер, специализирующийся на рекламе в Meta Ads (Facebook/Instagram). Твоя задача — создать ровно ${n} идеальных продающих рекламных текста для Meta Ads на языке ${geo}.
106960
106587
 
106961
106588
  ВАЖНО — СИСТЕМА ПАРЫ (заголовок + текст):
@@ -106985,24 +106612,18 @@ ${deadlineNote}
106985
106612
  - Избегай академических, синтетических или звучащих как ИИ слов и фраз
106986
106613
  - Избегай искусственного или неестественного формулирования, которое реальные рекламодатели не использовали бы
106987
106614
  - Если фраза звучит синтетически или академически — перепиши её
106988
- - Избегай медицинской терминологии, которую обычный человек не использует в разговоре («secreția biliară», «diureza», «peristaltică»). Пиши так, как человек описал бы проблему другу
106989
-
106990
106615
  Требования к содержанию:
106991
106616
  - КРИТИЧНО: Держи каждый текст кратким — цель примерно 150-280 символов на текст. Тексты в стиле отзыва могут использовать до 300 символов при необходимости для естественного повествования. Будь эффектным, но коротким
106992
106617
  - Если текст длиннее 280 символов (или 300 для стиля отзыва) — сожми и перепиши, сохраняя смысл
106993
106618
  - КРИТИЧНО: Каждый текст должен содержать хотя бы одно ясное ключевое слово проблемы, релевантное категории продукта (например, для суставов: боль/скованность/подвижность; для пищеварения: дискомфорт/тяжесть; для сна: бессонница/усталость — примеры только для формата, адаптируй под категорию). Если ключевое слово проблемы отсутствует — перепиши текст
106994
106619
  - КРИТИЧНО: Каждый текст должен заканчиваться сильным и явным призывом к действию (кликни, попробуй сейчас, закажи сегодня, узнай больше и т.д.)
106995
106620
  - Избегай слабых окончаний или информационного тона. Слабые или нейтральные окончания не допускаются
106996
- - CTA должен быть “нажимным”: допускаются формулировки «не откладывай», «последний шанс», «только сегодня», «забери -50%» (всё строго на языке GEO). Но избегай слов «гарантированно», «вылечит», «100%», «навсегда»
106621
+ - CTA должен быть “нажимным”: допускаются формулировки «не откладывай», «последний шанс», «только сегодня», «забери -50%» (всё строго на языке GEO)
106997
106622
  - В каждом тексте строго следуй заданному формату старта согласно его подходу (см. распределение выше)
106998
106623
  - Если ингредиенты предоставлены в дополнительной информации, упоминай их ТОЛЬКО если они естественно подходят выбранному рекламному подходу. Например: подходы авторитета/экспертизы выигрывают от деталей ингредиентов, в то время как подходы срочности/эмоционального триггера могут не нуждаться в них. Упоминай только релевантные ингредиенты, не обязательно все из них. Если ингредиенты не подходят подходу или не предоставлены — полностью пропусти их упоминание и не изобретай и не упоминай никакие ингредиенты
106999
106624
  - Добавляй только существенные поддерживающие детали, которые усиливают убеждение или ясность, а не заполнители или общие утверждения. Будь кратким и прямым
107000
106625
  - Используй 1-4 эмодзи естественно внутри текста, не только в конце. Эмодзи должны заменять или визуально отмечать смысл (эмоция, действие, предупреждение), а не украшать или заполнять пространство. Максимум 1-2 на строку
107001
106626
  - Избегай эмодзи ⚠️ и 🚨 — они ассоциируются с опасностью. Для привлечения внимания используй 👉, ✅, 🌿, ⏳
107002
- - Разрешены формулировки: «reduce frecvența», «elimină disconfortul», «rezultat vizibil în X zile», «9 din 10 observă diferența». Запрещены только: «лечит/vindecă», «гарантируем 100%», «навсегда/definitiv».
107003
- - Ты можешь усиливать эффект и результаты, но без прямых медицинских обещаний
107004
- - Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
107005
- - Если в категории продукта есть специфические медицинские термины или диагнозы — используй их осторожно, не в каждом тексте. Максимум ${maxMedTerms} текст из ${n} может содержать прямое упоминание таких терминов. Остальные тексты фокусируй на симптомах, дискомфорте и ощущениях пользователя, релевантных категории продукта. Избегай эвфемизмов и искусственных замен — используй естественные формулировки
107006
106627
  - Избегай прямых упоминаний интимных или деликатных деталей, связанных с проблемой. Используй мягкие формулировки, описывающие дискомфорт и симптомы, а не физиологические процессы
107007
106628
  - Избегай повторения полного названия продукта более 2 раз в одном тексте. Используй местоимения или более короткие ссылки при необходимости
107008
106629
 
@@ -107065,8 +106686,8 @@ function getPairsSystemPrompt(geo, noOverride, count = 3, selectedIndices) {
107065
106686
  .map((p, i) => `Пара ${i + 1} → [${p.name}]:\n Заголовок: ${p.titleApproach}\n Текст: ${p.textApproach}`)
107066
106687
  .join('\n');
107067
106688
  const deadlineNote = n >= 2
107068
- ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}). Формулируй как ожидание/видимый результат, НЕ как гарантию.`
107069
- : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}). Формулируй как ожидание/видимый результат.`;
106689
+ ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}).`
106690
+ : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}).`;
107070
106691
  return `Ты — эксперт-копирайтер, специализирующийся на рекламе в Meta Ads (Facebook/Instagram). Создай ровно ${n} пар «заголовок + текст» для рекламы на языке ${geo}.
107071
106692
 
107072
106693
  ФОРМАТ ОТВЕТА (строго соблюдай, без лишних символов):
@@ -107092,7 +106713,6 @@ ${distributionLines}
107092
106713
  - 1–2 эмодзи естественно внутри (не только в конце); избегай ⚠️ и 🚨
107093
106714
  - Хотя бы одно ключевое слово проблемы, релевантное продукту
107094
106715
  - НЕ упоминай название продукта — заголовок = боль + триггер
107095
- - Максимум ${maxMedTerms} из ${n} заголовков могут содержать прямые медицинские термины
107096
106716
  - Звучит как живая рекламная фраза, а не строка ключевых слов
107097
106717
 
107098
106718
  ТРЕБОВАНИЯ К ТЕКСТУ:
@@ -107101,7 +106721,6 @@ ${distributionLines}
107101
106721
  - Сильный явный CTA в конце («кликни», «попробуй сейчас», «закажи», «не жди»)
107102
106722
  - Короткие рубленые фразы, императивы, FOMO допустим${hasTestimonial ? '\n- Testimonial: строго от первого лица, начинается с «Имя, возраст:», конкретный результат с таймингом' : ''}
107103
106723
  ${deadlineNote}
107104
- - Избегай «гарантированно», «вылечит», «100%», «навсегда»
107105
106724
 
107106
106725
  ОБЩИЕ ТРЕБОВАНИЯ:
107107
106726
  - Язык: ${geo} — все тексты строго на этом языке
@@ -107175,6 +106794,7 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
107175
106794
  // Resolve no-bullets conditions in code — don't leave "if approach = X" for the LLM
107176
106795
  const noBulletsApproachNames = CREO_APPROACHES.filter(a => a.noBullets).map(a => a.name);
107177
106796
  const isNoBullets = noBulletsApproachNames.includes(approachName?.trim() ?? '');
106797
+ const isScreenshotReviews = (approachName?.trim() ?? '') === 'Скрин отзывов';
107178
106798
  // ШАГ 0 — hint for BULLETS depends on approach
107179
106799
  const step0BulletsHint = isNoBullets
107180
106800
  ? `BULLETS: [] (для данного подхода буллиты запрещены — ожидается 0)`
@@ -107195,12 +106815,17 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
107195
106815
  - Буллиты должны быть расположены вертикально (столбиком). Если буллиты идут горизонтально в одну строку — ОШИБКА: плохая читаемость на мобиле
107196
106816
  - Цена визуально ОТЛИЧНА от буллитов (цена — компактный блок без иконок; буллеты — с иконками/галочками). Если цена выглядит как буллет — ОШИБКА.`;
107197
106817
  // ШАГ 3 — TEXT LIMIT: PVP has no badges allowed, normal allows urgency/trust (без проверок по длине/количеству слов)
107198
- const step3TextLimit = isNoBullets
106818
+ const step3TextLimit = isScreenshotReviews
107199
106819
  ? `ШАГ 3 — OTHER_TEXT (критично):
106820
+ - Для «Скрин отзывов» блок отзывов (аватарки, имена, возраст, 5 звёзд, текст отзывов) — это НЕ ошибка, это основной контент.
106821
+ - Допускаются также trust‑печати (0–3 шт). URGENCY — не рекомендуется, но не ошибка.
106822
+ - На креативе допустимы: HOOK, блок отзывов, цена, скидка, CTA, опционально trust‑печати.`
106823
+ : isNoBullets
106824
+ ? `ШАГ 3 — OTHER_TEXT (критично):
107200
106825
  - OTHER_TEXT должен быть ПУСТЫМ (0 элементов). Никаких дополнительных бейджей, подписей, ярлыков, trust‑печатей/urgency — НИЧЕГО.
107201
106826
  - Любой дополнительный текст (бейджи/подписи/пояснения/urgency/trust‑печати) — ОШИБКА: слишком много текста для punch‑креатива.
107202
106827
  - На креативе допустимы ТОЛЬКО: HOOK, цена, скидка, кнопка CTA.`
107203
- : `ШАГ 3 — OTHER_TEXT (критично):
106828
+ : `ШАГ 3 — OTHER_TEXT (критично):
107204
106829
  - По умолчанию любой другой рекламный текст (бейджи/подписи/пояснения) запрещён.
107205
106830
  - НО допускаются (не обязательно) дополнительные бейджи ВНЕ упаковки — это НЕ ошибка, если ВСЕ элементы OTHER_TEXT подпадают под разрешённые типы:
107206
106831
  A) URGENCY (0–1 шт): бейдж срочности/дефицита — только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» и подобные — непонятно что означает.
@@ -107211,7 +106836,7 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
107211
106836
  * TRUST‑печати — про натуральность/премиальность/безопасность (не про доставку/поддержку)
107212
106837
  Примеры стиля (адаптируй к языку GEO, не требуются):
107213
106838
  - URGENCY: «Ostatnie sztuki», «Tylko dziś», «Koniec dziś» (НЕ «24h» — непонятно)
107214
- - TRUST: «Naturalna formuła», «Wysoka jakość», «Bezpieczne», «Eko», «Kontrola jakości»
106839
+ - TRUST: «Naturalna formuła», «Wysoka jakość» (для PL); «Ingrediente naturale», «Calitate» (для RO). ЗАПРЕЩЕНО: «NATURAL», «QUALITY», «100% NATURAL» — английские слова.
107215
106840
  - Если OTHER_TEXT содержит что-то вне этих типов, или URGENCY > 1, или TRUST > 3 (для Минимализма — TRUST > 1) — ОШИБКА.`;
107216
106841
  return `Ты — СТРОГИЙ валидатор рекламных креативов (1:1). Не улучшай и не предлагай идеи — только проверка и вердикт.
107217
106842
  Продукт: ${product}. GEO/язык: ${geo}.${approachLine}
@@ -107233,10 +106858,10 @@ URGENCY_BADGE: "..." (бейдж срочности, если есть; «24h»
107233
106858
  TRUST_BADGES: ["..."] (печати цветные, яркие, в стиле FDA; про натуральность/премиальность/безопасность; размер как у буллитов — крупные, если есть)
107234
106859
 
107235
106860
  ШАГ 1 — HOOK/HEADLINE (критично):
107236
- - Должен быть в верхнем блоке и читабелен (хорошо читается на телефоне)
107237
- - Допускается 1–2 строки (это НЕ ошибка). Ошибка только если текст нечитабелен или есть разрыв/перенос внутри слова.
106861
+ - Должен быть ВЫШЕ всех элементов и читабелен (хорошо читается на телефоне)
106862
+ - Допускается 1–4 строки (это НЕ ошибка). Ошибка только если текст нечитабелен или есть разрыв/перенос внутри слова.
107238
106863
  - Смысл: релевантно проблеме/выгоде продукта. Допускаются и боль/дискомфорт, и обещание/выгода (например «Könnyebb napok»). НЕ считай ошибкой формулировки обещаний результата или клеймы.
107239
- - Желательно: ключевое слово боли/проблемы в формулировке (как часть смысла), но отсутствие ключевого слова само по себе НЕ является ошибкой. Примеры для ориентира: ${keywords.join(', ')} (можно синонимы).
106864
+ - ОБЯЗАТЕЛЬНО: ключевое слово проблемы явно в формулировке. Примеры по категориям: простата простатит, простата; похудение лишний вес, похудение; суставы боль, колени, скованность; пищеварение → дискомфорт, вздутие; сон → бессонница, усталость. Отсутствие ключевого слова — ОШИБКА. Дополнительно для ориентира: ${keywords.join(', ')} (можно синонимы).
107240
106865
  - Проверь вторую часть заголовка. Если она состоит только из абстрактных слов («Mai ușor», «Mai bine», «Soluția», «Ajutor») без конкретного бенефита — отметь: РЕКОМЕНДАЦИЯ: заголовок можно усилить конкретикой
107241
106866
  - Если заголовок слишком общий/абстрактный («Mai ușor», «Mai bine» без конкретики) — отметь как РЕКОМЕНДАЦИЯ к улучшению (не ошибка, но слабо)
107242
106867
  - РЕКОМЕНДАЦИЯ (CTR): верхний текст лучше делать как HOOK — ОЧЕНЬ крупный, жирный, ВСЕ БУКВЫ ПРОПИСНЫЕ, на яркой контрастной плашке. Если это не так — не ошибка, но отметь рекомендацией
@@ -107251,7 +106876,7 @@ ${step3TextLimit}
107251
106876
  ШАГ 4 — CTA > PRICE (критично):
107252
106877
  - CTA: на языке ${geo}, хорошо заметная кнопка (позиция НЕ важна: можно вправо/влево/по центру). Главное — CTA читабельна и выглядит как кнопка.
107253
106878
  - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (до скидки) + новая. Если только одна цена — ОШИБКА. Старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Тонкое/незаметное зачёркивание — ОШИБКА.
107254
- - Скидка ОБЯЗАТЕЛЬНА: «-50%» (возможны варианты символа минуса: -, или ). Ярко, заметно. Строго НЕ на упаковке/банке.
106879
+ - Скидка ОБЯЗАТЕЛЬНА: «-50%» отдельным видимым бейджем (процент должен быть явно читаем, не только в цене). Ярко, заметно. Строго НЕ на упаковке/банке.
107255
106880
  - Не считать ошибкой, если скидка/цена конкурируют с CTA по акценту — это допустимо, пока CTA остаётся хорошо заметной и читабельной.
107256
106881
  - Если скидка указана и она не равна -50% — ошибка.
107257
106882
 
@@ -107264,7 +106889,7 @@ ${step3TextLimit}
107264
106889
  - КОНТРАСТ ПЛАШЕК: если хотя бы одна текстовая плашка (HOOK, буллеты, CTA или цена) визуально сливается с фоном и текст плохо читается — это ОШИБКА.
107265
106890
  - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА. Цена не должна выглядеть как буллет.
107266
106891
  - TRUST‑печати: если печати мелкие или текст нечитабелен на телефоне — ОШИБКА (размер как минимум как у буллитов).
107267
- - СКИДКА «-50%»: ярко, заметно, на контрастной подложке. Строго НЕ на упаковке/банке. Если скидка на продукте или бледная/незаметна — ОШИБКА.
106892
+ - СКИДКА «-50%»: ОБЯЗАТЕЛЬНО отдельным видимым бейджем (процент явно читаем). Если скидка не отображается или только в цене без отдельного «-50%» — ОШИБКА. Ярко, заметно, НЕ на упаковке.
107268
106893
  - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА.
107269
106894
  - Бейджи срочности: «24h» и подобные неясные формулировки — ОШИБКА (непонятно что означает). Ясные («только сегодня», «последние штуки») — ок.
107270
106895
 
@@ -107331,18 +106956,17 @@ CRITICAL RULES (это ВАЛИДАЦИЯ, не рекомендации; есл
107331
106956
 
107332
106957
  ЯЗЫК + ТОН:
107333
106958
  - ВСЕ слова строго на языке ${generateGeo} (HOOK/буллеты/CTA/скидка/опциональный бейдж срочности/опциональные trust‑печати). Английские слова запрещены. Исключений нет.
107334
- - Тон: убедительный, но честный. Лёгкая эмоция и срочность — ок. Запрещено: ложные обещания, диагностика заболеваний, слова «лечит/вылечивает/гарантируем».
106959
+ - Тон: убедительный, но честный. Лёгкая эмоция и срочность — ок.
107335
106960
 
107336
106961
  HOOK / HEADLINE (строгое правило):
107337
- - ровно 35 слов
107338
- - строго 1 строка; без переноса/разбиения слов; без подзаголовка
107339
- - заголовок должен описывать боль пользователя на языке ${generateGeo}; ключевое слово проблемы часть боли (не отдельный ярлык/бейдж)
106962
+ - 14 строки; до 12 слов. Без переноса внутри слова
106963
+ - ОБЯЗАТЕЛЬНО содержит ключевое слово проблемы ЯВНО. Примеры по категориям: простата → простатит, простата; похудение → лишний вес, похудение; суставы → боль в суставах, колени, скованность; пищеварение → дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Без ключевого слова — ОШИБКА
106964
+ - HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). Самый крупный текстовый элемент на креативе
107340
106965
  - HOOK должен быть ВИЗУАЛЬНО “тяжёлым”: ОЧЕНЬ крупный, ТОЛСТЫЙ/жирный шрифт, ВСЕ БУКВЫ ПРОПИСНЫЕ (CAPS), на яркой контрастной плашке/подложке. Это верхний главный “thumb‑stop” элемент
107341
- - Можно (не обязательно) включить СРОК прямо в HOOK (например «7 dni», «14 dni») как триггер, но без жёсткой гарантии. Лучше формулировать как ожидание/вопрос (например с «?»), не как медицинское обещание
107342
- - Заголовок должен быть РЕЗКИМ и призывным: короткие рубленые фразы, вопросы и предупреждения допустимы. Можно использовать обращение на «ты» (в языке GEO: «Masz…», «Twój…») и вопросительный знак — но без диагнозов и без жёстких медицинских гарантий
107343
- - заголовок может содержать мягкое обещание облегчения, но не медицинскую гарантию. ПРАВИЛЬНО: «Nopți fără treziri? Posibil». НЕ ТАК: «Vindecă prostata definitiv».
106966
+ - Можно (не обязательно) включить СРОК прямо в HOOK (например «7 dni», «14 dni») как триггер
106967
+ - Заголовок должен быть РЕЗКИМ и призывным: короткие рубленые фразы, вопросы и предупреждения допустимы. Можно использовать обращение на «ты» (в языке GEO: «Masz…», «Twój…») и вопросительный знак
107344
106968
  - заголовок НЕ должен быть абстрактным слоганом/лозунгом или метафорой без конкретной боли. Допускается «thumb‑stop» стиль (вызов/вопрос/предупреждение), если это конкретно и релевантно категории
107345
- - если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт.
106969
+ - если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт. HOOK всегда остаётся выше всех остальных элементов
107346
106970
  - Не повторяй один и тот же заголовок в разных подходах: для текущего подхода придумай новый, уникальный заголовок. Варьируй: угол боли, формулировку, акцент (проблема vs облегчение)
107347
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»
107348
106972
  - Специфические медицинские термины или диагнозы разрешены в заголовке, но не обязательны. Варьируй между прямыми формулировками (если релевантно категории) и мягкими формулировками, фокусирующимися на симптомах и дискомфорте, для разных креативов. Не используй один и тот же термин во всех креативах
@@ -107351,27 +106975,34 @@ HOOK / HEADLINE (строгое правило):
107351
106975
  - ТАК (пищеварение): «Puffadás gond? Könnyebb napok»
107352
106976
  - ТАК (суставы): «Ízületi fájdalom? Mozogj könnyebben»
107353
106977
  - ТАК (сон): «Álmatlan éjszakák? Pihenj végre»
107354
- Если заголовок > 5 слов, похож на предложение, распадается на 2 смысловых блока или звучит как лозунг — ОШИБКА → пересоздай вариант.
106978
+ - ТАК (простата): «Prostată? Alinare rapidă»
106979
+ - ТАК (похудение): «Greutate în plus? Fără diete extreme»
106980
+ ❗ Если заголовок > 12 слов, похож на длинное предложение или звучит как лозунг без ключевого слова проблемы — ОШИБКА → пересоздай вариант.
107355
106981
 
107356
106982
  BULLETS (жёсткое правило, ровно 3):
107357
106983
  - По умолчанию: ровно 3 буллета
107358
106984
  - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллиты ЗАПРЕЩЕНЫ (0 буллитов). Не добавляй буллет‑блок вообще
107359
106985
  - каждый 2–3 слова (макс 4); без запятых; буллет НЕ должен выглядеть как предложение
107360
- - буллеты = свойства ИЛИ ощущаемые преимущества (комфорт, лёгкость, спокойствие). Запрещено: диагнозы, жёсткие медицинские гарантии («лечит», «вылечивает», «избавляет навсегда»). Разрешено: мягкие результаты («Reduce disconfortul», «Mai puține simptome») — примеры формата, адаптируй под категорию, сроки без гарантий («Efect în 7-14 zile», «Rezultat vizibil rapid»), цифры как соц. доказательство («9 din 10 bărbați recomandă»).
107361
- - Делай буллеты более активными и конкретными: глаголы действия + сроки + цифры. Примеры формата: «Reduce X», «Ulga od 1. dnia», «Efekt w 7 dni», «93% potwierdza», «3× szybciej» (адаптируй под язык GEO и категорию)
107362
106986
  - ВИЗУАЛ БУЛЛЕТОВ: крупные, с иконками/галочками (✓), на контрастных подложках. Буллеты ВНЕ упаковки/банки — не на продукте.
107363
106987
  - Буллиты должны быть расположены ВЕРТИКАЛЬНО (столбиком), не горизонтально в одну строку. Минимальный размер шрифта буллитов — чтобы читались на экране телефона без зума
107364
- - Не повторяй один и тот же набор буллитов во всех подходах: для текущего подхода используй свой набор и акценты. Варианты: свойства (Formulă naturală), бенефиты (Reduce disconfortul), сроки (Efect în 7-14 zile), соц. доказательство (9 din 10 recomandă)
107365
- ПРАВИЛЬНО (пример формата адаптируй под категорию продукта): «Confort zilnic», «Fără dureri», «Reduce disconfortul», «Mișcare ușoară», «Efect în 2 săptămâni».
107366
- ЗАПРЕЩЕНО: «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
+
107367
106998
  ❗ Если хоть один буллет > 4 слов или похож на предложение/обещание — ОШИБКА → пересоздай.
107368
106999
 
107369
107000
  TEXT LIMIT:
107370
107001
  - кроме: HOOK, 3 буллетов, цены, скидки, кнопки — ЛЮБОЙ другой текст запрещён (ярлыки/подписи/дисклеймеры/пояснения)
107371
107002
  - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → кроме HOOK, цены, скидки, CTA — НИКАКОГО другого текста. Буллиты запрещены. Other_text/urgency/trust‑печати запрещены
107372
107003
  - ДОПУСКАЕТСЯ (не обязательно) ОДИН бейдж срочности: только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» — непонятно что означает.
107373
- - ДОПУСКАЕТСЯ (не обязательно) 1–3 trust‑печати: печати цветные, яркие, очень похожие на печать FDA. Подчёркивают: натуральность состава, премиальность, безопасность продукта. Допустимо также: качество, натуральные ингредиенты, экологичность, контроль качества. Короткий текст (1–3 слова) на языке ${generateGeo}, компактно по низу. КРИТИЧНО: печати должны быть КРУПНЫМИ размер как минимум как у буллитов, лучше крупнее. Текст читабелен на телефоне без зума. Даже одна печать — крупная. Мелкие печати — ОШИБКА. Запрещено: «рекомендовано врачами», «клинически доказано», «одобрено Минздравом» и любые фразы, требующие доказательств
107374
- - HOOK + буллеты <= 16 слов суммарно; если больше — ОШИБКА → пересоздай
107004
+ - ДОПУСКАЕТСЯ (не обязательно) 1–3 trust‑печати: печати цветные, яркие, в стиле FDA. Текст СТРОГО на языке ${generateGeo} запрещены английские слова («NATURAL», «QUALITY» и т.д.). Примеры для RO: «naturale», «Ingrediente naturale»; для PL: «naturalna». Размер как минимум как у буллитов. Мелкие печати — ОШИБКА.
107005
+ - HOOK + буллеты <= 24 слова суммарно (HOOK до 12, буллеты до 12); если больше — ОШИБКА → пересоздай
107375
107006
  - Если хочешь добавить ощущение срочности — делай это через формулировки в HOOK/BULLETS, через реквизит/иконки, ИЛИ (не обязательно) через один короткий бейдж срочности. Только ясные формулировки («Ostatnie sztuki», «Tylko dziś», «Koniec dziś»). ЗАПРЕЩЕНО «24h» — непонятно что означает.
107376
107007
 
107377
107008
  CTA > PRICE:
@@ -107382,17 +107013,17 @@ CTA > PRICE:
107382
107013
  ❗ ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → цена и «-50%» могут быть более крупными и заметными, но CTA всё равно должен оставаться очень заметным и выглядеть как кнопка (не теряется на фоне)
107383
107014
 
107384
107015
  ПОКАЗЫВАЙ ТОЛЬКО ЭТИ ТЕКСТОВЫЕ ЭЛЕМЕНТЫ:
107385
- - HOOK (1 строка; CAPS; жирный; на яркой контрастной подложке)
107016
+ - HOOK (1–4 строки; выше всех элементов; самый крупный; CAPS; жирный; на яркой контрастной подложке; ключевое слово проблемы явно)
107386
107017
  - 3 буллета (крупные, с иконками/галочками ✓, на контрастных подложках, НЕ на банке/упаковке)
107387
107018
  - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (2×${generatePrice} ${generateCurrency}) зачёркнута ТОЛСТОЙ контрастной линией + новая ${generatePrice} ${generateCurrency} выразительно. Одна цена = ОШИБКА. Тонкая линия зачёркивания = ОШИБКА. Без слов. Не на упаковке.
107388
- - Скидка: «-50%» (обязательно, отдельным бейджем; ярко, заметно; строго НЕ на банке/упаковке; визуально слабее CTA)
107019
+ - Скидка: «-50%» ОБЯЗАТЕЛЬНО отдельным видимым бейджем (не только в цене!). Процент скидки должен быть явно читаем. Ярко, заметно, строго НЕ на банке/упаковке, визуально слабее CTA
107389
107020
  - Кнопка CTA (1-2 слова)
107390
107021
  - Опционально: 1 короткий бейдж срочности (ясные формулировки: «только сегодня», «последние штуки»; ЗАПРЕЩЕНО «24h»). Бейдж в углу кадра, контрастный, но меньше CTA. НЕ обязателен
107391
- - Опционально: 1–3 trust‑печати (цветные, яркие, очень похожие на печать FDA; подчёркивают натуральность, премиальность, безопасность; допустимо: качество, натуральные ингредиенты, экологичность, контроль качества). Короткий текст на языке ${generateGeo}. Размер как минимум как у буллитов, лучше крупнее читабельны на телефоне без зума. Даже одна печать крупная. Для подхода «Минимализм» — макс 1 печать. НЕ обязательны
107022
+ - Опционально: 1–3 trust‑печати (цветные, яркие, в стиле FDA; натуральность, премиальность, безопасность). Текст СТРОГО на языке ${generateGeo} никакого английского. Для RO: «naturale», «Ingrediente naturale», «Calitate»; для PL: «naturalna», «naturalne»; НЕ «NATURAL», «QUALITY». Размер как минимум как у буллитов. НЕ обязательны
107392
107023
  - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → показывай только: HOOK + PRICE + DISCOUNT + CTA. Буллиты/urgency/trust‑печати запрещены
107393
107024
 
107394
107025
  ВИЗУАЛ:
107395
- - Иерархия: HOOK > продукт > CTA > цена > буллеты
107026
+ - Иерархия: HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). HOOK — самый крупный текстовый элемент
107396
107027
  - Буллеты: крупные, с иконками/галочками (✓), на контрастных подложках. Строго ВНЕ упаковки/банки.
107397
107028
  - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание = ОШИБКА. Визуально ОТЛИЧНА от буллитов.
107398
107029
  - Скидка «-50%»: ярко, заметно, на контрастной подложке. Строго ВНЕ упаковки/банки.
@@ -107402,8 +107033,9 @@ CTA > PRICE:
107402
107033
  - Без ссылок/доменов и мелкого текста
107403
107034
  - LIFESTYLE/CLEAN: продукт в контексте использования. Для пищеварения — кухня, обеденный стол, рядом с едой/чашкой. Для сна/простаты — спальня, тумбочка, стакан воды. Для суставов — стол/тумбочка рядом с эластичным бинтом, ортезом или активным фоном (кроссовки, лестница). Контекст = момент приёма. Просто продукт на белом фоне — НЕ допускается
107404
107035
  - Цвета и композиция должны быть «thumb‑stop»: высокий контраст, яркий акцент на продукте (луч света/подсветка/обводка/глоу), избегай бледных пастельных сцен
107405
- - ЛЮДИ: человек допускается ТОЛЬКО для подхода «Эмоция / Портрет». Для всех остальных подходов — БЕЗ человека.
107406
- - Если есть человек (только в «Эмоция / Портрет»): прямой взгляд в камеру (как обращение лично к зрителю), эмоция облегчения/надежды (без стоковой улыбки). Возраст человека ДОЛЖЕН соответствовать категории и целевой аудитории продукта: по умолчанию 50–60, но для категорий, где аудитория обычно моложе (например похудение/фитнес) допускается 35–55. Этнически соответствует GEO/рынку
107036
+ - ЛЮДИ: человек допускается ТОЛЬКО для подходов «Эмоция / Портрет» и «Врач / Эксперт». Для всех остальных — БЕЗ человека.
107037
+ - Если «Эмоция / Портрет»: прямой взгляд в камеру, эмоция облегчения/надежды. Возраст 50–60 (похудение/фитнес 35–55). Этнически соответствует GEO/рынку.
107038
+ - Если «Врач / Эксперт»: женщина-врач 40–55 лет. Внешность и этничность СТРОГО соответствуют GEO/рынку (локал — врач выглядит как местный специалист).
107407
107039
 
107408
107040
  ANTI-TEMPLATE DIVERSITY (КРИТИЧНО):
107409
107041
  - Ты сейчас создаёшь ОДИН креатив для текущего подхода. В проекте будет серия из ${totalApproaches} креативов, поэтому у каждого подхода должна быть своя узнаваемая композиция.
@@ -107411,15 +107043,17 @@ ANTI-TEMPLATE DIVERSITY (КРИТИЧНО):
107411
107043
  - Ориентируйся на строку «🎯 ПОДХОД: ...» и примени соответствующую раскладку ниже (только одну, соответствующую текущему подходу):
107412
107044
  * 🎯 ПОДХОД: Эксперт / Авторитет → БЕЗ человека. Профессиональный контекст с реквизитом экспертизы вокруг продукта. Продукт в центре. HOOK сверху справа, BULLETS справа ниже, CTA снизу справа, PRICE/DISCOUNT рядом с CTA (слабее CTA). Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
107413
107045
  * 🎯 ПОДХОД: Lifestyle / Момент приёма → FLAT-LAY/СВЕРХУ или 3/4 сверху на столе/тумбочке. Продукт в центре, реквизит "момент приёма" вокруг. HOOK сверху по центру, BULLETS сбоку (вертикально, шрифт достаточно крупный для чтения на телефоне без зума), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Urgency‑бейдж (если есть) — в углу, только ясные формулировки, НЕ «24h». Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
107414
- * 🎯 ПОДХОД: Эмоция / Портрет → ЭТО ЕДИНСТВЕННЫЙ ПОДХОД С ЧЕЛОВЕКОМ В СЕРИИ. ОЧЕНЬ крупный портрет лица (thumb‑stop), взгляд в камеру — лицо занимает верхние 2/3 кадра и доминирует. Продукт в руке или у подбородка. HOOK сверху по центру. BULLETS, CTA, PRICE/DISCOUNT, 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 внизу.
107415
107049
  * 🎯 ПОДХОД: Визуализация проблемы → Инфографика/схема: слева проблема (иконка/схема зоны тела, релевантной продукту), справа решение + продукт. HOOK сверху по центру, BULLETS справа или снизу (вертикально), CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне). Красный акцент только на проблеме.
107416
107050
  * 🎯 ПОДХОД: Power / Сила решения → БЕЗ человека. ДИНАМИЧНЫЙ комикс‑кадр, диагональная композиция. Продукт как “герой” + power‑иконки (щит/молния/бёрст). HOOK сверху слева на яркой плашке, BULLETS слева ниже, CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
107417
107051
  * 🎯 ПОДХОД: Минимализм / Clean Big Text → Белый/градиентный фон, минимум элементов. ОГРОМНЫЙ HOOK занимает верх/центр, продукт крупно (центр/право), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Trust‑печати (если есть) — размер как минимум как у буллитов, крупные и читабельные.
107418
- * 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK 3–4 слова МАКС, огромный CAPS. БЕЗ буллитов. PRICE + «-50%» крупно и заметно. CTA контрастной кнопкой
107419
- * 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — крупный, грубый bold/рукописный, CAPS, 3–4 слова. Крупные надписи цены/скидки вокруг продукта. CTA — плоская кнопка без градиентов. БЕЗ буллитов, бейджей, теней, иконок.
107052
+ * 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK 1–4 строки, до 12 слов, огромный CAPS, выше всех. БЕЗ буллитов. PRICE + «-50%» крупно и заметно. CTA контрастной кнопкой
107053
+ * 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — 1–4 строки, до 12 слов, крупный, грубый bold/рукописный, CAPS, выше всех. Крупные надписи цены/скидки вокруг продукта. CTA — плоская кнопка без градиентов. БЕЗ буллитов, бейджей, теней, иконок.
107420
107054
 
107421
107055
  AUTO-CHECK (перед финалом):
107422
- - HOOK: 35 слов, 1 строка, CAPS, жирный на контрастной плашке, боль + направление облегчения (не лозунг), язык ${generateGeo} без английских слов
107056
+ - HOOK: 14 строки, до 12 слов, CAPS, жирный на контрастной плашке, выше всех элементов, ключевое слово проблемы явно, язык ${generateGeo} без английских слов
107423
107057
  - Буллеты: по умолчанию ровно 3, каждый <= 4 слов, без запятых, не предложения. ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллетов 0 (запрещены)
107424
107058
  - Нет лишнего текста кроме: HOOK, буллетов, цены, скидки, кнопки (+ опционально 1 бейдж срочности + опционально 1–3 trust‑печати)
107425
107059
  - Есть скидка «-50%» (вне упаковки) и она слабее CTA
@@ -107463,38 +107097,38 @@ const CREO_APPROACHES = [
107463
107097
  {
107464
107098
  name: 'Эксперт / Авторитет',
107465
107099
  prompt: `ЭКСПЕРТ / АВТОРИТЕТ (без человека): стиль “рекомендация эксперта”, но БЕЗ человека. Профессиональный контекст — аптечные полки на фоне / медицинский планшет / стетоскоп / рецептурный блокнот как реквизит рядом с продуктом. Продукт в центре как “рекомендованное решение”. Чистый профессиональный фон, высокий контраст. Trust‑печати (1–3 шт): цветные, яркие, очень похожие на печать FDA; подчёркивают натуральность состава, премиальность, безопасность. Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная.`,
107466
- headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 35 слов, 1 строка, CAPS.`,
107467
- bulletsFocus: `БУЛЛИТЫ: “доверие + действие + конкретика”. Минимум 1 буллет с действием на главную боль (глагол), минимум 1 со сроком/скоростью, плюс 1 с цифрой/соц.доказательством (если уместно). Всё строго на языке GEO и релевантно категории. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры (например «Naturalna formuła» сама по себе).`
107100
+ headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107101
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор для этого подхода не повторяй комбинации из других креативов.`
107468
107102
  },
107469
107103
  {
107470
107104
  name: 'Lifestyle / Момент приёма',
107471
107105
  prompt: `LIFESTYLE / МОМЕНТ ПРИЁМА: продукт в контексте использования (кухня/стол/спальня/тумбочка). Без человека, но с "историей" (стакан воды, чай, тарелка, будильник/часы/календарь). Срочность: реквизит (часы/таймер‑иконка) и/или опциональный urgency‑бейдж (1–3 слова) в углу кадра — только ясные формулировки («только сегодня», «последние штуки»), ЗАПРЕЩЕНО «24h». Высокий контраст, яркий акцент на продукте. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ВАЖНО: буллиты располагаются вертикально и всегда читабельны на экране телефона без зума — даже если уходят вбок, минимальный размер шрифта буллитов не уменьшается. Если буллиты не вмещаются сбоку с нужным шрифтом — переставь их вниз.`,
107472
- headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 35 слов, 1 строка, CAPS.`,
107473
- bulletsFocus: `БУЛЛИТЫ: активные глаголы + сроки + цифры. Минимум 1 буллет со сроком/скоростью, минимум 1 с цифрой/соц.доказательством. Всё 2–4 слова, без запятых, строго на языке GEO и релевантно категории. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
107106
+ headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107107
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (срок + цифра + действие/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107474
107108
  },
107475
107109
  {
107476
107110
  name: 'Эмоция / Портрет',
107477
- prompt: `ЭМОЦИЯ / ПОРТРЕТ: ЭТО ЕДИНСТВЕННЫЙ ПОДХОД С ЧЕЛОВЕКОМ В СЕРИИ. Используй максимально: ОЧЕНЬ крупный план лица (thumb‑stop), прямой взгляд в камеру, сильная эмоция облегчения/надежды (без широкой стоковой улыбки). Возраст человека подбери под категорию и ЦА продукта: по умолчанию 50–60, но для категорий с более молодой аудиторией (например похудение/фитнес) допускается 35–55. Продукт в руке на уровне лица или рядом, хорошо виден. Свет "тень → свет" на лице допустим. Минимум отвлекающих деталей, высокий контраст. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ИЕРАРХИЯ: лицо — главный и доминирующий визуальный элемент (верхние 2/3 кадра). Все текстовые блоки (HOOK исключение — сверху) уходят в нижнюю треть. Буллиты, цена, CTA НЕ перекрывают лицо и НЕ конкурируют с ним по визуальному весу — они заметно меньше и ниже.`,
107478
- headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение». 35 слов, 1 строка, CAPS. Тон максимально личный и прямой.`,
107479
- 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 СЛУЧАЙНЫХ из пула (качество жизни + действие + срок/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107480
107114
  },
107481
107115
  {
107482
107116
  name: 'Визуализация проблемы',
107483
107117
  prompt: `ВИЗУАЛИЗАЦИЯ ПРОБЛЕМЫ (без человека): схема/иконка проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав/позвоночник; для пищеварения — желудок; для простаты — силуэт мужчины) + мягкий красный акцент на этой зоне (не шок‑контент, без графики). Рядом продукт как решение. Можно добавить простую стрелку/переход “проблема → облегчение” как графику (без лишнего текста). Чистый фон, высокая читабельность. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека.`,
107484
- headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 35 слов, 1 строка, CAPS, вопросительный знак допустим.`,
107485
- bulletsFocus: `БУЛЛИТЫ: механика/результат в активной форме + срок/скорость + конкретика. Без диагнозов и гарантий. Всё строго на GEO. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
107118
+ headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно, вопросительный знак допустим.`,
107119
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107486
107120
  },
107487
107121
  {
107488
107122
  name: 'Power / Сила решения',
107489
107123
  prompt: `POWER / СИЛА РЕШЕНИЯ (без человека): метафоры силы и победы над проблемой: щит, молния, энергия, взрыв‑бёрст, мощные стикеры/иконки. Продукт как “герой” в центре, высокая энергия, контрастные агрессивные цвета. Можно комикс/anti‑design подачу, но всё должно быть читабельно. Без лишнего текста. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека. Только продукт + power‑иконки.`,
107490
- headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 35 слов, 1 строка, CAPS.`,
107491
- bulletsFocus: `БУЛЛИТЫ: активные глаголы + скорость/срок + конкретика + (если уместно) цифра. Без гарантий. Всё строго на GEO. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
107124
+ headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107125
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + скорость + цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107492
107126
  },
107493
107127
  {
107494
107128
  name: 'Минимализм / Clean Big Text',
107495
107129
  prompt: `МИНИМАЛИЗМ / CLEAN BIG TEXT (без человека): белый или мягкий градиентный фон, премиальное ощущение. ОГРОМНЫЙ HOOK как главный элемент (CAPS, жирный, на контрастной плашке). Продукт крупно, минимум реквизита, минимум шума. Тени/премиальные материалы допустимы, но без лишнего текста. БЕЗ человека. Urgency и trust‑печати в этом подходе НЕ рекомендуются — они нарушают чистоту и ощущение премиальности. Добавляй печати только если это критически необходимо, не более 1, но крупную — размер как у буллитов.`,
107496
- headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 35 слов, 1 строка, CAPS.`,
107497
- bulletsFocus: `БУЛЛИТЫ: максимально коротко, но конкретно: действие + срок/скорость + цифра/доказательство (если уместно). Никаких абстрактных буллитов без конкретики. Всё строго на GEO.`
107130
+ headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107131
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + формула/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107498
107132
  },
107499
107133
  {
107500
107134
  name: 'Problem Visual Punch',
@@ -107503,14 +107137,14 @@ const CREO_APPROACHES = [
107503
107137
  - Фон: яркий сплошной цвет (красный/оранжевый) или агрессивный градиент
107504
107138
  - Центр: КРУПНАЯ иконка/схема проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав; для простаты — силуэт мужчины; для пищеварения — желудок), с красным свечением
107505
107139
  - Продукт: крупно рядом
107506
- - HOOK: 3–4 слова МАКСИМУМ, огромный, CAPS
107140
+ - HOOK: 1–4 строки, до 12 слов, огромный, CAPS, ключевое слово проблемы явно
107507
107141
  - БЕЗ буллитов
107508
107142
  - Цена + скидка: крупно, заметно
107509
107143
  - CTA: контрастная яркая кнопка
107510
107144
 
107511
107145
  Цель: считывание за 1 секунду. Минимум текста, максимум визуального удара.
107512
107146
  БЕЗ человека. Никаких trust‑печатей/urgency бейджей — только HOOK + PRICE + -50% + CTA.`,
107513
- headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 3–4 слова, CAPS.`,
107147
+ headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107514
107148
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK и крупных надписях.`
107515
107149
  },
107516
107150
  {
@@ -107525,8 +107159,33 @@ const CREO_APPROACHES = [
107525
107159
  - Крупные текстовые надписи рядом с продуктом: цена, скидка; допустимо одно короткое слово-восклицание («РАБОТАЕТ!», «ПРОВЕРЕНО!») на языке GEO
107526
107160
  - Кнопка CTA: плоская, без градиентов, контрастный сплошной цвет
107527
107161
  - НИКАКИХ буллитов, trust‑печатей, urgency‑бейджей, иконок — только продукт и текст`,
107528
- headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 3–4 слова, CAPS, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
107162
+ headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
107529
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.`
107530
107189
  }
107531
107190
  ];
107532
107191