docs-combiner 0.1.10 → 0.1.12

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)([]);
@@ -99916,8 +99948,7 @@ function App() {
99916
99948
  const [linkCopied, setLinkCopied] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99917
99949
  const [openRouterKeyCopied, setOpenRouterKeyCopied] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99918
99950
  const [loadingContentFromDrive, setLoadingContentFromDrive] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99919
- const [loadingImagesFromDrive, setLoadingImagesFromDrive] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99920
- const [driveFilesFound, setDriveFilesFound] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({ content: false, images: false });
99951
+ const [driveFilesFound, setDriveFilesFound] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({ content: false });
99921
99952
  // Theme state
99922
99953
  const [darkMode, setDarkMode] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => {
99923
99954
  const saved = localStorage.getItem('themeMode');
@@ -99964,7 +99995,7 @@ function App() {
99964
99995
  }
99965
99996
  };
99966
99997
  // Create theme based on mode
99967
- const theme = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => (0,_mui_material__WEBPACK_IMPORTED_MODULE_36__["default"])({
99998
+ const theme = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => (0,_mui_material__WEBPACK_IMPORTED_MODULE_31__["default"])({
99968
99999
  palette: {
99969
100000
  mode: darkMode ? 'dark' : 'light',
99970
100001
  ...(darkMode
@@ -100056,7 +100087,7 @@ function App() {
100056
100087
  };
100057
100088
  loadKey();
100058
100089
  // Load prompt overrides from Electron config
100059
- (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_54__.loadOverridesFromElectron)();
100090
+ (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.loadOverridesFromElectron)();
100060
100091
  }, []);
100061
100092
  // Save form fields to localStorage whenever they change
100062
100093
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
@@ -100085,7 +100116,7 @@ function App() {
100085
100116
  const timeoutId = setTimeout(async () => {
100086
100117
  try {
100087
100118
  logToTerminal('log', '[Auto Save] Auto-saving settings to Google Drive');
100088
- await saveGeneratedContentToDrive(folderId, generatedTitlesData, generatedTextsData, {
100119
+ await saveGeneratedContentToDrive(folderId, {
100089
100120
  generateProduct,
100090
100121
  generateGeo,
100091
100122
  generateAdditionalInfo,
@@ -100098,21 +100129,19 @@ function App() {
100098
100129
  }
100099
100130
  }, 1000);
100100
100131
  return () => clearTimeout(timeoutId);
100101
- }, [generateProduct, generateGeo, generateAdditionalInfo, generatePriceWithCurrency, brand, link, driveFolderUrl, generatedTitlesData, generatedTextsData, loadingContentFromDrive]);
100132
+ }, [generateProduct, generateGeo, generateAdditionalInfo, generatePriceWithCurrency, brand, link, driveFolderUrl, loadingContentFromDrive]);
100102
100133
  // Load generated content from Google Drive when driveFolderUrl changes
100103
100134
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
100104
100135
  if (!driveFolderUrl) {
100105
100136
  logToTerminal('log', '[Load] No driveFolderUrl, skipping load');
100106
100137
  setLoadingContentFromDrive(false);
100107
- setLoadingImagesFromDrive(false);
100108
- setDriveFilesFound({ content: false, images: false });
100138
+ setDriveFilesFound({ content: false });
100109
100139
  // Clear all data when driveFolderUrl is cleared
100110
100140
  setGeneratedTitlesData([]);
100111
100141
  setGeneratedTextsData([]);
100112
100142
  setGeneratedImagesData([]);
100113
100143
  setTitles('');
100114
100144
  setTexts(['']);
100115
- setBrand('');
100116
100145
  setLink('');
100117
100146
  return;
100118
100147
  }
@@ -100120,15 +100149,13 @@ function App() {
100120
100149
  if (!folderId) {
100121
100150
  logToTerminal('warn', '[Load] Invalid driveFolderUrl, cannot extract folderId:', driveFolderUrl);
100122
100151
  setLoadingContentFromDrive(false);
100123
- setLoadingImagesFromDrive(false);
100124
- setDriveFilesFound({ content: false, images: false });
100152
+ setDriveFilesFound({ content: false });
100125
100153
  // Clear all data when folderId cannot be extracted
100126
100154
  setGeneratedTitlesData([]);
100127
100155
  setGeneratedTextsData([]);
100128
100156
  setGeneratedImagesData([]);
100129
100157
  setTitles('');
100130
100158
  setTexts(['']);
100131
- setBrand('');
100132
100159
  setLink('');
100133
100160
  return;
100134
100161
  }
@@ -100139,11 +100166,9 @@ function App() {
100139
100166
  setGeneratedImagesData([]);
100140
100167
  setTitles('');
100141
100168
  setTexts(['']);
100142
- setBrand('');
100143
100169
  setLink('');
100144
100170
  setLoadingContentFromDrive(true);
100145
- setLoadingImagesFromDrive(true);
100146
- setDriveFilesFound({ content: false, images: false });
100171
+ setDriveFilesFound({ content: false });
100147
100172
  // Load content from Google Drive
100148
100173
  loadGeneratedContentFromDrive(folderId).then((result) => {
100149
100174
  logToTerminal('log', '[Load] Content loading completed, found:', result.found);
@@ -100152,8 +100177,6 @@ function App() {
100152
100177
  logToTerminal('error', '[Load] Error loading content from Google Drive:', err);
100153
100178
  setLoadingContentFromDrive(false);
100154
100179
  });
100155
- // Креативы не кешируются — не загружаем изображения при открытии
100156
- setLoadingImagesFromDrive(false);
100157
100180
  }, [driveFolderUrl]);
100158
100181
  // Sync generatedTitlesData with titles when titles changes (except when user is editing in UI)
100159
100182
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
@@ -100179,12 +100202,6 @@ function App() {
100179
100202
  }, [titles]); // Only depend on titles, not generatedTitlesData to avoid loops
100180
100203
  // Auto-scroll logs when new logs are added
100181
100204
  (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
100205
  if (imagesGenerationLogs.length > 0 && (generatingImages || uploadingImages)) {
100189
100206
  const logContainer = document.getElementById('images-generation-logs');
100190
100207
  if (logContainer) {
@@ -100197,30 +100214,7 @@ function App() {
100197
100214
  logContainer.scrollTop = logContainer.scrollHeight;
100198
100215
  }
100199
100216
  }
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]);
100217
+ }, [imagesGenerationLogs, generatingImages, uploadingImages, contentGenerationLogs, generating]);
100224
100218
  // Timer for images generation
100225
100219
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
100226
100220
  let interval = null;
@@ -100441,19 +100435,16 @@ function App() {
100441
100435
  return;
100442
100436
  if (response.ok) {
100443
100437
  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
100438
  const hasProduct = data.files.some((f) => {
100449
100439
  const name = f.name?.toLowerCase() || '';
100450
100440
  return name === 'product.png' || name === 'product.jpg';
100451
100441
  });
100442
+ const hasCreativeImages = data.files.some((f) => {
100443
+ const name = f.name?.toLowerCase() || '';
100444
+ return name !== 'product.png' && name !== 'product.jpg';
100445
+ });
100452
100446
  if (!cancelled) {
100453
- setFolderFilesInfo({
100454
- bankaCount: bankaFiles.length,
100455
- hasProduct
100456
- });
100447
+ setFolderFilesInfo({ hasProduct, hasCreativeImages });
100457
100448
  // If product not found, poll every 10 seconds until found
100458
100449
  if (!hasProduct) {
100459
100450
  pollTimeoutId = setTimeout(() => {
@@ -101540,7 +101531,7 @@ function App() {
101540
101531
  setTexts(['']);
101541
101532
  setPairTranslations({});
101542
101533
  // Read pairs count from settings (3–10, default 3)
101543
- const pairsCountInit = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_54__.getPairsCount)();
101534
+ const pairsCountInit = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.getPairsCount)();
101544
101535
  // Initialize placeholders
101545
101536
  const initialTitles = Array.from({ length: pairsCountInit }, (_, index) => ({
101546
101537
  index: index + 1,
@@ -101569,7 +101560,7 @@ function App() {
101569
101560
  }
101570
101561
  // Generate all pairs (title + text) in a single request
101571
101562
  addLog(formatLogMessage('log', '📋 Generating title+text pairs in a single request...'));
101572
- const selectedIndices = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_54__.getSelectedPairApproaches)();
101563
+ const selectedIndices = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.getSelectedPairApproaches)();
101573
101564
  setLastUsedApproachIndices(selectedIndices);
101574
101565
  const pairsCount = selectedIndices.length;
101575
101566
  addLog(formatLogMessage('log', `⚙️ Generating ${pairsCount} pairs (approaches: ${selectedIndices.join(', ')})`));
@@ -101606,20 +101597,8 @@ function App() {
101606
101597
  try {
101607
101598
  const folderId = extractFolderId(driveFolderUrl);
101608
101599
  if (folderId) {
101609
- addLog(formatLogMessage('log', '💾 Saving generated content to Google Drive...'));
101610
- const currentTitlesData = pairsResult.map((p, index) => ({
101611
- index: index + 1,
101612
- title: p.title,
101613
- generating: false,
101614
- failed: !p.title
101615
- }));
101616
- const currentTextsData = pairsResult.map((p, index) => ({
101617
- index: index + 1,
101618
- text: p.text,
101619
- generating: false,
101620
- failed: !p.text
101621
- }));
101622
- await saveGeneratedContentToDrive(folderId, currentTitlesData, currentTextsData, {
101600
+ addLog(formatLogMessage('log', '💾 Saving settings to Google Drive...'));
101601
+ await saveGeneratedContentToDrive(folderId, {
101623
101602
  generateProduct,
101624
101603
  generateGeo,
101625
101604
  generateAdditionalInfo,
@@ -102202,362 +102181,54 @@ function App() {
102202
102181
  errors: finalErrors
102203
102182
  };
102204
102183
  };
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`);
102328
- }
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;
102335
- };
102336
- const handleGenerateProduct = async () => {
102337
- const validToken = await getValidAccessToken();
102338
- if (!validToken) {
102339
- alert('Please log in with Google first');
102340
- return;
102341
- }
102184
+ const handleUploadProductFile = () => {
102342
102185
  if (!driveFolderUrl.trim()) {
102343
- alert('Please fill in Google Drive Folder URL');
102186
+ alert('Укажите папку Google Drive');
102344
102187
  return;
102345
102188
  }
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
- }
102189
+ productFileInputRef.current?.click();
102486
102190
  };
102487
- const handleRegenerateProduct = async () => {
102488
- if (!productSourceImages.length || !productGeneratedImage)
102191
+ const handleProductFileSelected = async (e) => {
102192
+ const file = e.target.files?.[0];
102193
+ if (!file)
102489
102194
  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
- }
102523
- };
102524
- const handleUploadProduct = async () => {
102525
- if (!productGeneratedImage || !driveFolderUrl.trim()) {
102526
- alert('No product image to upload');
102195
+ e.target.value = '';
102196
+ const ext = file.name.toLowerCase().split('.').pop();
102197
+ if (ext !== 'png' && ext !== 'jpg' && ext !== 'jpeg') {
102198
+ alert('Выберите изображение PNG или JPG');
102527
102199
  return;
102528
102200
  }
102529
102201
  setUploadingProduct(true);
102530
- setProductGenerationLogs([]);
102531
- if (!productProcessStartTime) {
102532
- setProductProcessStartTime(Date.now());
102533
- }
102534
- const addLog = (msg) => {
102535
- setProductGenerationLogs(prev => [...prev, msg]);
102536
- };
102537
102202
  try {
102203
+ const validToken = await getValidAccessToken();
102204
+ if (!validToken) {
102205
+ alert('Войдите в Google Drive');
102206
+ setUploadingProduct(false);
102207
+ return;
102208
+ }
102538
102209
  const folderId = extractFolderId(driveFolderUrl);
102539
102210
  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
- });
102211
+ alert('Некорректная ссылка на папку Google Drive');
102212
+ setUploadingProduct(false);
102213
+ return;
102553
102214
  }
102554
- alert('product.png uploaded successfully!');
102555
- setProductModalOpen(false);
102215
+ const addLog = () => { };
102216
+ const dataUrl = await new Promise((resolve, reject) => {
102217
+ const reader = new FileReader();
102218
+ reader.onloadend = () => resolve(reader.result);
102219
+ reader.onerror = reject;
102220
+ reader.readAsDataURL(file);
102221
+ });
102222
+ const filename = ext === 'png' ? 'product.png' : 'product.jpg';
102223
+ logToTerminal('log', '📤 Загрузка product.png/jpg...');
102224
+ await uploadImageToDrive(dataUrl, filename, folderId, addLog);
102225
+ logToTerminal('log', '✅ product.png/jpg загружен');
102226
+ setFolderFilesInfo(prev => prev ? { ...prev, hasProduct: true } : { hasProduct: true });
102227
+ alert('product.png/jpg успешно загружен в папку!');
102556
102228
  }
102557
102229
  catch (err) {
102558
- const errorMsg = 'Error uploading product: ' + err.message;
102559
- addLog(formatLogMessage('error', errorMsg));
102560
- logToTerminal('error', '❌ Error uploading product:', err);
102230
+ const errorMsg = 'Ошибка загрузки: ' + err.message;
102231
+ logToTerminal('error', '❌', errorMsg);
102561
102232
  alert(errorMsg);
102562
102233
  }
102563
102234
  finally {
@@ -102801,7 +102472,7 @@ function App() {
102801
102472
  addLog(formatLogMessage(level, ...args));
102802
102473
  };
102803
102474
  logMsg('log', '📦 Creating ZIP archive with HTML and product image...');
102804
- const zip = new (jszip__WEBPACK_IMPORTED_MODULE_56___default())();
102475
+ const zip = new (jszip__WEBPACK_IMPORTED_MODULE_51___default())();
102805
102476
  zip.file('index.html', htmlContent);
102806
102477
  zip.file('product.png', productImageBlob);
102807
102478
  logMsg('log', '✅ Files added to archive: index.html, product.png');
@@ -102948,477 +102619,476 @@ function App() {
102948
102619
  alert('OpenRouter API key is not set');
102949
102620
  return;
102950
102621
  }
102951
- const validToken = await getValidAccessToken();
102952
- if (!validToken) {
102953
- alert('Please log in with Google first');
102954
- return;
102955
- }
102956
102622
  setGeneratingLanding(true);
102957
102623
  setLandingGenerationLogs([]);
102958
102624
  setLandingProcessStartTime(Date.now());
102959
- const addLog = (msg) => {
102960
- setLandingGenerationLogs(prev => [...prev, msg]);
102961
- };
102962
102625
  try {
102963
- const folderId = extractFolderId(driveFolderUrl);
102964
- if (!folderId) {
102965
- throw new Error('Invalid Google Drive Folder URL');
102966
- }
102967
- // Get product image
102968
- addLog(formatLogMessage('log', '🔍 Получение изображения продукта...'));
102969
- logToTerminal('log', '🔍 Получение изображения продукта...');
102970
- const productImage = await fetchProductImage(folderId);
102971
- if (!productImage) {
102972
- throw new Error('product.png/jpg not found in the folder');
102973
- }
102974
- // Download product image as blob
102975
- addLog(formatLogMessage('log', '📥 Скачивание product.png...'));
102976
- logToTerminal('log', '📥 Скачивание product.png...');
102977
- let token = await getValidAccessToken();
102978
- if (!token) {
102979
- throw new Error('Not logged in to Google Drive');
102626
+ const validToken = await getValidAccessToken();
102627
+ if (!validToken) {
102628
+ alert('Please log in with Google first');
102629
+ setGeneratingLanding(false);
102630
+ return;
102980
102631
  }
102981
- let imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${productImage.id}?alt=media`, {
102982
- headers: {
102983
- 'Authorization': `Bearer ${token}`
102632
+ const addLog = (msg) => {
102633
+ setLandingGenerationLogs(prev => [...prev, msg]);
102634
+ };
102635
+ try {
102636
+ const folderId = extractFolderId(driveFolderUrl);
102637
+ if (!folderId) {
102638
+ throw new Error('Invalid Google Drive Folder URL');
102984
102639
  }
102985
- });
102986
- // Handle 401 - try to refresh token and retry
102987
- if (imageResponse.status === 401 && refreshToken) {
102988
- addLog(formatLogMessage('log', '🔄 Got 401, refreshing token...'));
102989
- const newToken = await refreshAccessToken(refreshToken);
102990
- if (newToken) {
102991
- token = newToken;
102992
- addLog(formatLogMessage('log', '✅ Token refreshed, retrying download...'));
102993
- imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${productImage.id}?alt=media`, {
102994
- headers: {
102995
- 'Authorization': `Bearer ${newToken}`
102996
- }
102640
+ // Get product image
102641
+ addLog(formatLogMessage('log', '🔍 Получение изображения продукта...'));
102642
+ logToTerminal('log', '🔍 Получение изображения продукта...');
102643
+ const productImage = await fetchProductImage(folderId);
102644
+ if (!productImage) {
102645
+ throw new Error('product.png/jpg not found in the folder');
102646
+ }
102647
+ // Download product image as blob
102648
+ addLog(formatLogMessage('log', '📥 Скачивание product.png...'));
102649
+ logToTerminal('log', '📥 Скачивание product.png...');
102650
+ let token = await getValidAccessToken();
102651
+ if (!token) {
102652
+ throw new Error('Not logged in to Google Drive');
102653
+ }
102654
+ let imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${productImage.id}?alt=media`, {
102655
+ headers: {
102656
+ 'Authorization': `Bearer ${token}`
102657
+ }
102658
+ });
102659
+ // Handle 401 - try to refresh token and retry
102660
+ if (imageResponse.status === 401 && refreshToken) {
102661
+ addLog(formatLogMessage('log', '🔄 Got 401, refreshing token...'));
102662
+ const newToken = await refreshAccessToken(refreshToken);
102663
+ if (newToken) {
102664
+ token = newToken;
102665
+ addLog(formatLogMessage('log', '✅ Token refreshed, retrying download...'));
102666
+ imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${productImage.id}?alt=media`, {
102667
+ headers: {
102668
+ 'Authorization': `Bearer ${newToken}`
102669
+ }
102670
+ });
102671
+ }
102672
+ }
102673
+ if (!imageResponse.ok) {
102674
+ throw new Error(`Failed to download product image: ${imageResponse.status} ${imageResponse.statusText}`);
102675
+ }
102676
+ const productImageBlob = await imageResponse.blob();
102677
+ addLog(formatLogMessage('log', '✅ Изображение продукта скачано'));
102678
+ logToTerminal('log', '✅ Изображение продукта скачано, размер:', (productImageBlob.size / 1024).toFixed(2), 'KB');
102679
+ // Generate HTML landing page
102680
+ if (!generateProduct.trim() || !generateGeo.trim()) {
102681
+ throw new Error('Please fill in Product and Geo fields');
102682
+ }
102683
+ addLog(formatLogMessage('log', '🎨 Генерация HTML лендинга...'));
102684
+ logToTerminal('log', '🎨 Генерация HTML лендинга...');
102685
+ const htmlContent = await generateLandingHTML(generateProduct, generateGeo, addLog);
102686
+ addLog(formatLogMessage('log', '✅ HTML лендинг сгенерирован'));
102687
+ // Save HTML and image for preview
102688
+ setGeneratedLandingHTML(htmlContent);
102689
+ setGeneratedLandingImageBlob(productImageBlob);
102690
+ // Open preview in browser immediately
102691
+ try {
102692
+ addLog(formatLogMessage('log', '👁️ Открытие предпросмотра в браузере...'));
102693
+ logToTerminal('log', '👁️ Открытие предпросмотра в браузере...');
102694
+ // Convert image blob to base64
102695
+ const imageBase64 = await new Promise((resolve, reject) => {
102696
+ const reader = new FileReader();
102697
+ reader.onloadend = () => {
102698
+ const base64String = reader.result;
102699
+ resolve(base64String);
102700
+ };
102701
+ reader.onerror = reject;
102702
+ reader.readAsDataURL(productImageBlob);
102997
102703
  });
102704
+ // Replace relative image path with base64 data URL in HTML
102705
+ const htmlWithImage = htmlContent.replace(/src=["']product\.(png|jpe?g)["']/gi, `src="${imageBase64}"`);
102706
+ // Create blob URL and open in new window
102707
+ const blob = new Blob([htmlWithImage], { type: 'text/html' });
102708
+ const url = URL.createObjectURL(blob);
102709
+ window.open(url, '_blank');
102710
+ addLog(formatLogMessage('log', '✅ Предпросмотр открыт в браузере'));
102711
+ logToTerminal('log', '✅ Предпросмотр открыт в браузере');
102712
+ // Clean up URL after a delay (give browser time to load)
102713
+ setTimeout(() => {
102714
+ URL.revokeObjectURL(url);
102715
+ }, 1000);
102998
102716
  }
102717
+ catch (previewErr) {
102718
+ addLog(formatLogMessage('warn', '⚠️ Не удалось открыть предпросмотр: ' + previewErr.message));
102719
+ logToTerminal('warn', '⚠️ Не удалось открыть предпросмотр:', previewErr);
102720
+ }
102721
+ // Create ZIP archive with HTML and product image
102722
+ addLog(formatLogMessage('log', '📦 Создание ZIP архива с HTML и изображением...'));
102723
+ logToTerminal('log', '📦 Создание ZIP архива...');
102724
+ const zipBlob = await createZipArchive(htmlContent, productImageBlob, addLog);
102725
+ addLog(formatLogMessage('log', '✅ ZIP архив создан'));
102726
+ // Upload ZIP to Google Drive
102727
+ const zipFilename = `landing_${Date.now()}.zip`;
102728
+ addLog(formatLogMessage('log', '📤 Загрузка ZIP архива на Google Drive...'));
102729
+ logToTerminal('log', '📤 Загрузка ZIP архива на Google Drive...');
102730
+ const driveLink = await uploadZipToDrive(zipBlob, zipFilename, folderId, addLog);
102731
+ addLog(formatLogMessage('log', '✅ ZIP архив загружен на Google Drive'));
102732
+ logToTerminal('log', '✅ ZIP архив загружен:', driveLink);
102999
102733
  }
103000
- if (!imageResponse.ok) {
103001
- throw new Error(`Failed to download product image: ${imageResponse.status} ${imageResponse.statusText}`);
103002
- }
103003
- const productImageBlob = await imageResponse.blob();
103004
- addLog(formatLogMessage('log', '✅ Изображение продукта скачано'));
103005
- logToTerminal('log', '✅ Изображение продукта скачано, размер:', (productImageBlob.size / 1024).toFixed(2), 'KB');
103006
- // Generate HTML landing page
103007
- if (!generateProduct.trim() || !generateGeo.trim()) {
103008
- throw new Error('Please fill in Product and Geo fields');
103009
- }
103010
- addLog(formatLogMessage('log', '🎨 Генерация HTML лендинга...'));
103011
- logToTerminal('log', '🎨 Генерация HTML лендинга...');
103012
- const htmlContent = await generateLandingHTML(generateProduct, generateGeo, addLog);
103013
- addLog(formatLogMessage('log', '✅ HTML лендинг сгенерирован'));
103014
- // Save HTML and image for preview
103015
- setGeneratedLandingHTML(htmlContent);
103016
- setGeneratedLandingImageBlob(productImageBlob);
103017
- // Open preview in browser immediately
103018
- try {
103019
- addLog(formatLogMessage('log', '👁️ Открытие предпросмотра в браузере...'));
103020
- logToTerminal('log', '👁️ Открытие предпросмотра в браузере...');
103021
- // Convert image blob to base64
103022
- const imageBase64 = await new Promise((resolve, reject) => {
103023
- const reader = new FileReader();
103024
- reader.onloadend = () => {
103025
- const base64String = reader.result;
103026
- resolve(base64String);
103027
- };
103028
- reader.onerror = reject;
103029
- reader.readAsDataURL(productImageBlob);
103030
- });
103031
- // Replace relative image path with base64 data URL in HTML
103032
- const htmlWithImage = htmlContent.replace(/src=["']product\.(png|jpe?g)["']/gi, `src="${imageBase64}"`);
103033
- // Create blob URL and open in new window
103034
- const blob = new Blob([htmlWithImage], { type: 'text/html' });
103035
- const url = URL.createObjectURL(blob);
103036
- window.open(url, '_blank');
103037
- addLog(formatLogMessage('log', '✅ Предпросмотр открыт в браузере'));
103038
- logToTerminal('log', '✅ Предпросмотр открыт в браузере');
103039
- // Clean up URL after a delay (give browser time to load)
103040
- setTimeout(() => {
103041
- URL.revokeObjectURL(url);
103042
- }, 1000);
102734
+ catch (err) {
102735
+ const errorMsg = 'Ошибка создания лендинга: ' + err.message;
102736
+ addLog(formatLogMessage('error', errorMsg));
102737
+ logToTerminal('error', '❌ Ошибка создания лендинга:', err);
102738
+ alert(errorMsg);
103043
102739
  }
103044
- catch (previewErr) {
103045
- addLog(formatLogMessage('warn', '⚠️ Не удалось открыть предпросмотр: ' + previewErr.message));
103046
- logToTerminal('warn', '⚠️ Не удалось открыть предпросмотр:', previewErr);
102740
+ finally {
102741
+ setGeneratingLanding(false);
103047
102742
  }
103048
- // Create ZIP archive with HTML and product image
103049
- addLog(formatLogMessage('log', '📦 Создание ZIP архива с HTML и изображением...'));
103050
- logToTerminal('log', '📦 Создание ZIP архива...');
103051
- const zipBlob = await createZipArchive(htmlContent, productImageBlob, addLog);
103052
- addLog(formatLogMessage('log', '✅ ZIP архив создан'));
103053
- // Upload ZIP to Google Drive
103054
- const zipFilename = `landing_${Date.now()}.zip`;
103055
- addLog(formatLogMessage('log', '📤 Загрузка ZIP архива на Google Drive...'));
103056
- logToTerminal('log', '📤 Загрузка ZIP архива на Google Drive...');
103057
- const driveLink = await uploadZipToDrive(zipBlob, zipFilename, folderId, addLog);
103058
- addLog(formatLogMessage('log', '✅ ZIP архив загружен на Google Drive'));
103059
- logToTerminal('log', '✅ ZIP архив загружен:', driveLink);
103060
102743
  }
103061
102744
  catch (err) {
103062
- const errorMsg = 'Ошибка создания лендинга: ' + err.message;
103063
- addLog(formatLogMessage('error', errorMsg));
103064
- logToTerminal('error', '❌ Ошибка создания лендинга:', err);
103065
- alert(errorMsg);
103066
- }
103067
- finally {
103068
102745
  setGeneratingLanding(false);
102746
+ logToTerminal('error', '❌ Error:', err?.message || err);
103069
102747
  }
103070
102748
  };
103071
102749
  const handleGenerateImages = async () => {
103072
- logToTerminal('log', '🎨 === Starting image generation process ===');
103073
- logToTerminal('log', '📅 Timestamp:', new Date().toISOString());
103074
- const validToken = await getValidAccessToken();
103075
- if (!validToken) {
103076
- logToTerminal('error', '❌ Not logged in to Google');
103077
- alert('Please log in with Google first');
103078
- return;
103079
- }
103080
- logToTerminal('log', '✅ Google authentication OK');
103081
- if (!generateProduct.trim() || !generateGeo.trim()) {
103082
- logToTerminal('error', '❌ Missing product or geo');
103083
- alert('Please fill in Product and Geo fields');
103084
- return;
103085
- }
103086
- const { price: generatePrice, currency: generateCurrency, currencySymbol } = parsePriceAndCurrency(generatePriceWithCurrency);
103087
- // Use original currency symbol if available, otherwise use currency code
103088
- const currencyForPrompt = currencySymbol || generateCurrency;
103089
- logToTerminal('log', '📦 Product:', generateProduct);
103090
- logToTerminal('log', '🌍 Geo:', generateGeo);
103091
- logToTerminal('log', '💰 Price:', generatePrice, currencyForPrompt);
103092
- if (!driveFolderUrl.trim()) {
103093
- logToTerminal('error', '❌ Missing Drive folder URL');
103094
- alert('Please fill in Google Drive Folder URL');
103095
- return;
103096
- }
103097
- logToTerminal('log', '📁 Drive folder URL:', driveFolderUrl);
103098
102750
  setGeneratingImages(true);
103099
102751
  setImagesGenerationLogs([]);
103100
102752
  setImagesProcessStartTime(Date.now());
103101
102753
  setCheckingImages(false);
103102
- const overallStartTime = Date.now();
103103
- const addLog = (msg) => {
103104
- setImagesGenerationLogs(prev => [...prev, msg]);
103105
- };
102754
+ logToTerminal('log', '🎨 === Starting image generation process ===');
102755
+ logToTerminal('log', '📅 Timestamp:', new Date().toISOString());
103106
102756
  try {
103107
- const folderId = extractFolderId(driveFolderUrl);
103108
- if (!folderId) {
103109
- const errorMsg = 'Invalid Google Drive Folder URL';
103110
- addLog(formatLogMessage('error', '❌ Invalid folder ID extracted'));
103111
- throw new Error(errorMsg);
102757
+ const validToken = await getValidAccessToken();
102758
+ if (!validToken) {
102759
+ logToTerminal('error', '❌ Not logged in to Google');
102760
+ alert('Please log in with Google first');
102761
+ setGeneratingImages(false);
102762
+ return;
103112
102763
  }
103113
- addLog(formatLogMessage('log', '✅ Folder ID extracted:', folderId));
103114
- // Check for product.png
103115
- addLog(formatLogMessage('log', '🔍 Checking for product.png...'));
103116
- const productImage = await fetchProductImage(folderId);
103117
- if (!productImage) {
103118
- const errorMsg = 'product.png/jpg not found in the folder. Please generate it first using "Generate Product from Banka" button.';
103119
- addLog(formatLogMessage('error', errorMsg));
103120
- throw new Error(errorMsg);
102764
+ logToTerminal('log', '✅ Google authentication OK');
102765
+ if (!generateProduct.trim() || !generateGeo.trim()) {
102766
+ logToTerminal('error', ' Missing product or geo');
102767
+ alert('Please fill in Product and Geo fields');
102768
+ setGeneratingImages(false);
102769
+ return;
103121
102770
  }
103122
- addLog(formatLogMessage('log', '✅ product.png found'));
103123
- // Generate images with different approaches (количество по каждому подходу)
103124
- const approaches = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getCreoApproaches)();
103125
- if (approaches.length === 0) {
103126
- addLog(formatLogMessage('error', ' Укажите количество изображений (хотя бы 1) в настройках подходов'));
103127
- alert('Укажите количество изображений (1–4) хотя бы для одного подхода в настройках');
102771
+ const { price: generatePrice, currency: generateCurrency, currencySymbol } = parsePriceAndCurrency(generatePriceWithCurrency);
102772
+ const currencyForPrompt = currencySymbol || generateCurrency;
102773
+ logToTerminal('log', '📦 Product:', generateProduct);
102774
+ logToTerminal('log', '🌍 Geo:', generateGeo);
102775
+ logToTerminal('log', '💰 Price:', generatePrice, currencyForPrompt);
102776
+ if (!driveFolderUrl.trim()) {
102777
+ logToTerminal('error', '❌ Missing Drive folder URL');
102778
+ alert('Please fill in Google Drive Folder URL');
103128
102779
  setGeneratingImages(false);
103129
102780
  return;
103130
102781
  }
103131
- // При "both" на каждый подход генерируем и 1:1, и 2:3
103132
- const useBoth = imageAspectRatio === 'both';
103133
- const tasks = useBoth
103134
- ? approaches.flatMap(a => [
103135
- { approach: a, ratio: '1:1' },
103136
- { approach: a, ratio: '2:3' }
103137
- ])
103138
- : approaches.map(a => ({ approach: a, ratio: imageAspectRatio }));
103139
- const additionalInfoLine = generateAdditionalInfo.trim()
103140
- ? `\n📋 ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ О ПРОДУКТЕ: ${generateAdditionalInfo.trim()}`
103141
- : '';
103142
- const imagePrompts = tasks.map(t => {
103143
- 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 обязателен; буллиты добавляй только если они разрешены для подхода).`;
103145
- });
103146
- // Initialize placeholders for all images
103147
- const initialPlaceholders = tasks.map((t, index) => ({
103148
- index: index + 1,
103149
- imageUrl: undefined,
103150
- approach: t.approach.name + (useBoth ? ` (${t.ratio})` : ''),
103151
- aspectRatio: t.ratio,
103152
- uploaded: false,
103153
- checking: false,
103154
- checkStatus: 'pending',
103155
- checkResult: undefined,
103156
- checkErrors: undefined,
103157
- originalPrompt: undefined,
103158
- productImageUrl: undefined,
103159
- regenerating: false,
103160
- customRegeneratePrompt: '',
103161
- failed: false,
103162
- generating: false // In sequential mode, images start queued (not generating)
103163
- }));
103164
- setGeneratedImagesData(initialPlaceholders);
103165
- addLog(formatLogMessage('log', `📝 Generated prompts for ${tasks.length} images${useBoth ? ' (1:1 + 2:3 на каждый подход)' : ''}`));
103166
- const maxParallel = 100; // like infinity
103167
- addLog(formatLogMessage('log', `🚀 Generating ${tasks.length} images in parallel (up to ${maxParallel} at a time)...`));
103168
- const generationStartTime = Date.now();
103169
- const resultsMap = new Map();
103170
- // Ensure each slot has prompt+product reference from the start (needed for regeneration even on failures)
103171
- setGeneratedImagesData(prev => prev.map((img, i) => ({
103172
- ...img,
103173
- originalPrompt: img.originalPrompt || imagePrompts[i],
103174
- productImageUrl: img.productImageUrl || productImage.url
103175
- })));
103176
- const runOne = async (i, isRetry) => {
103177
- const imageIndex = i + 1;
103178
- const task = tasks[i];
103179
- const approachName = task?.approach.name || 'Unknown';
103180
- const prompt = imagePrompts[i];
103181
- const ratio = task?.ratio || '1:1';
103182
- addLog(formatLogMessage('log', `${isRetry ? '🔄' : '🎨'} Генерация изображения ${imageIndex}/${tasks.length} (${approachName} ${ratio})...`));
103183
- // Reset slot state for this run
103184
- setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
103185
- ? {
103186
- ...img,
103187
- generating: true,
103188
- failed: false,
103189
- errorMessage: undefined,
103190
- imageUrl: undefined,
103191
- checking: false,
103192
- checkStatus: 'pending',
103193
- checkResult: undefined,
103194
- checkErrors: undefined,
103195
- originalPrompt: prompt,
103196
- productImageUrl: productImage.url
103197
- }
103198
- : img));
103199
- try {
103200
- const imageUrl = await generateImageWithDALLE(prompt, productImage.url, ratio);
103201
- if (!imageUrl || typeof imageUrl !== 'string' || imageUrl.trim() === '') {
103202
- throw new Error('Generated image URL is empty or invalid');
103203
- }
103204
- // Update UI immediately when image is ready
102782
+ logToTerminal('log', '📁 Drive folder URL:', driveFolderUrl);
102783
+ const overallStartTime = Date.now();
102784
+ const addLog = (msg) => {
102785
+ setImagesGenerationLogs(prev => [...prev, msg]);
102786
+ };
102787
+ try {
102788
+ const folderId = extractFolderId(driveFolderUrl);
102789
+ if (!folderId) {
102790
+ const errorMsg = 'Invalid Google Drive Folder URL';
102791
+ addLog(formatLogMessage('error', '❌ Invalid folder ID extracted'));
102792
+ throw new Error(errorMsg);
102793
+ }
102794
+ addLog(formatLogMessage('log', '✅ Folder ID extracted:', folderId));
102795
+ // Check for product.png
102796
+ addLog(formatLogMessage('log', '🔍 Checking for product.png...'));
102797
+ const productImage = await fetchProductImage(folderId);
102798
+ if (!productImage) {
102799
+ const errorMsg = 'product.png/jpg not found in the folder. Please generate it first using "Generate Product from Banka" button.';
102800
+ addLog(formatLogMessage('error', errorMsg));
102801
+ throw new Error(errorMsg);
102802
+ }
102803
+ addLog(formatLogMessage('log', '✅ product.png found'));
102804
+ // Generate images with different approaches (количество по каждому подходу)
102805
+ const approaches = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getCreoApproaches)();
102806
+ if (approaches.length === 0) {
102807
+ addLog(formatLogMessage('error', '❌ Укажите количество изображений (хотя бы 1) в настройках подходов'));
102808
+ alert('Укажите количество изображений (1–4) хотя бы для одного подхода в настройках');
102809
+ setGeneratingImages(false);
102810
+ return;
102811
+ }
102812
+ // При "both" — на каждый подход генерируем и 1:1, и 2:3
102813
+ const useBoth = imageAspectRatio === 'both';
102814
+ const tasks = useBoth
102815
+ ? approaches.flatMap(a => [
102816
+ { approach: a, ratio: '1:1' },
102817
+ { approach: a, ratio: '2:3' }
102818
+ ])
102819
+ : approaches.map(a => ({ approach: a, ratio: imageAspectRatio }));
102820
+ const additionalInfoLine = generateAdditionalInfo.trim()
102821
+ ? `\n📋 ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ О ПРОДУКТЕ: ${generateAdditionalInfo.trim()}`
102822
+ : '';
102823
+ const imagePrompts = tasks.map(t => {
102824
+ const basePromptStructure = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getImageGenerationBasePrompt)(generateGeo, generatePrice, currencyForPrompt, undefined, t.ratio);
102825
+ const bulletsLine = t.approach.noBullets
102826
+ ? t.approach.bulletsFocus
102827
+ : (() => {
102828
+ const [b1, b2, b3] = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.pickRandomBullets)(t.approach.name);
102829
+ return `ОБЯЗАТЕЛЬНЫЕ БУЛЛЕТЫ (используй именно эти 3, в указанном порядке; переведи на язык ${generateGeo}): 1) ${b1} 2) ${b2} 3) ${b3}`;
102830
+ })();
102831
+ 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 обязателен; буллиты добавляй только если они разрешены для подхода).`;
102832
+ });
102833
+ // Initialize placeholders for all images
102834
+ const initialPlaceholders = tasks.map((t, index) => ({
102835
+ index: index + 1,
102836
+ imageUrl: undefined,
102837
+ approach: t.approach.name + (useBoth ? ` (${t.ratio})` : ''),
102838
+ aspectRatio: t.ratio,
102839
+ uploaded: false,
102840
+ checking: false,
102841
+ checkStatus: 'pending',
102842
+ checkResult: undefined,
102843
+ checkErrors: undefined,
102844
+ originalPrompt: undefined,
102845
+ productImageUrl: undefined,
102846
+ regenerating: false,
102847
+ customRegeneratePrompt: '',
102848
+ failed: false,
102849
+ generating: false // In sequential mode, images start queued (not generating)
102850
+ }));
102851
+ setGeneratedImagesData(initialPlaceholders);
102852
+ addLog(formatLogMessage('log', `📝 Generated prompts for ${tasks.length} images${useBoth ? ' (1:1 + 2:3 на каждый подход)' : ''}`));
102853
+ const maxParallel = 100; // like infinity
102854
+ addLog(formatLogMessage('log', `🚀 Generating ${tasks.length} images in parallel (up to ${maxParallel} at a time)...`));
102855
+ const generationStartTime = Date.now();
102856
+ const resultsMap = new Map();
102857
+ // Ensure each slot has prompt+product reference from the start (needed for regeneration even on failures)
102858
+ setGeneratedImagesData(prev => prev.map((img, i) => ({
102859
+ ...img,
102860
+ originalPrompt: img.originalPrompt || imagePrompts[i],
102861
+ productImageUrl: img.productImageUrl || productImage.url
102862
+ })));
102863
+ const runOne = async (i, isRetry) => {
102864
+ const imageIndex = i + 1;
102865
+ const task = tasks[i];
102866
+ const approachName = task?.approach.name || 'Unknown';
102867
+ const prompt = imagePrompts[i];
102868
+ const ratio = task?.ratio || '1:1';
102869
+ addLog(formatLogMessage('log', `${isRetry ? '🔄' : '🎨'} Генерация изображения ${imageIndex}/${tasks.length} (${approachName} ${ratio})...`));
102870
+ // Reset slot state for this run
103205
102871
  setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
103206
102872
  ? {
103207
102873
  ...img,
103208
- imageUrl,
103209
- generating: false,
102874
+ generating: true,
103210
102875
  failed: false,
103211
- checking: true,
103212
- checkStatus: 'checking',
102876
+ errorMessage: undefined,
102877
+ imageUrl: undefined,
102878
+ checking: false,
102879
+ checkStatus: 'pending',
102880
+ checkResult: undefined,
102881
+ checkErrors: undefined,
103213
102882
  originalPrompt: prompt,
103214
102883
  productImageUrl: productImage.url
103215
102884
  }
103216
102885
  : img));
103217
- addLog(formatLogMessage('log', `✅ Изображение ${imageIndex}/${tasks.length} сгенерировано`));
103218
- // Validate right after generation (skip if validation disabled)
103219
- const validationResult = validationDisabled
103220
- ? { status: 'ok', result: 'Проверка отключена', errors: [], checkFailed: false }
103221
- : await (async () => {
103222
- try {
103223
- const res = await validateCreativeImage(imageUrl, generateProduct, generateGeo, addLog, approachName);
103224
- if (!res)
103225
- throw new Error('No validation result');
103226
- return { ...res, checkFailed: false };
103227
- }
103228
- catch (validationErr) {
103229
- const msg = validationErr?.message || String(validationErr);
103230
- addLog(formatLogMessage('error', `❌ Ошибка проверки изображения ${imageIndex}: ${msg}`));
103231
- return {
103232
- status: 'needs_rebuild',
103233
- result: `Ошибка проверки: ${msg}`,
103234
- errors: [msg],
103235
- checkFailed: true
103236
- };
103237
- }
103238
- })();
103239
- setGeneratedImagesData(prev => {
103240
- const updated = prev.map(img => img.index === imageIndex
102886
+ try {
102887
+ const imageUrl = await generateImageWithDALLE(prompt, productImage.url, ratio);
102888
+ if (!imageUrl || typeof imageUrl !== 'string' || imageUrl.trim() === '') {
102889
+ throw new Error('Generated image URL is empty or invalid');
102890
+ }
102891
+ // Update UI immediately when image is ready
102892
+ setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
103241
102893
  ? {
103242
102894
  ...img,
103243
- checking: false,
103244
- checkFailed: validationResult.checkFailed ?? false,
103245
- checkStatus: validationResult.status,
103246
- checkResult: validationResult.result,
103247
- checkErrors: validationResult.errors,
103248
- customRegeneratePrompt: validationResult.errors.length > 0
103249
- ? validationResult.errors.map(err => err.replace(/^ОШИБКА:\s*/i, '')).join('\n')
103250
- : '',
103251
- originalPrompt: img.originalPrompt || prompt,
103252
- productImageUrl: img.productImageUrl || productImage.url
102895
+ imageUrl,
102896
+ generating: false,
102897
+ failed: false,
102898
+ checking: true,
102899
+ checkStatus: 'checking',
102900
+ originalPrompt: prompt,
102901
+ productImageUrl: productImage.url
103253
102902
  }
103254
- : img);
103255
- return updated;
103256
- });
103257
- return {
103258
- index: imageIndex,
103259
- imageUrl,
103260
- success: true,
103261
- approach: approachName,
103262
- originalPrompt: prompt,
103263
- productImageUrl: productImage.url
103264
- };
103265
- }
103266
- catch (err) {
103267
- const errorMessage = err?.message || String(err);
103268
- addLog(formatLogMessage('error', `❌ Не удалось сгенерировать изображение ${imageIndex}/${tasks.length}: ${errorMessage}`));
103269
- setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
103270
- ? {
103271
- ...img,
103272
- generating: false,
103273
- failed: true,
103274
- errorMessage,
103275
- checking: false,
102903
+ : img));
102904
+ addLog(formatLogMessage('log', `✅ Изображение ${imageIndex}/${tasks.length} сгенерировано`));
102905
+ // Validate right after generation (skip if validation disabled)
102906
+ const validationResult = validationDisabled
102907
+ ? { status: 'ok', result: 'Проверка отключена', errors: [], checkFailed: false }
102908
+ : await (async () => {
102909
+ try {
102910
+ const res = await validateCreativeImage(imageUrl, generateProduct, generateGeo, addLog, approachName);
102911
+ if (!res)
102912
+ throw new Error('No validation result');
102913
+ return { ...res, checkFailed: false };
102914
+ }
102915
+ catch (validationErr) {
102916
+ const msg = validationErr?.message || String(validationErr);
102917
+ addLog(formatLogMessage('error', `❌ Ошибка проверки изображения ${imageIndex}: ${msg}`));
102918
+ return {
102919
+ status: 'needs_rebuild',
102920
+ result: `Ошибка проверки: ${msg}`,
102921
+ errors: [msg],
102922
+ checkFailed: true
102923
+ };
102924
+ }
102925
+ })();
102926
+ setGeneratedImagesData(prev => {
102927
+ const updated = prev.map(img => img.index === imageIndex
102928
+ ? {
102929
+ ...img,
102930
+ checking: false,
102931
+ checkFailed: validationResult.checkFailed ?? false,
102932
+ checkStatus: validationResult.status,
102933
+ checkResult: validationResult.result,
102934
+ checkErrors: validationResult.errors,
102935
+ customRegeneratePrompt: validationResult.errors.length > 0
102936
+ ? validationResult.errors.map(err => err.replace(/^ОШИБКА:\s*/i, '')).join('\n')
102937
+ : '',
102938
+ originalPrompt: img.originalPrompt || prompt,
102939
+ productImageUrl: img.productImageUrl || productImage.url
102940
+ }
102941
+ : img);
102942
+ return updated;
102943
+ });
102944
+ return {
102945
+ index: imageIndex,
102946
+ imageUrl,
102947
+ success: true,
102948
+ approach: approachName,
103276
102949
  originalPrompt: prompt,
103277
102950
  productImageUrl: productImage.url
103278
- }
103279
- : img));
103280
- return {
103281
- index: imageIndex,
103282
- imageUrl: null,
103283
- success: false,
103284
- error: err,
103285
- approach: approachName,
103286
- originalPrompt: prompt,
103287
- productImageUrl: productImage.url
103288
- };
103289
- }
103290
- finally {
103291
- // Small yield to keep UI responsive in Electron
103292
- await new Promise(resolve => setTimeout(resolve, 150));
103293
- }
103294
- };
103295
- // First pass (limited concurrency)
103296
- let cursor = 0;
103297
- const workerCount = Math.min(maxParallel, imagePrompts.length);
103298
- const workers = Array.from({ length: workerCount }, () => (async () => {
103299
- while (true) {
103300
- const i = cursor++;
103301
- if (i >= imagePrompts.length)
103302
- break;
103303
- const res = await runOne(i, false);
103304
- resultsMap.set(res.index, res);
103305
- }
103306
- })());
103307
- await Promise.all(workers);
103308
- let generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
103309
- // Safety: fill any missing slots as failed
103310
- if (generationResults.length !== imagePrompts.length) {
103311
- for (let i = 0; i < imagePrompts.length; i++) {
103312
- const idx = i + 1;
103313
- if (!resultsMap.has(idx)) {
103314
- generationResults.push({
103315
- index: idx,
102951
+ };
102952
+ }
102953
+ catch (err) {
102954
+ const errorMessage = err?.message || String(err);
102955
+ addLog(formatLogMessage('error', `❌ Не удалось сгенерировать изображение ${imageIndex}/${tasks.length}: ${errorMessage}`));
102956
+ setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
102957
+ ? {
102958
+ ...img,
102959
+ generating: false,
102960
+ failed: true,
102961
+ errorMessage,
102962
+ checking: false,
102963
+ originalPrompt: prompt,
102964
+ productImageUrl: productImage.url
102965
+ }
102966
+ : img));
102967
+ return {
102968
+ index: imageIndex,
103316
102969
  imageUrl: null,
103317
102970
  success: false,
103318
- error: new Error('Generation did not complete'),
103319
- approach: approaches[i]?.name || 'Unknown',
103320
- originalPrompt: imagePrompts[i],
102971
+ error: err,
102972
+ approach: approachName,
102973
+ originalPrompt: prompt,
103321
102974
  productImageUrl: productImage.url
103322
- });
102975
+ };
103323
102976
  }
103324
- }
103325
- generationResults = generationResults.sort((a, b) => a.index - b.index);
103326
- }
103327
- // Retry once for all transient failures including refusals (model sometimes randomly refuses)
103328
- const retryableErrors = generationResults.filter(r => {
103329
- if (r.success)
103330
- return false;
103331
- const errorMsg = r.error?.message || '';
103332
- const isRefusal = r.error?.isRefusal === true;
103333
- // isRefusal и hasReasoning — случайные отказы модели → ретраим с логом
103334
- // Всё остальное (сеть/JSON/таймаут/Provider) тоже ретраим
103335
- return (isRefusal ||
103336
- r.error?.hasReasoning === true ||
103337
- errorMsg.includes('Invalid JSON response') ||
103338
- errorMsg.includes('Unexpected end of JSON input') ||
103339
- (errorMsg.includes('JSON') && errorMsg.includes('parse')) ||
103340
- errorMsg.includes('Failed to fetch') ||
103341
- errorMsg.includes('Network error') ||
103342
- errorMsg.includes('timeout') ||
103343
- errorMsg.includes('Request timeout') ||
103344
- errorMsg.includes('Provider returned error') ||
103345
- errorMsg.toLowerCase().includes('provider error'));
103346
- });
103347
- if (retryableErrors.length > 0) {
103348
- const refusalCount = retryableErrors.filter(r => r.error?.isRefusal === true).length;
103349
- const otherCount = retryableErrors.length - refusalCount;
103350
- const parts = [];
103351
- if (refusalCount > 0)
103352
- parts.push(`${refusalCount} отказ${refusalCount > 1 ? 'а' : ''} модели`);
103353
- if (otherCount > 0)
103354
- parts.push(`${otherCount} сеть/таймаут/JSON`);
103355
- addLog(formatLogMessage('log', `🔄 Повтор: ${parts.join(', ')}. Пробую ещё раз...`));
103356
- const retryIndices = retryableErrors.map(r => r.index).sort((a, b) => a - b);
103357
- let retryCursor = 0;
103358
- const retryWorkers = Array.from({ length: Math.min(maxParallel, retryIndices.length) }, () => (async () => {
102977
+ finally {
102978
+ // Small yield to keep UI responsive in Electron
102979
+ await new Promise(resolve => setTimeout(resolve, 150));
102980
+ }
102981
+ };
102982
+ // First pass (limited concurrency)
102983
+ let cursor = 0;
102984
+ const workerCount = Math.min(maxParallel, imagePrompts.length);
102985
+ const workers = Array.from({ length: workerCount }, () => (async () => {
103359
102986
  while (true) {
103360
- const idx = retryIndices[retryCursor++];
103361
- if (idx === undefined)
102987
+ const i = cursor++;
102988
+ if (i >= imagePrompts.length)
103362
102989
  break;
103363
- // backoff
103364
- await new Promise(resolve => setTimeout(resolve, 1200));
103365
- const i = idx - 1;
103366
- const res = await runOne(i, true);
102990
+ const res = await runOne(i, false);
103367
102991
  resultsMap.set(res.index, res);
103368
102992
  }
103369
102993
  })());
103370
- await Promise.all(retryWorkers);
103371
- generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
103372
- }
103373
- const generationDuration = Date.now() - generationStartTime;
103374
- const successfulImages = generationResults.filter(r => r.success);
103375
- const finalFailedImages = generationResults.filter(r => !r.success);
103376
- addLog(formatLogMessage('log', `⏱️ Генерация завершена за ${Math.round(generationDuration / 1000)}s`));
103377
- addLog(formatLogMessage('log', `✅ Успешно: ${successfulImages.length}/${imagePrompts.length}, ошибок: ${finalFailedImages.length}`));
103378
- if (successfulImages.length === 0) {
103379
- const allErrors = finalFailedImages.map(f => f.error?.message || String(f.error || 'Unknown error')).join('; ');
103380
- addLog(formatLogMessage('warn', `⚠️ No images were successfully generated. All ${imagePrompts.length} slots are available for regeneration.`));
103381
- // Don't throw error - just show empty slots with regenerate buttons
103382
- }
103383
- const overallDuration = Date.now() - overallStartTime;
103384
- addLog(formatLogMessage('log', `🎉 === Image generation process completed ===`));
103385
- addLog(formatLogMessage('log', `⏱️ Total time: ${overallDuration}ms (${(overallDuration / 1000).toFixed(2)}s)`));
103386
- addLog(formatLogMessage('log', `✅ Successfully generated ${successfulImages.length}/${imagePrompts.length} images`));
103387
- // Stop generating state to hide backdrop and show images
103388
- setGeneratingImages(false);
103389
- setCheckingImages(false);
103390
- // Save generated images to Google Drive
103391
- if (driveFolderUrl) {
103392
- try {
103393
- const folderId = extractFolderId(driveFolderUrl);
103394
- if (folderId) {
103395
- addLog(formatLogMessage('log', '💾 Saving generated images data to Google Drive...'));
103396
- // Get latest state and save
103397
- setGeneratedImagesData(prev => {
103398
- // Save all images to separate files
103399
- saveGeneratedImagesToDrive(folderId, prev).then(() => {
103400
- addLog(formatLogMessage('log', '✅ Generated images data saved to Google Drive'));
103401
- }).catch((err) => {
103402
- addLog(formatLogMessage('warn', `⚠️ Failed to save images data to Google Drive: ${err.message || 'Unknown error'}`));
102994
+ await Promise.all(workers);
102995
+ let generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
102996
+ // Safety: fill any missing slots as failed
102997
+ if (generationResults.length !== imagePrompts.length) {
102998
+ for (let i = 0; i < imagePrompts.length; i++) {
102999
+ const idx = i + 1;
103000
+ if (!resultsMap.has(idx)) {
103001
+ generationResults.push({
103002
+ index: idx,
103003
+ imageUrl: null,
103004
+ success: false,
103005
+ error: new Error('Generation did not complete'),
103006
+ approach: approaches[i]?.name || 'Unknown',
103007
+ originalPrompt: imagePrompts[i],
103008
+ productImageUrl: productImage.url
103403
103009
  });
103404
- return prev;
103405
- });
103010
+ }
103406
103011
  }
103012
+ generationResults = generationResults.sort((a, b) => a.index - b.index);
103407
103013
  }
103408
- catch (err) {
103409
- addLog(formatLogMessage('warn', `⚠️ Failed to save images data to Google Drive: ${err.message || 'Unknown error'}`));
103014
+ // Retry once for all transient failures including refusals (model sometimes randomly refuses)
103015
+ const retryableErrors = generationResults.filter(r => {
103016
+ if (r.success)
103017
+ return false;
103018
+ const errorMsg = r.error?.message || '';
103019
+ const isRefusal = r.error?.isRefusal === true;
103020
+ // isRefusal и hasReasoning — случайные отказы модели → ретраим с логом
103021
+ // Всё остальное (сеть/JSON/таймаут/Provider) тоже ретраим
103022
+ return (isRefusal ||
103023
+ r.error?.hasReasoning === true ||
103024
+ errorMsg.includes('Invalid JSON response') ||
103025
+ errorMsg.includes('Unexpected end of JSON input') ||
103026
+ (errorMsg.includes('JSON') && errorMsg.includes('parse')) ||
103027
+ errorMsg.includes('Failed to fetch') ||
103028
+ errorMsg.includes('Network error') ||
103029
+ errorMsg.includes('timeout') ||
103030
+ errorMsg.includes('Request timeout') ||
103031
+ errorMsg.includes('Provider returned error') ||
103032
+ errorMsg.toLowerCase().includes('provider error'));
103033
+ });
103034
+ if (retryableErrors.length > 0) {
103035
+ const refusalCount = retryableErrors.filter(r => r.error?.isRefusal === true).length;
103036
+ const otherCount = retryableErrors.length - refusalCount;
103037
+ const parts = [];
103038
+ if (refusalCount > 0)
103039
+ parts.push(`${refusalCount} отказ${refusalCount > 1 ? 'а' : ''} модели`);
103040
+ if (otherCount > 0)
103041
+ parts.push(`${otherCount} сеть/таймаут/JSON`);
103042
+ addLog(formatLogMessage('log', `🔄 Повтор: ${parts.join(', ')}. Пробую ещё раз...`));
103043
+ const retryIndices = retryableErrors.map(r => r.index).sort((a, b) => a - b);
103044
+ let retryCursor = 0;
103045
+ const retryWorkers = Array.from({ length: Math.min(maxParallel, retryIndices.length) }, () => (async () => {
103046
+ while (true) {
103047
+ const idx = retryIndices[retryCursor++];
103048
+ if (idx === undefined)
103049
+ break;
103050
+ // backoff
103051
+ await new Promise(resolve => setTimeout(resolve, 1200));
103052
+ const i = idx - 1;
103053
+ const res = await runOne(i, true);
103054
+ resultsMap.set(res.index, res);
103055
+ }
103056
+ })());
103057
+ await Promise.all(retryWorkers);
103058
+ generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
103059
+ }
103060
+ const generationDuration = Date.now() - generationStartTime;
103061
+ const successfulImages = generationResults.filter(r => r.success);
103062
+ const finalFailedImages = generationResults.filter(r => !r.success);
103063
+ addLog(formatLogMessage('log', `⏱️ Генерация завершена за ${Math.round(generationDuration / 1000)}s`));
103064
+ addLog(formatLogMessage('log', `✅ Успешно: ${successfulImages.length}/${imagePrompts.length}, ошибок: ${finalFailedImages.length}`));
103065
+ if (successfulImages.length === 0) {
103066
+ const allErrors = finalFailedImages.map(f => f.error?.message || String(f.error || 'Unknown error')).join('; ');
103067
+ addLog(formatLogMessage('warn', `⚠️ No images were successfully generated. All ${imagePrompts.length} slots are available for regeneration.`));
103068
+ // Don't throw error - just show empty slots with regenerate buttons
103410
103069
  }
103070
+ const overallDuration = Date.now() - overallStartTime;
103071
+ addLog(formatLogMessage('log', `🎉 === Image generation process completed ===`));
103072
+ addLog(formatLogMessage('log', `⏱️ Total time: ${overallDuration}ms (${(overallDuration / 1000).toFixed(2)}s)`));
103073
+ addLog(formatLogMessage('log', `✅ Successfully generated ${successfulImages.length}/${imagePrompts.length} images`));
103074
+ // Stop generating state to hide backdrop and show images
103075
+ setGeneratingImages(false);
103076
+ setCheckingImages(false);
103077
+ // Validation is done individually per image; no final summary block
103078
+ }
103079
+ catch (err) {
103080
+ const errorMessage = err.message || 'Unknown error';
103081
+ addLog(formatLogMessage('error', '❌ === Error in image generation process ==='));
103082
+ addLog(formatLogMessage('error', errorMessage));
103083
+ alert('Error generating images: ' + errorMessage);
103084
+ }
103085
+ finally {
103086
+ setGeneratingImages(false);
103411
103087
  }
103412
- // Validation is done individually per image; no final summary block
103413
103088
  }
103414
103089
  catch (err) {
103415
- const errorMessage = err.message || 'Unknown error';
103416
- addLog(formatLogMessage('error', '❌ === Error in image generation process ==='));
103417
- addLog(formatLogMessage('error', errorMessage));
103418
- alert('Error generating images: ' + errorMessage);
103419
- }
103420
- finally {
103421
103090
  setGeneratingImages(false);
103091
+ logToTerminal('error', '❌ Error:', err?.message || err);
103422
103092
  }
103423
103093
  };
103424
103094
  const handleRegenerateImage = async (imageData) => {
@@ -103828,23 +103498,8 @@ ${imageData.originalPrompt}
103828
103498
  addLog(formatLogMessage('log', `📤 Uploading image ${imageData.index} to Drive: ${filename}`));
103829
103499
  const driveUrl = await uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog);
103830
103500
  addLog(formatLogMessage('log', `✅ Image ${imageData.index} uploaded: ${driveUrl}`));
103831
- // Update uploaded status and save single image to Drive
103832
- setGeneratedImagesData(prev => {
103833
- const updated = prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img);
103834
- // Save single image to its own file
103835
- if (driveFolderUrl) {
103836
- const currentFolderId = extractFolderId(driveFolderUrl);
103837
- if (currentFolderId) {
103838
- const updatedImage = updated.find(img => img.index === imageData.index);
103839
- if (updatedImage) {
103840
- saveSingleImageToDrive(currentFolderId, updatedImage).catch((err) => {
103841
- console.log(`Failed to save image ${imageData.index} to Drive:`, err);
103842
- });
103843
- }
103844
- }
103845
- }
103846
- return updated;
103847
- });
103501
+ // Update uploaded status
103502
+ setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img));
103848
103503
  // (Removed legacy Generated Images links list)
103849
103504
  }
103850
103505
  catch (err) {
@@ -103867,7 +103522,7 @@ ${imageData.originalPrompt}
103867
103522
  alert('Invalid Google Drive Folder URL');
103868
103523
  return;
103869
103524
  }
103870
- const notUploaded = generatedImagesData.filter(img => !img.uploaded);
103525
+ const notUploaded = generatedImagesData.filter(img => !img.uploaded && img.imageUrl);
103871
103526
  if (notUploaded.length === 0) {
103872
103527
  alert('All images are already uploaded');
103873
103528
  return;
@@ -103877,57 +103532,53 @@ ${imageData.originalPrompt}
103877
103532
  setImagesGenerationLogs(prev => [...prev, msg]);
103878
103533
  logToTerminal('log', msg.replace(/\[.*?\]\s*/, ''));
103879
103534
  };
103535
+ // Mark all as uploading
103536
+ setGeneratedImagesData(prev => prev.map(img => notUploaded.some(n => n.index === img.index)
103537
+ ? { ...img, uploading: true, uploadStartTime: Date.now() }
103538
+ : img));
103880
103539
  try {
103881
- addLog(formatLogMessage('log', `📤 Uploading ${notUploaded.length} image(s) to Drive...`));
103882
- let uploadedCount = 0;
103883
- for (const imageData of notUploaded) {
103884
- if (!imageData.imageUrl) {
103885
- addLog(formatLogMessage('warn', `⚠️ Skipping image ${imageData.index} - no image URL`));
103886
- continue;
103540
+ addLog(formatLogMessage('log', `📤 Uploading ${notUploaded.length} image(s) to Drive (parallel)...`));
103541
+ const baseTs = Date.now();
103542
+ const results = await Promise.allSettled(notUploaded.map((imageData, i) => {
103543
+ const filename = `${generateProduct.replace(/\s+/g, '_')}_${imageData.index}_${baseTs}_${i}.png`;
103544
+ addLog(formatLogMessage('log', `📤 Starting upload image ${imageData.index}: ${filename}`));
103545
+ return uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog).then(driveUrl => ({
103546
+ index: imageData.index,
103547
+ driveUrl,
103548
+ success: true
103549
+ }));
103550
+ }));
103551
+ const successfulIndices = new Set();
103552
+ results.forEach((result, i) => {
103553
+ const imageData = notUploaded[i];
103554
+ if (result.status === 'fulfilled' && result.value.success) {
103555
+ addLog(formatLogMessage('log', `✅ Image ${imageData.index} uploaded: ${result.value.driveUrl}`));
103556
+ successfulIndices.add(imageData.index);
103887
103557
  }
103888
- try {
103889
- const filename = `${generateProduct.replace(/\s+/g, '_')}_${imageData.index}_${Date.now()}.png`;
103890
- addLog(formatLogMessage('log', `📤 Uploading image ${imageData.index} to Drive: ${filename}`));
103891
- const driveUrl = await uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog);
103892
- addLog(formatLogMessage('log', `✅ Image ${imageData.index} uploaded: ${driveUrl}`));
103893
- // Update uploaded status
103894
- setGeneratedImagesData(prev => {
103895
- const updated = prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img);
103896
- // Save to Google Drive after upload
103897
- saveGeneratedImagesToDrive(folderId, updated).catch((err) => {
103898
- console.log('Failed to save images data after upload:', err);
103899
- });
103900
- return updated;
103901
- });
103902
- // (Removed legacy Generated Images links list)
103903
- uploadedCount++;
103558
+ else {
103559
+ const errMsg = result.status === 'rejected' ? result.reason?.message : 'Unknown error';
103560
+ addLog(formatLogMessage('error', `❌ Failed to upload image ${imageData.index}:`, errMsg));
103904
103561
  }
103905
- catch (err) {
103906
- addLog(formatLogMessage('error', `❌ Failed to upload image ${imageData.index}:`, err.message));
103562
+ });
103563
+ const uploadedCount = successfulIndices.size;
103564
+ // Update uploaded status and clear uploading state for all
103565
+ setGeneratedImagesData(prev => prev.map(img => notUploaded.some(n => n.index === img.index)
103566
+ ? {
103567
+ ...img,
103568
+ uploaded: successfulIndices.has(img.index),
103569
+ uploading: false,
103570
+ uploadStartTime: undefined
103907
103571
  }
103908
- }
103909
- addLog(formatLogMessage('log', `✅ Uploaded ${uploadedCount} image(s) successfully`));
103910
- // Save images data to Google Drive after all uploads complete
103911
- try {
103912
- addLog(formatLogMessage('log', '💾 Saving images data to Google Drive...'));
103913
- // Get latest state and save all images
103914
- setGeneratedImagesData(prev => {
103915
- saveGeneratedImagesToDrive(folderId, prev).then(() => {
103916
- addLog(formatLogMessage('log', '✅ Images data saved to Google Drive'));
103917
- }).catch((err) => {
103918
- addLog(formatLogMessage('warn', `⚠️ Failed to save images data to Google Drive: ${err.message || 'Unknown error'}`));
103919
- });
103920
- return prev;
103921
- });
103922
- }
103923
- catch (err) {
103924
- addLog(formatLogMessage('warn', `⚠️ Failed to save images data to Google Drive: ${err.message || 'Unknown error'}`));
103925
- }
103926
- alert(`Successfully uploaded ${uploadedCount} image(s) to Google Drive!`);
103572
+ : img));
103573
+ addLog(formatLogMessage('log', `✅ Uploaded ${uploadedCount}/${notUploaded.length} image(s) successfully`));
103574
+ alert(`Successfully uploaded ${uploadedCount} of ${notUploaded.length} image(s) to Google Drive!`);
103927
103575
  }
103928
103576
  catch (err) {
103929
103577
  addLog(formatLogMessage('error', `❌ Error uploading images:`, err.message));
103930
103578
  alert('Error uploading images: ' + err.message);
103579
+ setGeneratedImagesData(prev => prev.map(img => notUploaded.some(n => n.index === img.index)
103580
+ ? { ...img, uploading: false, uploadStartTime: undefined }
103581
+ : img));
103931
103582
  }
103932
103583
  finally {
103933
103584
  setUploadingImages(false);
@@ -104006,41 +103657,13 @@ ${imageData.originalPrompt}
104006
103657
  throw new Error(err.error?.message || 'Failed to fetch Drive files');
104007
103658
  }
104008
103659
  const data = await response.json();
104009
- // Filter out banka* files and product.png
103660
+ // Filter out product.png/jpg (used for product reference, not creative images)
104010
103661
  const filteredFiles = data.files.filter((f) => {
104011
103662
  const name = f.name?.toLowerCase() || '';
104012
- return !name.startsWith('banka') && name !== 'product.png' && name !== 'product.jpg';
103663
+ return name !== 'product.png' && name !== 'product.jpg';
104013
103664
  });
104014
103665
  return filteredFiles.map((f) => f.id ? `https://drive.google.com/file/d/${f.id}/view?usp=sharing` : '');
104015
103666
  };
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
103667
  const fetchProductImage = async (folderId) => {
104045
103668
  const validToken = await getValidAccessToken();
104046
103669
  if (!validToken)
@@ -104197,11 +103820,9 @@ ${imageData.originalPrompt}
104197
103820
  errorMessage: item.errorMessage
104198
103821
  }));
104199
103822
  };
104200
- // Save generated content to Google Drive
104201
- const saveGeneratedContentToDrive = async (folderId, titlesData, textsData, aiSettings, brandValue, linkValue) => {
104202
- const sanitizedTitlesData = sanitizeGeneratedTitlesData(titlesData);
104203
- const sanitizedTextsData = sanitizeGeneratedTextsData(textsData);
104204
- logToTerminal('log', '[Save Content] Saving content to folderId:', folderId, 'titles:', sanitizedTitlesData.length, 'texts:', sanitizedTextsData.length);
103823
+ // Save settings, brand, link to Google Drive (titles and texts are not saved)
103824
+ const saveGeneratedContentToDrive = async (folderId, aiSettings, brandValue, linkValue) => {
103825
+ logToTerminal('log', '[Save Content] Saving settings to folderId:', folderId);
104205
103826
  const validToken = await getValidAccessToken();
104206
103827
  if (!validToken) {
104207
103828
  logToTerminal('error', '[Save Content] No valid token');
@@ -104210,8 +103831,6 @@ ${imageData.originalPrompt}
104210
103831
  // Get or create 'env' folder
104211
103832
  const envFolderId = await getOrCreateEnvFolder(folderId);
104212
103833
  const dataToSave = {
104213
- generatedTitlesData: sanitizedTitlesData,
104214
- generatedTextsData: sanitizedTextsData,
104215
103834
  aiGenerationSettings: aiSettings || {
104216
103835
  generateProduct: generateProduct || '',
104217
103836
  generateGeo: generateGeo || '',
@@ -104364,38 +103983,8 @@ ${imageData.originalPrompt}
104364
103983
  logToTerminal('log', '[Load Content] File downloaded, length:', jsonContent.length);
104365
103984
  try {
104366
103985
  const loadedData = JSON.parse(jsonContent);
104367
- logToTerminal('log', '[Load Content] Parsed data, titles:', loadedData.generatedTitlesData?.length || 0, 'texts:', loadedData.generatedTextsData?.length || 0);
104368
- if (loadedData.generatedTitlesData && Array.isArray(loadedData.generatedTitlesData)) {
104369
- const sanitizedTitlesData = sanitizeGeneratedTitlesData(loadedData.generatedTitlesData);
104370
- logToTerminal('log', '[Load Content] Loading titles, count:', sanitizedTitlesData.length);
104371
- setGeneratedTitlesData(sanitizedTitlesData);
104372
- // Also update titles field
104373
- const titlesString = sanitizedTitlesData
104374
- .map((t) => t.title)
104375
- .filter((t) => t)
104376
- .join('\n');
104377
- if (titlesString) {
104378
- setTitles(titlesString);
104379
- }
104380
- }
104381
- else {
104382
- setGeneratedTitlesData([]);
104383
- }
104384
- if (loadedData.generatedTextsData && Array.isArray(loadedData.generatedTextsData)) {
104385
- const sanitizedTextsData = sanitizeGeneratedTextsData(loadedData.generatedTextsData);
104386
- logToTerminal('log', '[Load Content] Loading texts, count:', sanitizedTextsData.length);
104387
- setGeneratedTextsData(sanitizedTextsData);
104388
- // Also update texts field
104389
- const textsArray = sanitizedTextsData
104390
- .map((t) => t.text)
104391
- .filter((t) => t);
104392
- if (textsArray.length > 0) {
104393
- setTexts(textsArray);
104394
- }
104395
- }
104396
- else {
104397
- setGeneratedTextsData([]);
104398
- }
103986
+ logToTerminal('log', '[Load Content] Parsed settings');
103987
+ // Titles and texts are not loaded from Drive (no longer saved there)
104399
103988
  // Load AI Generation Settings
104400
103989
  if (loadedData.aiGenerationSettings) {
104401
103990
  logToTerminal('log', '[Load Content] Loading AI Generation Settings');
@@ -104417,11 +104006,7 @@ ${imageData.originalPrompt}
104417
104006
  logToTerminal('log', '[Load Content] Loaded generatePriceWithCurrency:', settings.generatePriceWithCurrency);
104418
104007
  }
104419
104008
  }
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
- }
104009
+ // Load link (brand is auto-generated from product, geo, price)
104425
104010
  if (loadedData.link !== undefined) {
104426
104011
  setLink(loadedData.link || '');
104427
104012
  logToTerminal('log', '[Load Content] Loaded link:', loadedData.link || '(empty)');
@@ -104436,250 +104021,7 @@ ${imageData.originalPrompt}
104436
104021
  return { found: false };
104437
104022
  }
104438
104023
  };
104439
- // Save single image to Google Drive as separate file
104440
- const saveSingleImageToDrive = async (folderId, imageData) => {
104441
- logToTerminal('log', '[Save Image] Saving image', imageData.index, 'to folderId:', folderId);
104442
- const validToken = await getValidAccessToken();
104443
- if (!validToken) {
104444
- logToTerminal('error', '[Save Image] No valid token');
104445
- throw new Error('Not logged in to Google Drive');
104446
- }
104447
- // Get or create 'env' folder
104448
- const envFolderId = await getOrCreateEnvFolder(folderId);
104449
- const jsonContent = JSON.stringify(imageData, null, 2);
104450
- const filename = `temp-image-${imageData.index}.json`;
104451
- logToTerminal('log', '[Save Image] Filename:', filename, 'content length:', jsonContent.length);
104452
- // Check if file already exists in 'env' folder
104453
- const q = `'${envFolderId}' in parents and trashed = false and name = '${filename}'`;
104454
- const fields = 'files(id, name)';
104455
- const searchUrl = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent(fields)}`;
104456
- const searchResponse = await fetch(searchUrl, {
104457
- headers: {
104458
- 'Authorization': `Bearer ${validToken}`
104459
- }
104460
- });
104461
- let fileId = null;
104462
- if (searchResponse.ok) {
104463
- const searchData = await searchResponse.json();
104464
- logToTerminal('log', '[Save Image] Search response, files found:', searchData.files?.length || 0);
104465
- if (searchData.files && searchData.files.length > 0) {
104466
- // Filter out duplicate files (with (1), (2), etc.) - prefer exact match
104467
- const exactMatch = searchData.files.find((f) => f.name === filename);
104468
- if (exactMatch) {
104469
- fileId = exactMatch.id;
104470
- logToTerminal('log', '[Save Image] Found exact match, will update, fileId:', fileId);
104471
- }
104472
- else {
104473
- // If no exact match, use first file but log warning
104474
- fileId = searchData.files[0].id;
104475
- logToTerminal('warn', '[Save Image] Found duplicate file, will update first one, fileId:', fileId, 'name:', searchData.files[0].name);
104476
- }
104477
- }
104478
- else {
104479
- logToTerminal('log', '[Save Image] File does not exist, will create new');
104480
- }
104481
- }
104482
- else {
104483
- logToTerminal('warn', '[Save Image] Search failed, status:', searchResponse.status);
104484
- }
104485
- if (fileId) {
104486
- // Update existing file - don't include parents in metadata for updates
104487
- logToTerminal('log', '[Save Image] Updating existing file');
104488
- const updateMetadata = {
104489
- name: filename,
104490
- mimeType: 'application/json'
104491
- };
104492
- const updateForm = new FormData();
104493
- updateForm.append('metadata', new Blob([JSON.stringify(updateMetadata)], { type: 'application/json' }));
104494
- updateForm.append('file', new Blob([jsonContent], { type: 'application/json' }));
104495
- const updateUrl = `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`;
104496
- const updateResponse = await fetch(updateUrl, {
104497
- method: 'PATCH',
104498
- headers: {
104499
- 'Authorization': `Bearer ${validToken}`
104500
- },
104501
- body: updateForm
104502
- });
104503
- if (!updateResponse.ok) {
104504
- const err = await updateResponse.json();
104505
- logToTerminal('error', '[Save Image] Update failed:', JSON.stringify(err));
104506
- throw new Error(err.error?.message || 'Failed to update file in Google Drive');
104507
- }
104508
- logToTerminal('log', '[Save Image] Successfully updated file');
104509
- }
104510
- else {
104511
- // Create new file - include parents for new files (in 'env' folder)
104512
- logToTerminal('log', '[Save Image] Creating new file in env folder');
104513
- const createMetadata = {
104514
- name: filename,
104515
- mimeType: 'application/json',
104516
- parents: [envFolderId]
104517
- };
104518
- const createForm = new FormData();
104519
- createForm.append('metadata', new Blob([JSON.stringify(createMetadata)], { type: 'application/json' }));
104520
- createForm.append('file', new Blob([jsonContent], { type: 'application/json' }));
104521
- const createUrl = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart';
104522
- const createResponse = await fetch(createUrl, {
104523
- method: 'POST',
104524
- headers: {
104525
- 'Authorization': `Bearer ${validToken}`
104526
- },
104527
- body: createForm
104528
- });
104529
- if (!createResponse.ok) {
104530
- const err = await createResponse.json();
104531
- logToTerminal('error', '[Save Image] Create failed:', JSON.stringify(err));
104532
- throw new Error(err.error?.message || 'Failed to create file in Google Drive');
104533
- }
104534
- const result = await createResponse.json();
104535
- logToTerminal('log', '[Save Image] Successfully created file, id:', result.id);
104536
- }
104537
- };
104538
- // Save all images to Google Drive (each in separate file)
104539
- const saveGeneratedImagesToDrive = async (folderId, imagesData) => {
104540
- const validToken = await getValidAccessToken();
104541
- if (!validToken) {
104542
- throw new Error('Not logged in to Google Drive');
104543
- }
104544
- // Save each image to its own file
104545
- for (const imageData of imagesData) {
104546
- try {
104547
- await saveSingleImageToDrive(folderId, imageData);
104548
- }
104549
- catch (err) {
104550
- console.error(`Failed to save image ${imageData.index} to Drive:`, err);
104551
- // Continue with other images even if one fails
104552
- }
104553
- }
104554
- };
104555
- // Load generated images data from Google Drive (from separate files)
104556
- const loadGeneratedImagesFromDrive = async (folderId) => {
104557
- logToTerminal('log', '[Load Images] Starting load from Google Drive, folderId:', folderId);
104558
- const validToken = await getValidAccessToken();
104559
- if (!validToken) {
104560
- logToTerminal('warn', '[Load Images] No valid token, skipping load');
104561
- setDriveFilesFound(prev => ({ ...prev, images: false }));
104562
- return { found: false };
104563
- }
104564
- // Get 'env' folder
104565
- let envFolderId;
104566
- try {
104567
- envFolderId = await getOrCreateEnvFolder(folderId);
104568
- }
104569
- catch (err) {
104570
- logToTerminal('warn', '[Load Images] Failed to get env folder:', err.message || 'Unknown error');
104571
- return { found: false };
104572
- }
104573
- // Find all temp-image-*.json files in 'env' folder
104574
- const q = `'${envFolderId}' in parents and trashed = false and name contains 'temp-image-'`;
104575
- const fields = 'files(id, name)';
104576
- const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent(fields)}`;
104577
- logToTerminal('log', '[Load Images] Searching for image files');
104578
- const response = await fetch(url, {
104579
- headers: {
104580
- 'Authorization': `Bearer ${validToken}`
104581
- }
104582
- });
104583
- if (!response.ok) {
104584
- const errorText = await response.text();
104585
- logToTerminal('warn', '[Load Images] Search failed, status:', response.status, 'error:', errorText);
104586
- setDriveFilesFound(prev => ({ ...prev, images: false }));
104587
- return { found: false };
104588
- }
104589
- const data = await response.json();
104590
- logToTerminal('log', '[Load Images] Search response, files found:', data.files?.length || 0);
104591
- if (!data.files || data.files.length === 0) {
104592
- logToTerminal('log', '[Load Images] No image files found - no temp-image-*.json files in folder');
104593
- return { found: false };
104594
- }
104595
- // Filter out duplicate files (with (1), (2), etc.) - only keep exact matches
104596
- const exactMatchFiles = data.files.filter((f) => {
104597
- const match = f.name.match(/^temp-image-(\d+)\.json$/);
104598
- return match !== null;
104599
- });
104600
- logToTerminal('log', '[Load Images] Found', exactMatchFiles.length, 'exact match files:', exactMatchFiles.map((f) => f.name).join(', '));
104601
- // Group files by index and take only one file per index (prefer exact match, then first one)
104602
- const filesByIndex = new Map();
104603
- for (const file of exactMatchFiles) {
104604
- const match = file.name.match(/^temp-image-(\d+)\.json$/);
104605
- if (match) {
104606
- const index = parseInt(match[1], 10);
104607
- if (!filesByIndex.has(index)) {
104608
- filesByIndex.set(index, file);
104609
- }
104610
- }
104611
- }
104612
- const uniqueFiles = Array.from(filesByIndex.values());
104613
- logToTerminal('log', '[Load Images] After deduplication:', uniqueFiles.length, 'unique files, indices:', Array.from(filesByIndex.keys()).sort((a, b) => a - b).join(', '));
104614
- // Helper function to download file with retries
104615
- const downloadFileWithRetry = async (file, maxRetries = 3, delayMs = 1000) => {
104616
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
104617
- try {
104618
- if (attempt > 1) {
104619
- logToTerminal('log', `[Load Images] Retry ${attempt}/${maxRetries} for file:`, file.name);
104620
- await new Promise(resolve => setTimeout(resolve, delayMs * (attempt - 1))); // Exponential backoff
104621
- }
104622
- else {
104623
- logToTerminal('log', '[Load Images] Loading file:', file.name);
104624
- }
104625
- const downloadUrl = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`;
104626
- const downloadResponse = await fetch(downloadUrl, {
104627
- headers: {
104628
- 'Authorization': `Bearer ${validToken}`
104629
- }
104630
- });
104631
- if (downloadResponse.ok) {
104632
- const jsonContent = await downloadResponse.text();
104633
- logToTerminal('log', '[Load Images] File', file.name, 'downloaded, length:', jsonContent.length);
104634
- const imageData = JSON.parse(jsonContent);
104635
- logToTerminal('log', '[Load Images] Parsed image data for index:', imageData.index);
104636
- return imageData;
104637
- }
104638
- else {
104639
- const errorText = await downloadResponse.text();
104640
- logToTerminal('warn', `[Load Images] Download failed for ${file.name} (attempt ${attempt}/${maxRetries}), status:`, downloadResponse.status);
104641
- if (attempt === maxRetries) {
104642
- logToTerminal('error', `[Load Images] Failed to load ${file.name} after ${maxRetries} attempts`);
104643
- return null;
104644
- }
104645
- }
104646
- }
104647
- catch (err) {
104648
- logToTerminal('error', `[Load Images] Failed to load image file ${file.name} (attempt ${attempt}/${maxRetries}):`, err);
104649
- if (attempt === maxRetries) {
104650
- logToTerminal('error', `[Load Images] Failed to load ${file.name} after ${maxRetries} attempts`);
104651
- return null;
104652
- }
104653
- }
104654
- }
104655
- return null;
104656
- };
104657
- // Load all image files in parallel
104658
- logToTerminal('log', '[Load Images] Starting parallel download of', uniqueFiles.length, 'files');
104659
- const downloadPromises = uniqueFiles.map(file => downloadFileWithRetry(file));
104660
- // Wait for all downloads to complete
104661
- const results = await Promise.all(downloadPromises);
104662
- const imagesData = results.filter((data) => data !== null);
104663
- // Sort by index and update state
104664
- if (imagesData.length > 0) {
104665
- imagesData.sort((a, b) => a.index - b.index);
104666
- logToTerminal('log', '[Load Images] Successfully loaded', imagesData.length, 'images, indices:', imagesData.map(img => img.index).join(', '));
104667
- setGeneratedImagesData(imagesData);
104668
- setDriveFilesFound(prev => ({ ...prev, images: false }));
104669
- return { found: true };
104670
- }
104671
- else {
104672
- logToTerminal('log', '[Load Images] No images loaded');
104673
- setDriveFilesFound(prev => ({ ...prev, images: false }));
104674
- return { found: false };
104675
- }
104676
- };
104677
104024
  const handleGenerate = async () => {
104678
- const validToken = await getValidAccessToken();
104679
- if (!validToken) {
104680
- alert('Please log in with Google first');
104681
- return;
104682
- }
104683
104025
  if (!driveFolderUrl || !brand || !link) {
104684
104026
  alert('Please fill all required fields');
104685
104027
  return;
@@ -104691,18 +104033,28 @@ ${imageData.originalPrompt}
104691
104033
  alert('Invalid Link URL');
104692
104034
  return;
104693
104035
  }
104694
- const titleList = titles.split('\n').map(t => t.trim()).filter(t => t);
104036
+ const titleList = titles.split('\n').map(t => t.trim()).filter(t => t).length > 0
104037
+ ? titles.split('\n').map(t => t.trim()).filter(t => t)
104038
+ : generatedTitlesData.map(t => t.title).filter(Boolean);
104695
104039
  if (titleList.length === 0) {
104696
104040
  alert('Please add at least one title');
104697
104041
  return;
104698
104042
  }
104699
- const textList = texts.map(t => t.trim()).filter(t => t);
104043
+ const textList = texts.map(t => t.trim()).filter(t => t).length > 0
104044
+ ? texts.map(t => t.trim()).filter(t => t)
104045
+ : generatedTextsData.map(t => t.text).filter(Boolean);
104700
104046
  if (textList.length === 0) {
104701
104047
  alert('Please add at least one text variant');
104702
104048
  return;
104703
104049
  }
104704
104050
  setLoading(true);
104705
104051
  try {
104052
+ const validToken = await getValidAccessToken();
104053
+ if (!validToken) {
104054
+ alert('Please log in with Google first');
104055
+ setLoading(false);
104056
+ return;
104057
+ }
104706
104058
  const folderId = extractFolderId(driveFolderUrl);
104707
104059
  if (!folderId) {
104708
104060
  throw new Error('Invalid Google Drive Folder URL');
@@ -104739,8 +104091,8 @@ ${imageData.originalPrompt}
104739
104091
  }
104740
104092
  setGeneratedData(rows);
104741
104093
  // 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);
104094
+ const wb = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_new();
104095
+ const ws = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.aoa_to_sheet(rows);
104744
104096
  // Set column widths (approximate pixel width / 7)
104745
104097
  ws['!cols'] = [
104746
104098
  { wch: 20 }, // id
@@ -104753,10 +104105,10 @@ ${imageData.originalPrompt}
104753
104105
  { wch: 40 }, // image_link
104754
104106
  { wch: 20 } // brand
104755
104107
  ];
104756
- xlsx__WEBPACK_IMPORTED_MODULE_55__.utils.book_append_sheet(wb, ws, "Products");
104108
+ xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_append_sheet(wb, ws, "Products");
104757
104109
  // Generate buffer
104758
- const wbout = xlsx__WEBPACK_IMPORTED_MODULE_55__.write(wb, { bookType: 'xlsx', type: 'array' });
104759
- // Upload to Drive
104110
+ const wbout = xlsx__WEBPACK_IMPORTED_MODULE_50__.write(wb, { bookType: 'xlsx', type: 'array' });
104111
+ // Upload to Drive (имя файла по бренду)
104760
104112
  const dateStr = new Date().toISOString().split('T')[0];
104761
104113
  const fileName = `${brand}-${dateStr}.xlsx`;
104762
104114
  const result = await uploadFileToDrive(wbout, fileName, folderId || undefined);
@@ -104877,13 +104229,13 @@ ${imageData.originalPrompt}
104877
104229
  setTestLoading(true);
104878
104230
  try {
104879
104231
  // Create simple test workbook with structure
104880
- const wb = xlsx__WEBPACK_IMPORTED_MODULE_55__.utils.book_new();
104232
+ const wb = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_new();
104881
104233
  const rows = [
104882
104234
  INSTRUCTION_ROW,
104883
104235
  ['id', 'title', 'description', 'availability', 'condition', 'price', 'link', 'image_link', 'brand'],
104884
104236
  ['test1', 'Test Title', 'Test Description', 'in stock', 'new', '10.00 USD', 'http://test.com', 'http://test.com/img.jpg', 'TestBrand']
104885
104237
  ];
104886
- const ws = xlsx__WEBPACK_IMPORTED_MODULE_55__.utils.aoa_to_sheet(rows);
104238
+ const ws = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.aoa_to_sheet(rows);
104887
104239
  // Set column widths
104888
104240
  ws['!cols'] = [
104889
104241
  { wch: 20 }, // id
@@ -104896,8 +104248,8 @@ ${imageData.originalPrompt}
104896
104248
  { wch: 40 }, // image_link
104897
104249
  { wch: 20 } // brand
104898
104250
  ];
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' });
104251
+ xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_append_sheet(wb, ws, "Test");
104252
+ const wbout = xlsx__WEBPACK_IMPORTED_MODULE_50__.write(wb, { bookType: 'xlsx', type: 'array' });
104901
104253
  // Try to extract folder ID if available, otherwise upload to root
104902
104254
  const folderId = driveFolderUrl ? extractFolderId(driveFolderUrl) : undefined;
104903
104255
  const result = await uploadFileToDrive(wbout, 'test_table.xlsx', folderId || undefined);
@@ -104931,7 +104283,7 @@ ${imageData.originalPrompt}
104931
104283
  };
104932
104284
  // Show lock screen if not unlocked
104933
104285
  if (!unlocked) {
104934
- return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_37__["default"], { theme: theme },
104286
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { theme: theme },
104935
104287
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_4__["default"], null),
104936
104288
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { onClick: handleSecretClick, sx: {
104937
104289
  width: '100vw',
@@ -104971,24 +104323,24 @@ ${imageData.originalPrompt}
104971
104323
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("br", null),
104972
104324
  "Please contact system administrator"))));
104973
104325
  }
104974
- return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_37__["default"], { theme: theme },
104326
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { theme: theme },
104975
104327
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_4__["default"], null),
104976
104328
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_7__["default"], { maxWidth: "lg", sx: { py: 4 } },
104977
104329
  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
104330
  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
104331
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
104980
104332
  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)))),
104333
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_46__["default"], null)),
104334
+ 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
104335
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_9__["default"], { variant: "outlined", sx: { mb: 4 } },
104984
104336
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_10__["default"], null,
104985
104337
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 3 },
104986
104338
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
104987
104339
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true }, "Google Drive Authentication"),
104988
104340
  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) },
104341
+ 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
104342
  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 } }),
104343
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_37__["default"], { sx: { mr: 1 } }),
104992
104344
  " Logged In (Credentials Hidden)")),
104993
104345
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_14__["default"], null,
104994
104346
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 2 },
@@ -104999,7 +104351,7 @@ ${imageData.originalPrompt}
104999
104351
  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
104352
  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
104353
  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' }),
104354
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_33__["default"], { color: openRouterAccountBalance !== null ? 'primary' : 'disabled' }),
105003
104355
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", color: openRouterAccountBalance !== null ? 'text.primary' : 'text.secondary' },
105004
104356
  "\u0411\u0430\u043B\u0430\u043D\u0441: ",
105005
104357
  openRouterBalanceLoading ? 'Loading...' : (openRouterAccountBalance !== null ? `$${openRouterAccountBalance.toFixed(2)}` : 'N/A')),
@@ -105008,8 +104360,8 @@ ${imageData.originalPrompt}
105008
104360
  "\u041B\u0438\u043C\u0438\u0442 \u043A\u043B\u044E\u0447\u0430: ",
105009
104361
  openRouterBalanceLoading ? 'Loading...' : (openRouterBalance === -1 ? 'Без лимита' : (openRouterBalance !== null ? `${openRouterBalance.toFixed(4)}` : 'N/A'))))),
105010
104362
  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")))),
104363
+ 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')),
104364
+ 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
104365
  !accessToken && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
105014
104366
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], null),
105015
104367
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
@@ -105025,16 +104377,30 @@ ${imageData.originalPrompt}
105025
104377
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 14 }),
105026
104378
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0444\u0430\u0439\u043B\u043E\u0432...")))),
105027
104379
  !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"))))),
104380
+ 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
104381
  !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
104382
  !driveFolderUrl.trim() ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "info", sx: { mt: 2 } },
105034
104383
  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
- 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,
104384
+ 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."))) : !extractFolderId(driveFolderUrl) ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "warning", sx: { mt: 2 } },
104385
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body1", sx: { fontWeight: 'bold', mb: 1 } }, "\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"),
104386
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2" }, "\u0421\u0441\u044B\u043B\u043A\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u0432 \u0444\u043E\u0440\u043C\u0430\u0442\u0435: https://drive.google.com/drive/folders/..."))) : loadingContentFromDrive ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
104387
+ display: 'flex',
104388
+ flexDirection: 'column',
104389
+ alignItems: 'center',
104390
+ justifyContent: 'center',
104391
+ gap: 2,
104392
+ p: 4,
104393
+ mt: 2,
104394
+ border: (theme) => `2px dashed ${theme.palette.primary.main}`,
104395
+ borderRadius: 1,
104396
+ backgroundColor: (theme) => theme.palette.mode === 'dark'
104397
+ ? 'rgba(25, 118, 210, 0.1)'
104398
+ : 'rgba(25, 118, 210, 0.05)'
104399
+ } },
104400
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 40, sx: { color: 'primary.main' } }),
104401
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body1", sx: { color: 'text.secondary', fontWeight: 'bold' } }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u0438\u0437 \u043F\u0430\u043F\u043A\u0438..."))) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
105036
104402
  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" }),
104403
+ 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
104404
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { flex: 1, minWidth: 0 } },
105039
104405
  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
104406
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { sx: { my: 2 } }),
@@ -105067,7 +104433,7 @@ ${imageData.originalPrompt}
105067
104433
  : 'Выбрана модель: ' + (validationModels.find(m => m.id === selectedValidationModel)?.name || selectedValidationModel))),
105068
104434
  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
104435
  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'),
104436
+ 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
104437
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 1, alignItems: "center", sx: { flexGrow: 1 } },
105072
104438
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["default"], { title: imageAspectRatio === '1:1'
105073
104439
  ? '1:1 — квадрат (1024×1024 px)'
@@ -105094,8 +104460,7 @@ ${imageData.originalPrompt}
105094
104460
  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
104461
  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
104462
  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 ||
105098
- loadingImagesFromDrive ||
104463
+ 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 ||
105099
104464
  !openaiApiKey ||
105100
104465
  (!accessToken && !refreshToken) ||
105101
104466
  !generateProduct.trim() ||
@@ -105113,6 +104478,18 @@ ${imageData.originalPrompt}
105113
104478
  : !driveFolderUrl.trim()
105114
104479
  ? 'Заполните URL папки Google Drive'
105115
104480
  : undefined }, generatingImages ? 'Generating Images...' : 'Generate Images')),
104481
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "success", size: "large", onClick: handleGenerate, disabled: loading, 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 Catalog'),
104482
+ uploadedLink && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "success", sx: { flexGrow: 1, minWidth: 0 }, 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 } },
104483
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_39__["default"], { fontSize: "small" })) },
104484
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' } },
104485
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
104486
+ "\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D!",
104487
+ ' ',
104488
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { href: "#", onClick: (e) => {
104489
+ e.preventDefault();
104490
+ getElectronAPI().openExternal(uploadedLink);
104491
+ }, style: { cursor: 'pointer', textDecoration: 'underline', color: 'inherit' } }, "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 Google Drive")),
104492
+ linkCopied && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'success.dark', fontWeight: 'bold' } }, "\u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u043E!"))))),
105116
104493
  folderFilesInfo !== null && !folderFilesInfo.hasProduct && driveFolderUrl.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "warning", sx: { mt: 1 } }, "\u0414\u043B\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F product.png/jpg. \u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439\u0442\u0435 \u0435\u0433\u043E \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043A\u043D\u043E\u043F\u043A\u0438 \"Generate Product from Banka\".")),
105117
104494
  !generatingImages && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
105118
104495
  !openaiApiKey && (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 OpenRouter API Key")),
@@ -105120,18 +104497,17 @@ ${imageData.originalPrompt}
105120
104497
  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
104498
  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
104499
  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"))))),
104500
+ 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
104501
  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 ||
104502
+ 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'),
104503
+ 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
104504
  !openaiApiKey ||
105129
104505
  !accessToken ||
105130
104506
  !driveFolderUrl.trim() ||
105131
104507
  (folderFilesInfo !== null && !folderFilesInfo.hasProduct), sx: { flexGrow: 1 }, size: "large", title: folderFilesInfo !== null && !folderFilesInfo.hasProduct
105132
- ? 'product.png/jpg не найден в папке. Сначала сгенерируйте его с помощью кнопки "Generate Product from Banka"'
104508
+ ? 'product.png/jpg не найден в папке. Загрузите его с помощью кнопки «Загрузить product.png/jpg»'
105133
104509
  : undefined }, generatingLanding ? 'Creating Landing...' : 'Создать лендинг')),
105134
- (generatedImagesData.length > 0 || loadingImagesFromDrive) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2, mt: 3 } },
104510
+ generatedImagesData.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2, mt: 3 } },
105135
104511
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } },
105136
104512
  "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u043A\u0440\u0435\u043E (",
105137
104513
  generatedImagesData.filter(img => img.imageUrl).length,
@@ -105141,22 +104517,6 @@ ${imageData.originalPrompt}
105141
104517
  checkingImages && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1, mb: 2 } },
105142
104518
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }),
105143
104519
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary' } }, "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0430..."))),
105144
- loadingImagesFromDrive && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
105145
- display: 'flex',
105146
- flexDirection: 'column',
105147
- alignItems: 'center',
105148
- justifyContent: 'center',
105149
- gap: 2,
105150
- p: 4,
105151
- border: (theme) => `2px dashed ${theme.palette.primary.main}`,
105152
- borderRadius: 1,
105153
- backgroundColor: (theme) => theme.palette.mode === 'dark'
105154
- ? 'rgba(25, 118, 210, 0.1)'
105155
- : 'rgba(25, 118, 210, 0.05)',
105156
- minHeight: 200
105157
- } },
105158
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 40, sx: { color: 'primary.main' } }),
105159
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body1", sx: { color: 'text.secondary', fontWeight: 'bold' } }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439 \u0438\u0437 Google Drive..."))),
105160
104520
  generatedImagesData.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
105161
104521
  display: 'grid',
105162
104522
  gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
@@ -105281,7 +104641,7 @@ ${imageData.originalPrompt}
105281
104641
  "+",
105282
104642
  imageData.checkErrors.length - 2,
105283
104643
  " \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")))),
104644
+ 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
104645
  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
104646
  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
104647
  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 +104664,7 @@ ${imageData.originalPrompt}
105304
104664
  : 'transparent'
105305
104665
  }
105306
104666
  } }),
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 ||
104667
+ 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
104668
  imageData.uploading ||
105309
104669
  imageData.checkStatus === 'checking' ||
105310
104670
  !imageData.originalPrompt ||
@@ -105314,12 +104674,12 @@ ${imageData.originalPrompt}
105314
104674
  : ((generatingImages && !imageData.imageUrl && !imageData.failed && !imageData.generating)
105315
104675
  ? 'В очереди'
105316
104676
  : (!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 ||
104677
+ 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
104678
  imageData.uploading ||
105319
104679
  imageData.checkStatus === 'checking' ||
105320
104680
  !imageData.originalPrompt ||
105321
104681
  !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: () => {
104682
+ !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
104683
  const folderId = extractFolderId(driveFolderUrl);
105324
104684
  if (folderId) {
105325
104685
  handleUploadImage(imageData, folderId);
@@ -105327,7 +104687,7 @@ ${imageData.originalPrompt}
105327
104687
  }, disabled: imageData.uploading || imageData.regenerating || !driveFolderUrl.trim(), fullWidth: true }, imageData.uploading ? 'Загрузка...' : 'Загрузить'))))));
105328
104688
  }))),
105329
104689
  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})`))))),
104690
+ 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
104691
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { sx: { my: 2 } }),
105332
104692
  (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
104693
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } },
@@ -105398,7 +104758,7 @@ ${imageData.originalPrompt}
105398
104758
  tooltip: { sx: { maxWidth: 360, fontSize: 12, lineHeight: 1.6, p: '10px 14px' } },
105399
104759
  } },
105400
104760
  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),
104761
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_41__["default"], { sx: { fontSize: 14 } })))) : null),
105402
104762
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 1.5 },
105403
104763
  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
104764
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16, sx: { color: 'primary.main' } }),
@@ -105465,102 +104825,8 @@ ${imageData.originalPrompt}
105465
104825
  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
104826
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }),
105467
104827
  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")))),
105469
- 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')),
105471
- 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" })) },
105473
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
105474
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
105475
- "File Uploaded!",
105476
- ' ',
105477
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { href: "#", onClick: (e) => {
105478
- e.preventDefault();
105479
- getElectronAPI().openExternal(uploadedLink);
105480
- }, style: { cursor: 'pointer', textDecoration: 'underline', color: 'inherit' } }, "Click here to open in Google Drive")),
105481
- 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) }))));
104828
+ !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"))))))))),
104829
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_PromptManagerDialog__WEBPACK_IMPORTED_MODULE_48__["default"], { open: promptManagerOpen, onClose: () => setPromptManagerOpen(false) }))));
105564
104830
  }
105565
104831
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (App);
105566
104832
 
@@ -105995,8 +105261,8 @@ function PromptManagerDialog({ open, onClose }) {
105995
105261
  const [viewModes, setViewModes] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({});
105996
105262
  // selectedApproaches: array of indices from PAIR_APPROACH_POOL, ordered
105997
105263
  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]);
105264
+ // imageApproachCounts: по одному на каждый подход (CREO_APPROACHES), каждый 0–4
105265
+ const [imageApproachCounts, setImageApproachCounts] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => Array(_prompts__WEBPACK_IMPORTED_MODULE_34__.CREO_APPROACHES.length).fill(1));
106000
105266
  // Загрузить оверрайды при открытии
106001
105267
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
106002
105268
  if (open) {
@@ -106596,25 +105862,32 @@ function getSelectedPairApproaches() {
106596
105862
  function getPairsCount() {
106597
105863
  return getSelectedPairApproaches().length;
106598
105864
  }
105865
+ /** Количество подходов для изображений. Должно совпадать с CREO_APPROACHES.length в prompts.ts */
105866
+ const IMAGE_APPROACH_COUNT = 10;
106599
105867
  /**
106600
- * Получить количество изображений по каждому подходу (8 элементов, 0–4).
105868
+ * Получить количество изображений по каждому подходу (N элементов, 0–4).
106601
105869
  * По умолчанию — по 1 на каждый подход.
106602
105870
  */
106603
105871
  function getImageApproachCounts() {
106604
105872
  const overrides = loadPromptOverrides();
106605
- if (overrides.imageApproachCounts && overrides.imageApproachCounts.length === 8) {
106606
- return overrides.imageApproachCounts.map(c => Math.max(0, Math.min(4, c)));
105873
+ const defaultCounts = Array(IMAGE_APPROACH_COUNT).fill(1);
105874
+ if (overrides.imageApproachCounts && overrides.imageApproachCounts.length > 0) {
105875
+ const stored = overrides.imageApproachCounts.map(c => Math.max(0, Math.min(4, c)));
105876
+ // backward compat: если было 8, дополняем новыми подходами
105877
+ while (stored.length < IMAGE_APPROACH_COUNT)
105878
+ stored.push(1);
105879
+ return stored.slice(0, IMAGE_APPROACH_COUNT);
106607
105880
  }
106608
105881
  // backward compat: selectedImageApproaches -> counts (1 for selected, 0 for not)
106609
105882
  if (overrides.selectedImageApproaches && overrides.selectedImageApproaches.length >= 1) {
106610
- const counts = Array(8).fill(0);
105883
+ const counts = Array(IMAGE_APPROACH_COUNT).fill(0);
106611
105884
  for (const i of overrides.selectedImageApproaches) {
106612
- if (i >= 0 && i < 8)
105885
+ if (i >= 0 && i < IMAGE_APPROACH_COUNT)
106613
105886
  counts[i] = 1;
106614
105887
  }
106615
105888
  return counts;
106616
105889
  }
106617
- return [1, 1, 1, 1, 1, 1, 1, 1];
105890
+ return defaultCounts;
106618
105891
  }
106619
105892
  /**
106620
105893
  * Загрузить оверрайды из localStorage
@@ -106778,7 +106051,8 @@ __webpack_require__.r(__webpack_exports__);
106778
106051
  /* harmony export */ getTextsSystemPrompt: () => (/* binding */ getTextsSystemPrompt),
106779
106052
  /* harmony export */ getTitlesSystemPrompt: () => (/* binding */ getTitlesSystemPrompt),
106780
106053
  /* harmony export */ getUserPrompt: () => (/* binding */ getUserPrompt),
106781
- /* harmony export */ getValidationPrompt: () => (/* binding */ getValidationPrompt)
106054
+ /* harmony export */ getValidationPrompt: () => (/* binding */ getValidationPrompt),
106055
+ /* harmony export */ pickRandomBullets: () => (/* binding */ pickRandomBullets)
106782
106056
  /* harmony export */ });
106783
106057
  /* harmony import */ var _promptOverrides__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./promptOverrides */ "./src/promptOverrides.ts");
106784
106058
  /**
@@ -106786,6 +106060,56 @@ __webpack_require__.r(__webpack_exports__);
106786
106060
  * Все промпты на русском языке для консистентности
106787
106061
  */
106788
106062
 
106063
+ /** Пул шаблонов буллетов по категориям (на русском — модель переводит на язык GEO) */
106064
+ const BULLET_BY_CATEGORY = {
106065
+ action: [
106066
+ 'Снижает дискомфорт', 'Снижает вздутие', 'Снижает боль', 'Быстрое облегчение',
106067
+ 'Комфорт каждый день', 'Лёгкость движений', 'Меньше симптомов', 'Без боли',
106068
+ 'Дни без дискомфорта', 'Спокойный желудок', 'Спокойные ночи', 'Видимый результат'
106069
+ ],
106070
+ timeframe: [
106071
+ 'Эффект за 7–14 дней', 'Эффект за 2 недели', 'Быстрый результат', 'Облегчение с 1 дня',
106072
+ 'Эффект за 7 дней', 'В 3 раза быстрее', 'С первого дня', 'В первые дни'
106073
+ ],
106074
+ socialProof: [
106075
+ '9 из 10 рекомендуют', '9 из 10 мужчин рекомендуют', '93% подтверждают',
106076
+ '8 из 10 довольны', 'Тысячи довольных'
106077
+ ],
106078
+ formula: [
106079
+ 'Натуральная формула', 'Натуральные ингредиенты', 'Без химии', 'Отборный состав',
106080
+ 'Дерматологически протестировано'
106081
+ ],
106082
+ qualityOfLife: [
106083
+ 'Активный комфорт', 'Дни без забот', 'Больше энергии', 'Здоровый сон', 'Лёгкая жизнь'
106084
+ ]
106085
+ };
106086
+ /** Какие категории для какого подхода (по 1 из каждой категории, порядок случайный) */
106087
+ const APPROACH_BULLET_CATEGORIES = {
106088
+ 'Эксперт / Авторитет': ['action', 'timeframe', 'socialProof'],
106089
+ 'Lifestyle / Момент приёма': ['timeframe', 'socialProof', 'action'], // срок + цифра + действие
106090
+ 'Эмоция / Портрет': ['qualityOfLife', 'action', 'timeframe'], // качество жизни + действие + срок
106091
+ 'Визуализация проблемы': ['action', 'timeframe', 'socialProof'],
106092
+ 'Power / Сила решения': ['action', 'timeframe', 'socialProof'], // действие + скорость + цифра
106093
+ 'Минимализм / Clean Big Text': ['action', 'timeframe', 'formula'],
106094
+ 'Врач / Эксперт': ['action', 'timeframe', 'socialProof']
106095
+ };
106096
+ function pickOne(arr) {
106097
+ return arr[Math.floor(Math.random() * arr.length)];
106098
+ }
106099
+ function shuffle(arr) {
106100
+ return [...arr].sort(() => Math.random() - 0.5);
106101
+ }
106102
+ /** Выбрать 3 случайных буллета по правилам подхода (1 из каждой категории, порядок случайный) */
106103
+ function pickRandomBullets(approachName) {
106104
+ const categories = APPROACH_BULLET_CATEGORIES[approachName];
106105
+ if (!categories || categories.length < 3) {
106106
+ // fallback: все категории
106107
+ const all = Object.values(BULLET_BY_CATEGORY).flat();
106108
+ return shuffle(all).slice(0, 3);
106109
+ }
106110
+ const bullets = categories.map(cat => pickOne(BULLET_BY_CATEGORY[cat] || []));
106111
+ return shuffle(bullets);
106112
+ }
106789
106113
  // Debug logging — пишет в терминал Electron (или в console как fallback)
106790
106114
  function _debugLog(...args) {
106791
106115
  const api = typeof window !== 'undefined' ? window.electronAPI : null;
@@ -106917,16 +106241,13 @@ function getTitlesSystemPrompt(geo, noOverride, count = 3) {
106917
106241
  - НЕ упоминай название продукта в заголовке — оно уже на креативе. Заголовок = только боль + решение/триггер
106918
106242
 
106919
106243
  Технические требования:
106920
- - Каждый заголовок должен предпочтительно быть до 55 символов. При необходимости для разнообразия смысла допускается до 60 символов
106921
- - Каждый заголовок максимум 6-7 слов. Если длиннее сократи, сохраняя смысл
106244
+ - Каждый заголовок может быть 1–4 строки. Допускается до 12 слов (при многострочности)
106245
+ - Каждый заголовок предпочтительно до 55 символов в строке. При необходимости для разнообразия допускается до 60 символов в строке
106922
106246
  - ЗАПРЕЩЕНО использовать двоеточие (:) в заголовках. Используй тире (—) или переформулируй заголовок без двоеточия. Если в заголовке есть двоеточие — это критическая ошибка, перепиши заголовок
106923
106247
  - Используй 1-2 эмодзи естественно внутри заголовка, не только в конце. Эмодзи должны заменять или визуально отмечать смысл (эмоция, действие, предупреждение), а не украшать или заполнять пространство. Максимум 1-2 на строку
106924
106248
  - Избегай эмодзи ⚠️ и 🚨 — они ассоциируются с опасностью. Для привлечения внимания используй 👉, ✅, 🌿, ⏳
106925
- - КРИТИЧНО: Каждый заголовок должен содержать хотя бы одно ясное ключевое слово проблемы, релевантное категории продукта (например, для суставов: боль/скованность/подвижность; для пищеварения: дискомфорт/тяжесть; для сна: бессонница/усталость примеры только для формата, адаптируй под категорию). Если ключевое слово проблемы отсутствует — перепиши заголовок
106249
+ - КРИТИЧНО: Каждый заголовок ОБЯЗАН содержать ключевое слово проблемы ЯВНО. Примеры по категориям: простата простатит, простата; похудение лишний вес, похудение; суставы боль в суставах, колени, скованность; пищеварение дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Если ключевое слово отсутствует — перепиши заголовок
106926
106250
  - Используй знания лучших практик рекламного копирайтинга
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
106251
  - После создания проверь их правильность и разнообразие
106931
106252
  - Верни только заголовки, по одному на строку, без нумерации или буллетов
106932
106253
  - Заголовки должны быть на языке ${geo}
@@ -106954,8 +106275,8 @@ function getTextsSystemPrompt(geo, noOverride, count = 3) {
106954
106275
  .map((p, i) => `- Текст ${i + 1} → [${p.name}]. ${p.textApproach}`)
106955
106276
  .join('\n');
106956
106277
  const deadlineNote = n >= 2
106957
- ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO). Формулируй как ожидание/видимый результат, НЕ как гарантию. НЕЛЬЗЯ использовать «гарантированно/100%/навсегда».`
106958
- : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO). Формулируй как ожидание/видимый результат.`;
106278
+ ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO).`
106279
+ : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO).`;
106959
106280
  return `Ты — эксперт-копирайтер, специализирующийся на рекламе в Meta Ads (Facebook/Instagram). Твоя задача — создать ровно ${n} идеальных продающих рекламных текста для Meta Ads на языке ${geo}.
106960
106281
 
106961
106282
  ВАЖНО — СИСТЕМА ПАРЫ (заголовок + текст):
@@ -106985,24 +106306,18 @@ ${deadlineNote}
106985
106306
  - Избегай академических, синтетических или звучащих как ИИ слов и фраз
106986
106307
  - Избегай искусственного или неестественного формулирования, которое реальные рекламодатели не использовали бы
106987
106308
  - Если фраза звучит синтетически или академически — перепиши её
106988
- - Избегай медицинской терминологии, которую обычный человек не использует в разговоре («secreția biliară», «diureza», «peristaltică»). Пиши так, как человек описал бы проблему другу
106989
-
106990
106309
  Требования к содержанию:
106991
106310
  - КРИТИЧНО: Держи каждый текст кратким — цель примерно 150-280 символов на текст. Тексты в стиле отзыва могут использовать до 300 символов при необходимости для естественного повествования. Будь эффектным, но коротким
106992
106311
  - Если текст длиннее 280 символов (или 300 для стиля отзыва) — сожми и перепиши, сохраняя смысл
106993
106312
  - КРИТИЧНО: Каждый текст должен содержать хотя бы одно ясное ключевое слово проблемы, релевантное категории продукта (например, для суставов: боль/скованность/подвижность; для пищеварения: дискомфорт/тяжесть; для сна: бессонница/усталость — примеры только для формата, адаптируй под категорию). Если ключевое слово проблемы отсутствует — перепиши текст
106994
106313
  - КРИТИЧНО: Каждый текст должен заканчиваться сильным и явным призывом к действию (кликни, попробуй сейчас, закажи сегодня, узнай больше и т.д.)
106995
106314
  - Избегай слабых окончаний или информационного тона. Слабые или нейтральные окончания не допускаются
106996
- - CTA должен быть “нажимным”: допускаются формулировки «не откладывай», «последний шанс», «только сегодня», «забери -50%» (всё строго на языке GEO). Но избегай слов «гарантированно», «вылечит», «100%», «навсегда»
106315
+ - CTA должен быть “нажимным”: допускаются формулировки «не откладывай», «последний шанс», «только сегодня», «забери -50%» (всё строго на языке GEO)
106997
106316
  - В каждом тексте строго следуй заданному формату старта согласно его подходу (см. распределение выше)
106998
106317
  - Если ингредиенты предоставлены в дополнительной информации, упоминай их ТОЛЬКО если они естественно подходят выбранному рекламному подходу. Например: подходы авторитета/экспертизы выигрывают от деталей ингредиентов, в то время как подходы срочности/эмоционального триггера могут не нуждаться в них. Упоминай только релевантные ингредиенты, не обязательно все из них. Если ингредиенты не подходят подходу или не предоставлены — полностью пропусти их упоминание и не изобретай и не упоминай никакие ингредиенты
106999
106318
  - Добавляй только существенные поддерживающие детали, которые усиливают убеждение или ясность, а не заполнители или общие утверждения. Будь кратким и прямым
107000
106319
  - Используй 1-4 эмодзи естественно внутри текста, не только в конце. Эмодзи должны заменять или визуально отмечать смысл (эмоция, действие, предупреждение), а не украшать или заполнять пространство. Максимум 1-2 на строку
107001
106320
  - Избегай эмодзи ⚠️ и 🚨 — они ассоциируются с опасностью. Для привлечения внимания используй 👉, ✅, 🌿, ⏳
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
106321
  - Избегай прямых упоминаний интимных или деликатных деталей, связанных с проблемой. Используй мягкие формулировки, описывающие дискомфорт и симптомы, а не физиологические процессы
107007
106322
  - Избегай повторения полного названия продукта более 2 раз в одном тексте. Используй местоимения или более короткие ссылки при необходимости
107008
106323
 
@@ -107065,8 +106380,8 @@ function getPairsSystemPrompt(geo, noOverride, count = 3, selectedIndices) {
107065
106380
  .map((p, i) => `Пара ${i + 1} → [${p.name}]:\n Заголовок: ${p.titleApproach}\n Текст: ${p.textApproach}`)
107066
106381
  .join('\n');
107067
106382
  const deadlineNote = n >= 2
107068
- ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}). Формулируй как ожидание/видимый результат, НЕ как гарантию.`
107069
- : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}). Формулируй как ожидание/видимый результат.`;
106383
+ ? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}).`
106384
+ : `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}).`;
107070
106385
  return `Ты — эксперт-копирайтер, специализирующийся на рекламе в Meta Ads (Facebook/Instagram). Создай ровно ${n} пар «заголовок + текст» для рекламы на языке ${geo}.
107071
106386
 
107072
106387
  ФОРМАТ ОТВЕТА (строго соблюдай, без лишних символов):
@@ -107092,7 +106407,6 @@ ${distributionLines}
107092
106407
  - 1–2 эмодзи естественно внутри (не только в конце); избегай ⚠️ и 🚨
107093
106408
  - Хотя бы одно ключевое слово проблемы, релевантное продукту
107094
106409
  - НЕ упоминай название продукта — заголовок = боль + триггер
107095
- - Максимум ${maxMedTerms} из ${n} заголовков могут содержать прямые медицинские термины
107096
106410
  - Звучит как живая рекламная фраза, а не строка ключевых слов
107097
106411
 
107098
106412
  ТРЕБОВАНИЯ К ТЕКСТУ:
@@ -107101,7 +106415,6 @@ ${distributionLines}
107101
106415
  - Сильный явный CTA в конце («кликни», «попробуй сейчас», «закажи», «не жди»)
107102
106416
  - Короткие рубленые фразы, императивы, FOMO допустим${hasTestimonial ? '\n- Testimonial: строго от первого лица, начинается с «Имя, возраст:», конкретный результат с таймингом' : ''}
107103
106417
  ${deadlineNote}
107104
- - Избегай «гарантированно», «вылечит», «100%», «навсегда»
107105
106418
 
107106
106419
  ОБЩИЕ ТРЕБОВАНИЯ:
107107
106420
  - Язык: ${geo} — все тексты строго на этом языке
@@ -107175,6 +106488,7 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
107175
106488
  // Resolve no-bullets conditions in code — don't leave "if approach = X" for the LLM
107176
106489
  const noBulletsApproachNames = CREO_APPROACHES.filter(a => a.noBullets).map(a => a.name);
107177
106490
  const isNoBullets = noBulletsApproachNames.includes(approachName?.trim() ?? '');
106491
+ const isScreenshotReviews = (approachName?.trim() ?? '') === 'Скрин отзывов';
107178
106492
  // ШАГ 0 — hint for BULLETS depends on approach
107179
106493
  const step0BulletsHint = isNoBullets
107180
106494
  ? `BULLETS: [] (для данного подхода буллиты запрещены — ожидается 0)`
@@ -107195,12 +106509,17 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
107195
106509
  - Буллиты должны быть расположены вертикально (столбиком). Если буллиты идут горизонтально в одну строку — ОШИБКА: плохая читаемость на мобиле
107196
106510
  - Цена визуально ОТЛИЧНА от буллитов (цена — компактный блок без иконок; буллеты — с иконками/галочками). Если цена выглядит как буллет — ОШИБКА.`;
107197
106511
  // ШАГ 3 — TEXT LIMIT: PVP has no badges allowed, normal allows urgency/trust (без проверок по длине/количеству слов)
107198
- const step3TextLimit = isNoBullets
106512
+ const step3TextLimit = isScreenshotReviews
107199
106513
  ? `ШАГ 3 — OTHER_TEXT (критично):
106514
+ - Для «Скрин отзывов» блок отзывов (аватарки, имена, возраст, 5 звёзд, текст отзывов) — это НЕ ошибка, это основной контент.
106515
+ - Допускаются также trust‑печати (0–3 шт). URGENCY — не рекомендуется, но не ошибка.
106516
+ - На креативе допустимы: HOOK, блок отзывов, цена, скидка, CTA, опционально trust‑печати.`
106517
+ : isNoBullets
106518
+ ? `ШАГ 3 — OTHER_TEXT (критично):
107200
106519
  - OTHER_TEXT должен быть ПУСТЫМ (0 элементов). Никаких дополнительных бейджей, подписей, ярлыков, trust‑печатей/urgency — НИЧЕГО.
107201
106520
  - Любой дополнительный текст (бейджи/подписи/пояснения/urgency/trust‑печати) — ОШИБКА: слишком много текста для punch‑креатива.
107202
106521
  - На креативе допустимы ТОЛЬКО: HOOK, цена, скидка, кнопка CTA.`
107203
- : `ШАГ 3 — OTHER_TEXT (критично):
106522
+ : `ШАГ 3 — OTHER_TEXT (критично):
107204
106523
  - По умолчанию любой другой рекламный текст (бейджи/подписи/пояснения) запрещён.
107205
106524
  - НО допускаются (не обязательно) дополнительные бейджи ВНЕ упаковки — это НЕ ошибка, если ВСЕ элементы OTHER_TEXT подпадают под разрешённые типы:
107206
106525
  A) URGENCY (0–1 шт): бейдж срочности/дефицита — только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» и подобные — непонятно что означает.
@@ -107211,7 +106530,7 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
107211
106530
  * TRUST‑печати — про натуральность/премиальность/безопасность (не про доставку/поддержку)
107212
106531
  Примеры стиля (адаптируй к языку GEO, не требуются):
107213
106532
  - URGENCY: «Ostatnie sztuki», «Tylko dziś», «Koniec dziś» (НЕ «24h» — непонятно)
107214
- - TRUST: «Naturalna formuła», «Wysoka jakość», «Bezpieczne», «Eko», «Kontrola jakości»
106533
+ - TRUST: «Naturalna formuła», «Wysoka jakość» (для PL); «Ingrediente naturale», «Calitate» (для RO). ЗАПРЕЩЕНО: «NATURAL», «QUALITY», «100% NATURAL» — английские слова.
107215
106534
  - Если OTHER_TEXT содержит что-то вне этих типов, или URGENCY > 1, или TRUST > 3 (для Минимализма — TRUST > 1) — ОШИБКА.`;
107216
106535
  return `Ты — СТРОГИЙ валидатор рекламных креативов (1:1). Не улучшай и не предлагай идеи — только проверка и вердикт.
107217
106536
  Продукт: ${product}. GEO/язык: ${geo}.${approachLine}
@@ -107233,10 +106552,10 @@ URGENCY_BADGE: "..." (бейдж срочности, если есть; «24h»
107233
106552
  TRUST_BADGES: ["..."] (печати цветные, яркие, в стиле FDA; про натуральность/премиальность/безопасность; размер как у буллитов — крупные, если есть)
107234
106553
 
107235
106554
  ШАГ 1 — HOOK/HEADLINE (критично):
107236
- - Должен быть в верхнем блоке и читабелен (хорошо читается на телефоне)
107237
- - Допускается 1–2 строки (это НЕ ошибка). Ошибка только если текст нечитабелен или есть разрыв/перенос внутри слова.
106555
+ - Должен быть ВЫШЕ всех элементов и читабелен (хорошо читается на телефоне)
106556
+ - Допускается 1–4 строки (это НЕ ошибка). Ошибка только если текст нечитабелен или есть разрыв/перенос внутри слова.
107238
106557
  - Смысл: релевантно проблеме/выгоде продукта. Допускаются и боль/дискомфорт, и обещание/выгода (например «Könnyebb napok»). НЕ считай ошибкой формулировки обещаний результата или клеймы.
107239
- - Желательно: ключевое слово боли/проблемы в формулировке (как часть смысла), но отсутствие ключевого слова само по себе НЕ является ошибкой. Примеры для ориентира: ${keywords.join(', ')} (можно синонимы).
106558
+ - ОБЯЗАТЕЛЬНО: ключевое слово проблемы явно в формулировке. Примеры по категориям: простата простатит, простата; похудение лишний вес, похудение; суставы боль, колени, скованность; пищеварение → дискомфорт, вздутие; сон → бессонница, усталость. Отсутствие ключевого слова — ОШИБКА. Дополнительно для ориентира: ${keywords.join(', ')} (можно синонимы).
107240
106559
  - Проверь вторую часть заголовка. Если она состоит только из абстрактных слов («Mai ușor», «Mai bine», «Soluția», «Ajutor») без конкретного бенефита — отметь: РЕКОМЕНДАЦИЯ: заголовок можно усилить конкретикой
107241
106560
  - Если заголовок слишком общий/абстрактный («Mai ușor», «Mai bine» без конкретики) — отметь как РЕКОМЕНДАЦИЯ к улучшению (не ошибка, но слабо)
107242
106561
  - РЕКОМЕНДАЦИЯ (CTR): верхний текст лучше делать как HOOK — ОЧЕНЬ крупный, жирный, ВСЕ БУКВЫ ПРОПИСНЫЕ, на яркой контрастной плашке. Если это не так — не ошибка, но отметь рекомендацией
@@ -107251,7 +106570,7 @@ ${step3TextLimit}
107251
106570
  ШАГ 4 — CTA > PRICE (критично):
107252
106571
  - CTA: на языке ${geo}, хорошо заметная кнопка (позиция НЕ важна: можно вправо/влево/по центру). Главное — CTA читабельна и выглядит как кнопка.
107253
106572
  - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (до скидки) + новая. Если только одна цена — ОШИБКА. Старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Тонкое/незаметное зачёркивание — ОШИБКА.
107254
- - Скидка ОБЯЗАТЕЛЬНА: «-50%» (возможны варианты символа минуса: -, или ). Ярко, заметно. Строго НЕ на упаковке/банке.
106573
+ - Скидка ОБЯЗАТЕЛЬНА: «-50%» отдельным видимым бейджем (процент должен быть явно читаем, не только в цене). Ярко, заметно. Строго НЕ на упаковке/банке.
107255
106574
  - Не считать ошибкой, если скидка/цена конкурируют с CTA по акценту — это допустимо, пока CTA остаётся хорошо заметной и читабельной.
107256
106575
  - Если скидка указана и она не равна -50% — ошибка.
107257
106576
 
@@ -107264,7 +106583,7 @@ ${step3TextLimit}
107264
106583
  - КОНТРАСТ ПЛАШЕК: если хотя бы одна текстовая плашка (HOOK, буллеты, CTA или цена) визуально сливается с фоном и текст плохо читается — это ОШИБКА.
107265
106584
  - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА. Цена не должна выглядеть как буллет.
107266
106585
  - TRUST‑печати: если печати мелкие или текст нечитабелен на телефоне — ОШИБКА (размер как минимум как у буллитов).
107267
- - СКИДКА «-50%»: ярко, заметно, на контрастной подложке. Строго НЕ на упаковке/банке. Если скидка на продукте или бледная/незаметна — ОШИБКА.
106586
+ - СКИДКА «-50%»: ОБЯЗАТЕЛЬНО отдельным видимым бейджем (процент явно читаем). Если скидка не отображается или только в цене без отдельного «-50%» — ОШИБКА. Ярко, заметно, НЕ на упаковке.
107268
106587
  - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА.
107269
106588
  - Бейджи срочности: «24h» и подобные неясные формулировки — ОШИБКА (непонятно что означает). Ясные («только сегодня», «последние штуки») — ок.
107270
106589
 
@@ -107331,18 +106650,17 @@ CRITICAL RULES (это ВАЛИДАЦИЯ, не рекомендации; есл
107331
106650
 
107332
106651
  ЯЗЫК + ТОН:
107333
106652
  - ВСЕ слова строго на языке ${generateGeo} (HOOK/буллеты/CTA/скидка/опциональный бейдж срочности/опциональные trust‑печати). Английские слова запрещены. Исключений нет.
107334
- - Тон: убедительный, но честный. Лёгкая эмоция и срочность — ок. Запрещено: ложные обещания, диагностика заболеваний, слова «лечит/вылечивает/гарантируем».
106653
+ - Тон: убедительный, но честный. Лёгкая эмоция и срочность — ок.
107335
106654
 
107336
106655
  HOOK / HEADLINE (строгое правило):
107337
- - ровно 35 слов
107338
- - строго 1 строка; без переноса/разбиения слов; без подзаголовка
107339
- - заголовок должен описывать боль пользователя на языке ${generateGeo}; ключевое слово проблемы часть боли (не отдельный ярлык/бейдж)
106656
+ - 14 строки; до 12 слов. Без переноса внутри слова
106657
+ - ОБЯЗАТЕЛЬНО содержит ключевое слово проблемы ЯВНО. Примеры по категориям: простата → простатит, простата; похудение → лишний вес, похудение; суставы → боль в суставах, колени, скованность; пищеварение → дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Без ключевого слова — ОШИБКА
106658
+ - HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). Самый крупный текстовый элемент на креативе
107340
106659
  - 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».
106660
+ - Можно (не обязательно) включить СРОК прямо в HOOK (например «7 dni», «14 dni») как триггер
106661
+ - Заголовок должен быть РЕЗКИМ и призывным: короткие рубленые фразы, вопросы и предупреждения допустимы. Можно использовать обращение на «ты» (в языке GEO: «Masz…», «Twój…») и вопросительный знак
107344
106662
  - заголовок НЕ должен быть абстрактным слоганом/лозунгом или метафорой без конкретной боли. Допускается «thumb‑stop» стиль (вызов/вопрос/предупреждение), если это конкретно и релевантно категории
107345
- - если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт.
106663
+ - если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт. HOOK всегда остаётся выше всех остальных элементов
107346
106664
  - Не повторяй один и тот же заголовок в разных подходах: для текущего подхода придумай новый, уникальный заголовок. Варьируй: угол боли, формулировку, акцент (проблема vs облегчение)
107347
106665
  - Вторая часть заголовка = конкретное улучшение, не абстрактное слово. ПРАВИЛЬНО: «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
106666
  - Специфические медицинские термины или диагнозы разрешены в заголовке, но не обязательны. Варьируй между прямыми формулировками (если релевантно категории) и мягкими формулировками, фокусирующимися на симптомах и дискомфорте, для разных креативов. Не используй один и тот же термин во всех креативах
@@ -107351,27 +106669,34 @@ HOOK / HEADLINE (строгое правило):
107351
106669
  - ТАК (пищеварение): «Puffadás gond? Könnyebb napok»
107352
106670
  - ТАК (суставы): «Ízületi fájdalom? Mozogj könnyebben»
107353
106671
  - ТАК (сон): «Álmatlan éjszakák? Pihenj végre»
107354
- Если заголовок > 5 слов, похож на предложение, распадается на 2 смысловых блока или звучит как лозунг — ОШИБКА → пересоздай вариант.
106672
+ - ТАК (простата): «Prostată? Alinare rapidă»
106673
+ - ТАК (похудение): «Greutate în plus? Fără diete extreme»
106674
+ ❗ Если заголовок > 12 слов, похож на длинное предложение или звучит как лозунг без ключевого слова проблемы — ОШИБКА → пересоздай вариант.
107355
106675
 
107356
106676
  BULLETS (жёсткое правило, ровно 3):
107357
106677
  - По умолчанию: ровно 3 буллета
107358
106678
  - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллиты ЗАПРЕЩЕНЫ (0 буллитов). Не добавляй буллет‑блок вообще
107359
106679
  - каждый 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
106680
  - ВИЗУАЛ БУЛЛЕТОВ: крупные, с иконками/галочками (✓), на контрастных подложках. Буллеты ВНЕ упаковки/банки — не на продукте.
107363
106681
  - Буллиты должны быть расположены ВЕРТИКАЛЬНО (столбиком), не горизонтально в одну строку. Минимальный размер шрифта буллитов — чтобы читались на экране телефона без зума
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», «Лечит простату».
106682
+
106683
+ ВЫБОР БУЛЛЕТОВ (КРИТИЧНО): Выбери СЛУЧАЙНЫЕ 3 буллета из пула ниже в ПРОИЗВОЛЬНОМ порядке. Адаптируй формулировки под язык ${generateGeo} и категорию продукта. НЕ используй один и тот же набор для разных подходов каждый креатив должен иметь УНИКАЛЬНУЮ комбинацию из 3 буллетов.
106684
+
106685
+ ПУЛ ВАРИАНТОВ (выбери 3 случайных, порядок произвольный; адаптируй под GEO и категорию):
106686
+ • Действие/бенефит: 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
106687
+ • Срок/скорость: 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
106688
+ • Соц. доказательство: 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
106689
+ • Формула/состав: Formulă naturală, Ingrediente naturale, Fără chimicale, Compoziție selectată, Testat dermatologic
106690
+ • Качество жизни: Confort activ, Zile fără griji, Mai multă energie, Somn odihnitor, Viață mai ușoară
106691
+
107367
106692
  ❗ Если хоть один буллет > 4 слов или похож на предложение/обещание — ОШИБКА → пересоздай.
107368
106693
 
107369
106694
  TEXT LIMIT:
107370
106695
  - кроме: HOOK, 3 буллетов, цены, скидки, кнопки — ЛЮБОЙ другой текст запрещён (ярлыки/подписи/дисклеймеры/пояснения)
107371
106696
  - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → кроме HOOK, цены, скидки, CTA — НИКАКОГО другого текста. Буллиты запрещены. Other_text/urgency/trust‑печати запрещены
107372
106697
  - ДОПУСКАЕТСЯ (не обязательно) ОДИН бейдж срочности: только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» — непонятно что означает.
107373
- - ДОПУСКАЕТСЯ (не обязательно) 1–3 trust‑печати: печати цветные, яркие, очень похожие на печать FDA. Подчёркивают: натуральность состава, премиальность, безопасность продукта. Допустимо также: качество, натуральные ингредиенты, экологичность, контроль качества. Короткий текст (1–3 слова) на языке ${generateGeo}, компактно по низу. КРИТИЧНО: печати должны быть КРУПНЫМИ размер как минимум как у буллитов, лучше крупнее. Текст читабелен на телефоне без зума. Даже одна печать — крупная. Мелкие печати — ОШИБКА. Запрещено: «рекомендовано врачами», «клинически доказано», «одобрено Минздравом» и любые фразы, требующие доказательств
107374
- - HOOK + буллеты <= 16 слов суммарно; если больше — ОШИБКА → пересоздай
106698
+ - ДОПУСКАЕТСЯ (не обязательно) 1–3 trust‑печати: печати цветные, яркие, в стиле FDA. Текст СТРОГО на языке ${generateGeo} запрещены английские слова («NATURAL», «QUALITY» и т.д.). Примеры для RO: «naturale», «Ingrediente naturale»; для PL: «naturalna». Размер как минимум как у буллитов. Мелкие печати — ОШИБКА.
106699
+ - HOOK + буллеты <= 24 слова суммарно (HOOK до 12, буллеты до 12); если больше — ОШИБКА → пересоздай
107375
106700
  - Если хочешь добавить ощущение срочности — делай это через формулировки в HOOK/BULLETS, через реквизит/иконки, ИЛИ (не обязательно) через один короткий бейдж срочности. Только ясные формулировки («Ostatnie sztuki», «Tylko dziś», «Koniec dziś»). ЗАПРЕЩЕНО «24h» — непонятно что означает.
107376
106701
 
107377
106702
  CTA > PRICE:
@@ -107382,17 +106707,17 @@ CTA > PRICE:
107382
106707
  ❗ ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → цена и «-50%» могут быть более крупными и заметными, но CTA всё равно должен оставаться очень заметным и выглядеть как кнопка (не теряется на фоне)
107383
106708
 
107384
106709
  ПОКАЗЫВАЙ ТОЛЬКО ЭТИ ТЕКСТОВЫЕ ЭЛЕМЕНТЫ:
107385
- - HOOK (1 строка; CAPS; жирный; на яркой контрастной подложке)
106710
+ - HOOK (1–4 строки; выше всех элементов; самый крупный; CAPS; жирный; на яркой контрастной подложке; ключевое слово проблемы явно)
107386
106711
  - 3 буллета (крупные, с иконками/галочками ✓, на контрастных подложках, НЕ на банке/упаковке)
107387
106712
  - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (2×${generatePrice} ${generateCurrency}) зачёркнута ТОЛСТОЙ контрастной линией + новая ${generatePrice} ${generateCurrency} выразительно. Одна цена = ОШИБКА. Тонкая линия зачёркивания = ОШИБКА. Без слов. Не на упаковке.
107388
- - Скидка: «-50%» (обязательно, отдельным бейджем; ярко, заметно; строго НЕ на банке/упаковке; визуально слабее CTA)
106713
+ - Скидка: «-50%» ОБЯЗАТЕЛЬНО отдельным видимым бейджем (не только в цене!). Процент скидки должен быть явно читаем. Ярко, заметно, строго НЕ на банке/упаковке, визуально слабее CTA
107389
106714
  - Кнопка CTA (1-2 слова)
107390
106715
  - Опционально: 1 короткий бейдж срочности (ясные формулировки: «только сегодня», «последние штуки»; ЗАПРЕЩЕНО «24h»). Бейдж в углу кадра, контрастный, но меньше CTA. НЕ обязателен
107391
- - Опционально: 1–3 trust‑печати (цветные, яркие, очень похожие на печать FDA; подчёркивают натуральность, премиальность, безопасность; допустимо: качество, натуральные ингредиенты, экологичность, контроль качества). Короткий текст на языке ${generateGeo}. Размер как минимум как у буллитов, лучше крупнее читабельны на телефоне без зума. Даже одна печать крупная. Для подхода «Минимализм» — макс 1 печать. НЕ обязательны
106716
+ - Опционально: 1–3 trust‑печати (цветные, яркие, в стиле FDA; натуральность, премиальность, безопасность). Текст СТРОГО на языке ${generateGeo} никакого английского. Для RO: «naturale», «Ingrediente naturale», «Calitate»; для PL: «naturalna», «naturalne»; НЕ «NATURAL», «QUALITY». Размер как минимум как у буллитов. НЕ обязательны
107392
106717
  - ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → показывай только: HOOK + PRICE + DISCOUNT + CTA. Буллиты/urgency/trust‑печати запрещены
107393
106718
 
107394
106719
  ВИЗУАЛ:
107395
- - Иерархия: HOOK > продукт > CTA > цена > буллеты
106720
+ - Иерархия: HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). HOOK — самый крупный текстовый элемент
107396
106721
  - Буллеты: крупные, с иконками/галочками (✓), на контрастных подложках. Строго ВНЕ упаковки/банки.
107397
106722
  - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание = ОШИБКА. Визуально ОТЛИЧНА от буллитов.
107398
106723
  - Скидка «-50%»: ярко, заметно, на контрастной подложке. Строго ВНЕ упаковки/банки.
@@ -107402,8 +106727,9 @@ CTA > PRICE:
107402
106727
  - Без ссылок/доменов и мелкого текста
107403
106728
  - LIFESTYLE/CLEAN: продукт в контексте использования. Для пищеварения — кухня, обеденный стол, рядом с едой/чашкой. Для сна/простаты — спальня, тумбочка, стакан воды. Для суставов — стол/тумбочка рядом с эластичным бинтом, ортезом или активным фоном (кроссовки, лестница). Контекст = момент приёма. Просто продукт на белом фоне — НЕ допускается
107404
106729
  - Цвета и композиция должны быть «thumb‑stop»: высокий контраст, яркий акцент на продукте (луч света/подсветка/обводка/глоу), избегай бледных пастельных сцен
107405
- - ЛЮДИ: человек допускается ТОЛЬКО для подхода «Эмоция / Портрет». Для всех остальных подходов — БЕЗ человека.
107406
- - Если есть человек (только в «Эмоция / Портрет»): прямой взгляд в камеру (как обращение лично к зрителю), эмоция облегчения/надежды (без стоковой улыбки). Возраст человека ДОЛЖЕН соответствовать категории и целевой аудитории продукта: по умолчанию 50–60, но для категорий, где аудитория обычно моложе (например похудение/фитнес) допускается 35–55. Этнически соответствует GEO/рынку
106730
+ - ЛЮДИ: человек допускается ТОЛЬКО для подходов «Эмоция / Портрет» и «Врач / Эксперт». Для всех остальных — БЕЗ человека.
106731
+ - Если «Эмоция / Портрет»: прямой взгляд в камеру, эмоция облегчения/надежды. Возраст 50–60 (похудение/фитнес 35–55). Этнически соответствует GEO/рынку.
106732
+ - Если «Врач / Эксперт»: женщина-врач 40–55 лет. Внешность и этничность СТРОГО соответствуют GEO/рынку (локал — врач выглядит как местный специалист).
107407
106733
 
107408
106734
  ANTI-TEMPLATE DIVERSITY (КРИТИЧНО):
107409
106735
  - Ты сейчас создаёшь ОДИН креатив для текущего подхода. В проекте будет серия из ${totalApproaches} креативов, поэтому у каждого подхода должна быть своя узнаваемая композиция.
@@ -107411,15 +106737,17 @@ ANTI-TEMPLATE DIVERSITY (КРИТИЧНО):
107411
106737
  - Ориентируйся на строку «🎯 ПОДХОД: ...» и примени соответствующую раскладку ниже (только одну, соответствующую текущему подходу):
107412
106738
  * 🎯 ПОДХОД: Эксперт / Авторитет → БЕЗ человека. Профессиональный контекст с реквизитом экспертизы вокруг продукта. Продукт в центре. HOOK сверху справа, BULLETS справа ниже, CTA снизу справа, PRICE/DISCOUNT рядом с CTA (слабее CTA). Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
107413
106739
  * 🎯 ПОДХОД: 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‑печати — строго в нижней трети, НЕ перекрывают лицо и не конкурируют с ним по размеру.
106740
+ * 🎯 ПОДХОД: Эмоция / Портрет → Один из двух подходов с человеком. ОЧЕНЬ крупный портрет лица (thumb‑stop), взгляд в камеру — лицо занимает верхние 2/3 кадра. Продукт в руке или у подбородка. HOOK сверху по центру. BULLETS, CTA, PRICE/DISCOUNT, trust‑печати — в нижней трети.
106741
+ * 🎯 ПОДХОД: Врач / Эксперт → Женщина-врач (локал по GEO), продукт в руках или рядом. Профессиональный фон. HOOK сверху, BULLETS сбоку/снизу, CTA и PRICE/DISCOUNT внизу. Врач и продукт — главные элементы.
106742
+ * 🎯 ПОДХОД: Скрин отзывов → Имитация скриншота: блок отзывов (аватарки, имена, возраст, 5 звёзд, текст на языке GEO). HOOK ОБЯЗАТЕЛЕН — выше отзывов или поверх, крупно. Продукт виден. CTA, PRICE, DISCOUNT внизу.
107415
106743
  * 🎯 ПОДХОД: Визуализация проблемы → Инфографика/схема: слева проблема (иконка/схема зоны тела, релевантной продукту), справа решение + продукт. HOOK сверху по центру, BULLETS справа или снизу (вертикально), CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне). Красный акцент только на проблеме.
107416
106744
  * 🎯 ПОДХОД: Power / Сила решения → БЕЗ человека. ДИНАМИЧНЫЙ комикс‑кадр, диагональная композиция. Продукт как “герой” + power‑иконки (щит/молния/бёрст). HOOK сверху слева на яркой плашке, BULLETS слева ниже, CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
107417
106745
  * 🎯 ПОДХОД: Минимализм / 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 — плоская кнопка без градиентов. БЕЗ буллитов, бейджей, теней, иконок.
106746
+ * 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK 1–4 строки, до 12 слов, огромный CAPS, выше всех. БЕЗ буллитов. PRICE + «-50%» крупно и заметно. CTA контрастной кнопкой
106747
+ * 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — 1–4 строки, до 12 слов, крупный, грубый bold/рукописный, CAPS, выше всех. Крупные надписи цены/скидки вокруг продукта. CTA — плоская кнопка без градиентов. БЕЗ буллитов, бейджей, теней, иконок.
107420
106748
 
107421
106749
  AUTO-CHECK (перед финалом):
107422
- - HOOK: 35 слов, 1 строка, CAPS, жирный на контрастной плашке, боль + направление облегчения (не лозунг), язык ${generateGeo} без английских слов
106750
+ - HOOK: 14 строки, до 12 слов, CAPS, жирный на контрастной плашке, выше всех элементов, ключевое слово проблемы явно, язык ${generateGeo} без английских слов
107423
106751
  - Буллеты: по умолчанию ровно 3, каждый <= 4 слов, без запятых, не предложения. ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллетов 0 (запрещены)
107424
106752
  - Нет лишнего текста кроме: HOOK, буллетов, цены, скидки, кнопки (+ опционально 1 бейдж срочности + опционально 1–3 trust‑печати)
107425
106753
  - Есть скидка «-50%» (вне упаковки) и она слабее CTA
@@ -107463,38 +106791,38 @@ const CREO_APPROACHES = [
107463
106791
  {
107464
106792
  name: 'Эксперт / Авторитет',
107465
106793
  prompt: `ЭКСПЕРТ / АВТОРИТЕТ (без человека): стиль “рекомендация эксперта”, но БЕЗ человека. Профессиональный контекст — аптечные полки на фоне / медицинский планшет / стетоскоп / рецептурный блокнот как реквизит рядом с продуктом. Продукт в центре как “рекомендованное решение”. Чистый профессиональный фон, высокий контраст. Trust‑печати (1–3 шт): цветные, яркие, очень похожие на печать FDA; подчёркивают натуральность состава, премиальность, безопасность. Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная.`,
107466
- headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 35 слов, 1 строка, CAPS.`,
107467
- bulletsFocus: `БУЛЛИТЫ: “доверие + действие + конкретика”. Минимум 1 буллет с действием на главную боль (глагол), минимум 1 со сроком/скоростью, плюс 1 с цифрой/соц.доказательством (если уместно). Всё строго на языке GEO и релевантно категории. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры (например «Naturalna formuła» сама по себе).`
106794
+ headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
106795
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор для этого подхода не повторяй комбинации из других креативов.`
107468
106796
  },
107469
106797
  {
107470
106798
  name: 'Lifestyle / Момент приёма',
107471
106799
  prompt: `LIFESTYLE / МОМЕНТ ПРИЁМА: продукт в контексте использования (кухня/стол/спальня/тумбочка). Без человека, но с "историей" (стакан воды, чай, тарелка, будильник/часы/календарь). Срочность: реквизит (часы/таймер‑иконка) и/или опциональный urgency‑бейдж (1–3 слова) в углу кадра — только ясные формулировки («только сегодня», «последние штуки»), ЗАПРЕЩЕНО «24h». Высокий контраст, яркий акцент на продукте. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ВАЖНО: буллиты располагаются вертикально и всегда читабельны на экране телефона без зума — даже если уходят вбок, минимальный размер шрифта буллитов не уменьшается. Если буллиты не вмещаются сбоку с нужным шрифтом — переставь их вниз.`,
107472
- headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 35 слов, 1 строка, CAPS.`,
107473
- bulletsFocus: `БУЛЛИТЫ: активные глаголы + сроки + цифры. Минимум 1 буллет со сроком/скоростью, минимум 1 с цифрой/соц.доказательством. Всё 2–4 слова, без запятых, строго на языке GEO и релевантно категории. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
106800
+ headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
106801
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (срок + цифра + действие/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107474
106802
  },
107475
106803
  {
107476
106804
  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. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
106805
+ prompt: `ЭМОЦИЯ / ПОРТРЕТ: один из двух подходов с человеком в серии. Используй максимально: ОЧЕНЬ крупный план лица (thumb‑stop), прямой взгляд в камеру, сильная эмоция облегчения/надежды (без широкой стоковой улыбки). Возраст человека подбери под категорию и ЦА продукта: по умолчанию 50–60, но для категорий с более молодой аудиторией (например похудение/фитнес) допускается 35–55. Продукт в руке на уровне лица или рядом, хорошо виден. Свет "тень → свет" на лице допустим. Минимум отвлекающих деталей, высокий контраст. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ИЕРАРХИЯ: лицо — главный и доминирующий визуальный элемент (верхние 2/3 кадра). Все текстовые блоки (HOOK исключение — сверху) уходят в нижнюю треть. Буллиты, цена, CTA НЕ перекрывают лицо и НЕ конкурируют с ним по визуальному весу — они заметно меньше и ниже.`,
106806
+ headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение». 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно. Тон максимально личный и прямой.`,
106807
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (качество жизни + действие + срок/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107480
106808
  },
107481
106809
  {
107482
106810
  name: 'Визуализация проблемы',
107483
106811
  prompt: `ВИЗУАЛИЗАЦИЯ ПРОБЛЕМЫ (без человека): схема/иконка проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав/позвоночник; для пищеварения — желудок; для простаты — силуэт мужчины) + мягкий красный акцент на этой зоне (не шок‑контент, без графики). Рядом продукт как решение. Можно добавить простую стрелку/переход “проблема → облегчение” как графику (без лишнего текста). Чистый фон, высокая читабельность. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека.`,
107484
- headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 35 слов, 1 строка, CAPS, вопросительный знак допустим.`,
107485
- bulletsFocus: `БУЛЛИТЫ: механика/результат в активной форме + срок/скорость + конкретика. Без диагнозов и гарантий. Всё строго на GEO. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
106812
+ headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно, вопросительный знак допустим.`,
106813
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107486
106814
  },
107487
106815
  {
107488
106816
  name: 'Power / Сила решения',
107489
106817
  prompt: `POWER / СИЛА РЕШЕНИЯ (без человека): метафоры силы и победы над проблемой: щит, молния, энергия, взрыв‑бёрст, мощные стикеры/иконки. Продукт как “герой” в центре, высокая энергия, контрастные агрессивные цвета. Можно комикс/anti‑design подачу, но всё должно быть читабельно. Без лишнего текста. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека. Только продукт + power‑иконки.`,
107490
- headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 35 слов, 1 строка, CAPS.`,
107491
- bulletsFocus: `БУЛЛИТЫ: активные глаголы + скорость/срок + конкретика + (если уместно) цифра. Без гарантий. Всё строго на GEO. ЗАПРЕЩЕНО: абстрактные буллиты без действия/результата/срока/цифры.`
106818
+ headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
106819
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + скорость + цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107492
106820
  },
107493
106821
  {
107494
106822
  name: 'Минимализм / Clean Big Text',
107495
106823
  prompt: `МИНИМАЛИЗМ / CLEAN BIG TEXT (без человека): белый или мягкий градиентный фон, премиальное ощущение. ОГРОМНЫЙ HOOK как главный элемент (CAPS, жирный, на контрастной плашке). Продукт крупно, минимум реквизита, минимум шума. Тени/премиальные материалы допустимы, но без лишнего текста. БЕЗ человека. Urgency и trust‑печати в этом подходе НЕ рекомендуются — они нарушают чистоту и ощущение премиальности. Добавляй печати только если это критически необходимо, не более 1, но крупную — размер как у буллитов.`,
107496
- headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 35 слов, 1 строка, CAPS.`,
107497
- bulletsFocus: `БУЛЛИТЫ: максимально коротко, но конкретно: действие + срок/скорость + цифра/доказательство (если уместно). Никаких абстрактных буллитов без конкретики. Всё строго на GEO.`
106824
+ headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 14 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
106825
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + формула/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор не повторяй комбинации из других креативов.`
107498
106826
  },
107499
106827
  {
107500
106828
  name: 'Problem Visual Punch',
@@ -107503,14 +106831,14 @@ const CREO_APPROACHES = [
107503
106831
  - Фон: яркий сплошной цвет (красный/оранжевый) или агрессивный градиент
107504
106832
  - Центр: КРУПНАЯ иконка/схема проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав; для простаты — силуэт мужчины; для пищеварения — желудок), с красным свечением
107505
106833
  - Продукт: крупно рядом
107506
- - HOOK: 3–4 слова МАКСИМУМ, огромный, CAPS
106834
+ - HOOK: 1–4 строки, до 12 слов, огромный, CAPS, ключевое слово проблемы явно
107507
106835
  - БЕЗ буллитов
107508
106836
  - Цена + скидка: крупно, заметно
107509
106837
  - CTA: контрастная яркая кнопка
107510
106838
 
107511
106839
  Цель: считывание за 1 секунду. Минимум текста, максимум визуального удара.
107512
106840
  БЕЗ человека. Никаких trust‑печатей/urgency бейджей — только HOOK + PRICE + -50% + CTA.`,
107513
- headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 3–4 слова, CAPS.`,
106841
+ headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
107514
106842
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK и крупных надписях.`
107515
106843
  },
107516
106844
  {
@@ -107525,8 +106853,33 @@ const CREO_APPROACHES = [
107525
106853
  - Крупные текстовые надписи рядом с продуктом: цена, скидка; допустимо одно короткое слово-восклицание («РАБОТАЕТ!», «ПРОВЕРЕНО!») на языке GEO
107526
106854
  - Кнопка CTA: плоская, без градиентов, контрастный сплошной цвет
107527
106855
  - НИКАКИХ буллитов, trust‑печатей, urgency‑бейджей, иконок — только продукт и текст`,
107528
- headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 3–4 слова, CAPS, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
106856
+ headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
107529
106857
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Всё — в HOOK и крупных надписях вокруг продукта (цена, скидка, одно слово-восклицание).`
106858
+ },
106859
+ {
106860
+ name: 'Врач / Эксперт',
106861
+ prompt: `ВРАЧ / ЭКСПЕРТ (с человеком): женщина-врач как авторитет, рекомендующий продукт. Один из двух подходов с человеком в серии.
106862
+ - Врач: женщина 40–55 лет, профессиональный вид (медицинский халат, планшет/стетоскоп/рецептурный блокнот). Уверенная, располагающая поза.
106863
+ - ЛОКАЛИЗАЦИЯ (КРИТИЧНО): врач НЕ generic — явно угадывается регион/GEO. Черты лица, тип кожи, причёска — характерные для целевого рынка. RO → румынский тип; PL → польский; HU → венгерский; ES → испанский/латино; IT → итальянский. Врач = местный специалист, не стоковое универсальное лицо.
106864
+ - Продукт: в руках врача или на столе/планшете рядом, хорошо виден.
106865
+ - Фон: профессиональный (кабинет, аптечные полки, медицинский контекст). Высокий контраст, читабельность.
106866
+ - Опционально: 1–3 trust‑печати по низу (цветные, яркие, в стиле FDA). Размер как минимум как у буллитов.
106867
+ - ИЕРАРХИЯ: врач + продукт — главные элементы. HOOK сверху, BULLETS сбоку или снизу, CTA и PRICE/DISCOUNT в нижнем блоке.`,
106868
+ headlineAngle: `HOOK: угол «врач/специалист рекомендует» или «эксперты знают». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
106869
+ bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
106870
+ },
106871
+ {
106872
+ name: 'Скрин отзывов',
106873
+ noBullets: true,
106874
+ prompt: `СКРИН ОТЗЫВОВ: имитация реалистичного скриншота с положительными благодарными отзывами и оценками 5 звёзд.
106875
+ - Визуал: реалистичный стиль — как скриншот приложения/сайта отзывов (App Store, Google Play, или страница отзывов). Высокое качество, читабельный текст.
106876
+ - Отзывы: 2–4 отзыва с аватарками, именами и возрастом (формат «Имя, 45 лет»), ОБЯЗАТЕЛЬНО 5 звёзд (★★★★★) в каждом, короткий текст благодарности. Текст отзывов СТРОГО на языке GEO — никакого английского. Релевантен категории продукта.
106877
+ - HOOK ОБЯЗАТЕЛЕН: крупно, выше блока отзывов или поверх, CAPS, на яркой подложке. Ключевое слово проблемы явно.
106878
+ - Продукт: виден в кадре (рядом со скрином или интегрирован в композицию).
106879
+ - Цена, скидка «-50%», CTA — в нижней части. БЕЗ буллитов — отзывы заменяют их.
106880
+ - Стиль: не мультяшный, не комикс — максимально реалистичный скрин.`,
106881
+ headlineAngle: `HOOK: угол «тысячи довольны» / «9 из 10 рекомендуют» / «реальный результат». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
106882
+ bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK, отзывах (с 5 звёздами), цене и CTA.`
107530
106883
  }
107531
106884
  ];
107532
106885