docs-combiner 0.1.11 → 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
@@ -99948,8 +99948,7 @@ function App() {
99948
99948
  const [linkCopied, setLinkCopied] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99949
99949
  const [openRouterKeyCopied, setOpenRouterKeyCopied] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99950
99950
  const [loadingContentFromDrive, setLoadingContentFromDrive] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99951
- const [loadingImagesFromDrive, setLoadingImagesFromDrive] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
99952
- 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 });
99953
99952
  // Theme state
99954
99953
  const [darkMode, setDarkMode] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => {
99955
99954
  const saved = localStorage.getItem('themeMode');
@@ -100117,7 +100116,7 @@ function App() {
100117
100116
  const timeoutId = setTimeout(async () => {
100118
100117
  try {
100119
100118
  logToTerminal('log', '[Auto Save] Auto-saving settings to Google Drive');
100120
- await saveGeneratedContentToDrive(folderId, generatedTitlesData, generatedTextsData, {
100119
+ await saveGeneratedContentToDrive(folderId, {
100121
100120
  generateProduct,
100122
100121
  generateGeo,
100123
100122
  generateAdditionalInfo,
@@ -100130,14 +100129,13 @@ function App() {
100130
100129
  }
100131
100130
  }, 1000);
100132
100131
  return () => clearTimeout(timeoutId);
100133
- }, [generateProduct, generateGeo, generateAdditionalInfo, generatePriceWithCurrency, brand, link, driveFolderUrl, generatedTitlesData, generatedTextsData, loadingContentFromDrive]);
100132
+ }, [generateProduct, generateGeo, generateAdditionalInfo, generatePriceWithCurrency, brand, link, driveFolderUrl, loadingContentFromDrive]);
100134
100133
  // Load generated content from Google Drive when driveFolderUrl changes
100135
100134
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
100136
100135
  if (!driveFolderUrl) {
100137
100136
  logToTerminal('log', '[Load] No driveFolderUrl, skipping load');
100138
100137
  setLoadingContentFromDrive(false);
100139
- setLoadingImagesFromDrive(false);
100140
- setDriveFilesFound({ content: false, images: false });
100138
+ setDriveFilesFound({ content: false });
100141
100139
  // Clear all data when driveFolderUrl is cleared
100142
100140
  setGeneratedTitlesData([]);
100143
100141
  setGeneratedTextsData([]);
@@ -100151,8 +100149,7 @@ function App() {
100151
100149
  if (!folderId) {
100152
100150
  logToTerminal('warn', '[Load] Invalid driveFolderUrl, cannot extract folderId:', driveFolderUrl);
100153
100151
  setLoadingContentFromDrive(false);
100154
- setLoadingImagesFromDrive(false);
100155
- setDriveFilesFound({ content: false, images: false });
100152
+ setDriveFilesFound({ content: false });
100156
100153
  // Clear all data when folderId cannot be extracted
100157
100154
  setGeneratedTitlesData([]);
100158
100155
  setGeneratedTextsData([]);
@@ -100171,8 +100168,7 @@ function App() {
100171
100168
  setTexts(['']);
100172
100169
  setLink('');
100173
100170
  setLoadingContentFromDrive(true);
100174
- setLoadingImagesFromDrive(true);
100175
- setDriveFilesFound({ content: false, images: false });
100171
+ setDriveFilesFound({ content: false });
100176
100172
  // Load content from Google Drive
100177
100173
  loadGeneratedContentFromDrive(folderId).then((result) => {
100178
100174
  logToTerminal('log', '[Load] Content loading completed, found:', result.found);
@@ -100181,8 +100177,6 @@ function App() {
100181
100177
  logToTerminal('error', '[Load] Error loading content from Google Drive:', err);
100182
100178
  setLoadingContentFromDrive(false);
100183
100179
  });
100184
- // Креативы не кешируются — не загружаем изображения при открытии
100185
- setLoadingImagesFromDrive(false);
100186
100180
  }, [driveFolderUrl]);
100187
100181
  // Sync generatedTitlesData with titles when titles changes (except when user is editing in UI)
100188
100182
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
@@ -100445,8 +100439,12 @@ function App() {
100445
100439
  const name = f.name?.toLowerCase() || '';
100446
100440
  return name === 'product.png' || name === 'product.jpg';
100447
100441
  });
100442
+ const hasCreativeImages = data.files.some((f) => {
100443
+ const name = f.name?.toLowerCase() || '';
100444
+ return name !== 'product.png' && name !== 'product.jpg';
100445
+ });
100448
100446
  if (!cancelled) {
100449
- setFolderFilesInfo({ hasProduct });
100447
+ setFolderFilesInfo({ hasProduct, hasCreativeImages });
100450
100448
  // If product not found, poll every 10 seconds until found
100451
100449
  if (!hasProduct) {
100452
100450
  pollTimeoutId = setTimeout(() => {
@@ -101599,20 +101597,8 @@ function App() {
101599
101597
  try {
101600
101598
  const folderId = extractFolderId(driveFolderUrl);
101601
101599
  if (folderId) {
101602
- addLog(formatLogMessage('log', '💾 Saving generated content to Google Drive...'));
101603
- const currentTitlesData = pairsResult.map((p, index) => ({
101604
- index: index + 1,
101605
- title: p.title,
101606
- generating: false,
101607
- failed: !p.title
101608
- }));
101609
- const currentTextsData = pairsResult.map((p, index) => ({
101610
- index: index + 1,
101611
- text: p.text,
101612
- generating: false,
101613
- failed: !p.text
101614
- }));
101615
- await saveGeneratedContentToDrive(folderId, currentTitlesData, currentTextsData, {
101600
+ addLog(formatLogMessage('log', '💾 Saving settings to Google Drive...'));
101601
+ await saveGeneratedContentToDrive(folderId, {
101616
101602
  generateProduct,
101617
101603
  generateGeo,
101618
101604
  generateAdditionalInfo,
@@ -102207,24 +102193,26 @@ function App() {
102207
102193
  if (!file)
102208
102194
  return;
102209
102195
  e.target.value = '';
102210
- const validToken = await getValidAccessToken();
102211
- if (!validToken) {
102212
- alert('Войдите в Google Drive');
102213
- return;
102214
- }
102215
- const folderId = extractFolderId(driveFolderUrl);
102216
- if (!folderId) {
102217
- alert('Некорректная ссылка на папку Google Drive');
102218
- return;
102219
- }
102220
102196
  const ext = file.name.toLowerCase().split('.').pop();
102221
102197
  if (ext !== 'png' && ext !== 'jpg' && ext !== 'jpeg') {
102222
102198
  alert('Выберите изображение PNG или JPG');
102223
102199
  return;
102224
102200
  }
102225
102201
  setUploadingProduct(true);
102226
- const addLog = () => { };
102227
102202
  try {
102203
+ const validToken = await getValidAccessToken();
102204
+ if (!validToken) {
102205
+ alert('Войдите в Google Drive');
102206
+ setUploadingProduct(false);
102207
+ return;
102208
+ }
102209
+ const folderId = extractFolderId(driveFolderUrl);
102210
+ if (!folderId) {
102211
+ alert('Некорректная ссылка на папку Google Drive');
102212
+ setUploadingProduct(false);
102213
+ return;
102214
+ }
102215
+ const addLog = () => { };
102228
102216
  const dataUrl = await new Promise((resolve, reject) => {
102229
102217
  const reader = new FileReader();
102230
102218
  reader.onloadend = () => resolve(reader.result);
@@ -102235,7 +102223,7 @@ function App() {
102235
102223
  logToTerminal('log', '📤 Загрузка product.png/jpg...');
102236
102224
  await uploadImageToDrive(dataUrl, filename, folderId, addLog);
102237
102225
  logToTerminal('log', '✅ product.png/jpg загружен');
102238
- setFolderFilesInfo({ hasProduct: true });
102226
+ setFolderFilesInfo(prev => prev ? { ...prev, hasProduct: true } : { hasProduct: true });
102239
102227
  alert('product.png/jpg успешно загружен в папку!');
102240
102228
  }
102241
102229
  catch (err) {
@@ -102631,483 +102619,476 @@ function App() {
102631
102619
  alert('OpenRouter API key is not set');
102632
102620
  return;
102633
102621
  }
102634
- const validToken = await getValidAccessToken();
102635
- if (!validToken) {
102636
- alert('Please log in with Google first');
102637
- return;
102638
- }
102639
102622
  setGeneratingLanding(true);
102640
102623
  setLandingGenerationLogs([]);
102641
102624
  setLandingProcessStartTime(Date.now());
102642
- const addLog = (msg) => {
102643
- setLandingGenerationLogs(prev => [...prev, msg]);
102644
- };
102645
102625
  try {
102646
- const folderId = extractFolderId(driveFolderUrl);
102647
- if (!folderId) {
102648
- throw new Error('Invalid Google Drive Folder URL');
102649
- }
102650
- // Get product image
102651
- addLog(formatLogMessage('log', '🔍 Получение изображения продукта...'));
102652
- logToTerminal('log', '🔍 Получение изображения продукта...');
102653
- const productImage = await fetchProductImage(folderId);
102654
- if (!productImage) {
102655
- throw new Error('product.png/jpg not found in the folder');
102656
- }
102657
- // Download product image as blob
102658
- addLog(formatLogMessage('log', '📥 Скачивание product.png...'));
102659
- logToTerminal('log', '📥 Скачивание product.png...');
102660
- let token = await getValidAccessToken();
102661
- if (!token) {
102662
- 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;
102663
102631
  }
102664
- let imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${productImage.id}?alt=media`, {
102665
- headers: {
102666
- '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');
102667
102639
  }
102668
- });
102669
- // Handle 401 - try to refresh token and retry
102670
- if (imageResponse.status === 401 && refreshToken) {
102671
- addLog(formatLogMessage('log', '🔄 Got 401, refreshing token...'));
102672
- const newToken = await refreshAccessToken(refreshToken);
102673
- if (newToken) {
102674
- token = newToken;
102675
- addLog(formatLogMessage('log', '✅ Token refreshed, retrying download...'));
102676
- imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${productImage.id}?alt=media`, {
102677
- headers: {
102678
- 'Authorization': `Bearer ${newToken}`
102679
- }
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);
102680
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);
102681
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);
102682
102733
  }
102683
- if (!imageResponse.ok) {
102684
- throw new Error(`Failed to download product image: ${imageResponse.status} ${imageResponse.statusText}`);
102685
- }
102686
- const productImageBlob = await imageResponse.blob();
102687
- addLog(formatLogMessage('log', '✅ Изображение продукта скачано'));
102688
- logToTerminal('log', '✅ Изображение продукта скачано, размер:', (productImageBlob.size / 1024).toFixed(2), 'KB');
102689
- // Generate HTML landing page
102690
- if (!generateProduct.trim() || !generateGeo.trim()) {
102691
- throw new Error('Please fill in Product and Geo fields');
102692
- }
102693
- addLog(formatLogMessage('log', '🎨 Генерация HTML лендинга...'));
102694
- logToTerminal('log', '🎨 Генерация HTML лендинга...');
102695
- const htmlContent = await generateLandingHTML(generateProduct, generateGeo, addLog);
102696
- addLog(formatLogMessage('log', '✅ HTML лендинг сгенерирован'));
102697
- // Save HTML and image for preview
102698
- setGeneratedLandingHTML(htmlContent);
102699
- setGeneratedLandingImageBlob(productImageBlob);
102700
- // Open preview in browser immediately
102701
- try {
102702
- addLog(formatLogMessage('log', '👁️ Открытие предпросмотра в браузере...'));
102703
- logToTerminal('log', '👁️ Открытие предпросмотра в браузере...');
102704
- // Convert image blob to base64
102705
- const imageBase64 = await new Promise((resolve, reject) => {
102706
- const reader = new FileReader();
102707
- reader.onloadend = () => {
102708
- const base64String = reader.result;
102709
- resolve(base64String);
102710
- };
102711
- reader.onerror = reject;
102712
- reader.readAsDataURL(productImageBlob);
102713
- });
102714
- // Replace relative image path with base64 data URL in HTML
102715
- const htmlWithImage = htmlContent.replace(/src=["']product\.(png|jpe?g)["']/gi, `src="${imageBase64}"`);
102716
- // Create blob URL and open in new window
102717
- const blob = new Blob([htmlWithImage], { type: 'text/html' });
102718
- const url = URL.createObjectURL(blob);
102719
- window.open(url, '_blank');
102720
- addLog(formatLogMessage('log', '✅ Предпросмотр открыт в браузере'));
102721
- logToTerminal('log', '✅ Предпросмотр открыт в браузере');
102722
- // Clean up URL after a delay (give browser time to load)
102723
- setTimeout(() => {
102724
- URL.revokeObjectURL(url);
102725
- }, 1000);
102734
+ catch (err) {
102735
+ const errorMsg = 'Ошибка создания лендинга: ' + err.message;
102736
+ addLog(formatLogMessage('error', errorMsg));
102737
+ logToTerminal('error', '❌ Ошибка создания лендинга:', err);
102738
+ alert(errorMsg);
102726
102739
  }
102727
- catch (previewErr) {
102728
- addLog(formatLogMessage('warn', '⚠️ Не удалось открыть предпросмотр: ' + previewErr.message));
102729
- logToTerminal('warn', '⚠️ Не удалось открыть предпросмотр:', previewErr);
102740
+ finally {
102741
+ setGeneratingLanding(false);
102730
102742
  }
102731
- // Create ZIP archive with HTML and product image
102732
- addLog(formatLogMessage('log', '📦 Создание ZIP архива с HTML и изображением...'));
102733
- logToTerminal('log', '📦 Создание ZIP архива...');
102734
- const zipBlob = await createZipArchive(htmlContent, productImageBlob, addLog);
102735
- addLog(formatLogMessage('log', '✅ ZIP архив создан'));
102736
- // Upload ZIP to Google Drive
102737
- const zipFilename = `landing_${Date.now()}.zip`;
102738
- addLog(formatLogMessage('log', '📤 Загрузка ZIP архива на Google Drive...'));
102739
- logToTerminal('log', '📤 Загрузка ZIP архива на Google Drive...');
102740
- const driveLink = await uploadZipToDrive(zipBlob, zipFilename, folderId, addLog);
102741
- addLog(formatLogMessage('log', '✅ ZIP архив загружен на Google Drive'));
102742
- logToTerminal('log', '✅ ZIP архив загружен:', driveLink);
102743
102743
  }
102744
102744
  catch (err) {
102745
- const errorMsg = 'Ошибка создания лендинга: ' + err.message;
102746
- addLog(formatLogMessage('error', errorMsg));
102747
- logToTerminal('error', '❌ Ошибка создания лендинга:', err);
102748
- alert(errorMsg);
102749
- }
102750
- finally {
102751
102745
  setGeneratingLanding(false);
102746
+ logToTerminal('error', '❌ Error:', err?.message || err);
102752
102747
  }
102753
102748
  };
102754
102749
  const handleGenerateImages = async () => {
102755
- logToTerminal('log', '🎨 === Starting image generation process ===');
102756
- logToTerminal('log', '📅 Timestamp:', new Date().toISOString());
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
- return;
102762
- }
102763
- logToTerminal('log', '✅ Google authentication OK');
102764
- if (!generateProduct.trim() || !generateGeo.trim()) {
102765
- logToTerminal('error', '❌ Missing product or geo');
102766
- alert('Please fill in Product and Geo fields');
102767
- return;
102768
- }
102769
- const { price: generatePrice, currency: generateCurrency, currencySymbol } = parsePriceAndCurrency(generatePriceWithCurrency);
102770
- // Use original currency symbol if available, otherwise use currency code
102771
- const currencyForPrompt = currencySymbol || generateCurrency;
102772
- logToTerminal('log', '📦 Product:', generateProduct);
102773
- logToTerminal('log', '🌍 Geo:', generateGeo);
102774
- logToTerminal('log', '💰 Price:', generatePrice, currencyForPrompt);
102775
- if (!driveFolderUrl.trim()) {
102776
- logToTerminal('error', '❌ Missing Drive folder URL');
102777
- alert('Please fill in Google Drive Folder URL');
102778
- return;
102779
- }
102780
- logToTerminal('log', '📁 Drive folder URL:', driveFolderUrl);
102781
102750
  setGeneratingImages(true);
102782
102751
  setImagesGenerationLogs([]);
102783
102752
  setImagesProcessStartTime(Date.now());
102784
102753
  setCheckingImages(false);
102785
- const overallStartTime = Date.now();
102786
- const addLog = (msg) => {
102787
- setImagesGenerationLogs(prev => [...prev, msg]);
102788
- };
102754
+ logToTerminal('log', '🎨 === Starting image generation process ===');
102755
+ logToTerminal('log', '📅 Timestamp:', new Date().toISOString());
102789
102756
  try {
102790
- const folderId = extractFolderId(driveFolderUrl);
102791
- if (!folderId) {
102792
- const errorMsg = 'Invalid Google Drive Folder URL';
102793
- addLog(formatLogMessage('error', '❌ Invalid folder ID extracted'));
102794
- 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;
102795
102763
  }
102796
- addLog(formatLogMessage('log', '✅ Folder ID extracted:', folderId));
102797
- // Check for product.png
102798
- addLog(formatLogMessage('log', '🔍 Checking for product.png...'));
102799
- const productImage = await fetchProductImage(folderId);
102800
- if (!productImage) {
102801
- const errorMsg = 'product.png/jpg not found in the folder. Please generate it first using "Generate Product from Banka" button.';
102802
- addLog(formatLogMessage('error', errorMsg));
102803
- 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;
102804
102770
  }
102805
- addLog(formatLogMessage('log', '✅ product.png found'));
102806
- // Generate images with different approaches (количество по каждому подходу)
102807
- const approaches = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getCreoApproaches)();
102808
- if (approaches.length === 0) {
102809
- addLog(formatLogMessage('error', ' Укажите количество изображений (хотя бы 1) в настройках подходов'));
102810
- 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');
102811
102779
  setGeneratingImages(false);
102812
102780
  return;
102813
102781
  }
102814
- // При "both" на каждый подход генерируем и 1:1, и 2:3
102815
- const useBoth = imageAspectRatio === 'both';
102816
- const tasks = useBoth
102817
- ? approaches.flatMap(a => [
102818
- { approach: a, ratio: '1:1' },
102819
- { approach: a, ratio: '2:3' }
102820
- ])
102821
- : approaches.map(a => ({ approach: a, ratio: imageAspectRatio }));
102822
- const additionalInfoLine = generateAdditionalInfo.trim()
102823
- ? `\n📋 ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ О ПРОДУКТЕ: ${generateAdditionalInfo.trim()}`
102824
- : '';
102825
- const imagePrompts = tasks.map(t => {
102826
- const basePromptStructure = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getImageGenerationBasePrompt)(generateGeo, generatePrice, currencyForPrompt, undefined, t.ratio);
102827
- const bulletsLine = t.approach.noBullets
102828
- ? t.approach.bulletsFocus
102829
- : (() => {
102830
- const [b1, b2, b3] = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.pickRandomBullets)(t.approach.name);
102831
- return `ОБЯЗАТЕЛЬНЫЕ БУЛЛЕТЫ (используй именно эти 3, в указанном порядке; переведи на язык ${generateGeo}): 1) ${b1} 2) ${b2} 3) ${b3}`;
102832
- })();
102833
- return `🏷️ ПРОДУКТ: ${generateProduct}${additionalInfoLine}\n\n${basePromptStructure}🏷️ ПРОДУКТ (повторение для ясности): ${generateProduct}${additionalInfoLine}\n🎯 ПОДХОД: ${t.approach.name}\n\n${t.approach.prompt}\n\n${t.approach.headlineAngle}\n\n${bulletsLine}\n\nТекст — строго следуй правилам этого подхода (HOOK обязателен; буллиты добавляй только если они разрешены для подхода).`;
102834
- });
102835
- // Initialize placeholders for all images
102836
- const initialPlaceholders = tasks.map((t, index) => ({
102837
- index: index + 1,
102838
- imageUrl: undefined,
102839
- approach: t.approach.name + (useBoth ? ` (${t.ratio})` : ''),
102840
- aspectRatio: t.ratio,
102841
- uploaded: false,
102842
- checking: false,
102843
- checkStatus: 'pending',
102844
- checkResult: undefined,
102845
- checkErrors: undefined,
102846
- originalPrompt: undefined,
102847
- productImageUrl: undefined,
102848
- regenerating: false,
102849
- customRegeneratePrompt: '',
102850
- failed: false,
102851
- generating: false // In sequential mode, images start queued (not generating)
102852
- }));
102853
- setGeneratedImagesData(initialPlaceholders);
102854
- addLog(formatLogMessage('log', `📝 Generated prompts for ${tasks.length} images${useBoth ? ' (1:1 + 2:3 на каждый подход)' : ''}`));
102855
- const maxParallel = 100; // like infinity
102856
- addLog(formatLogMessage('log', `🚀 Generating ${tasks.length} images in parallel (up to ${maxParallel} at a time)...`));
102857
- const generationStartTime = Date.now();
102858
- const resultsMap = new Map();
102859
- // Ensure each slot has prompt+product reference from the start (needed for regeneration even on failures)
102860
- setGeneratedImagesData(prev => prev.map((img, i) => ({
102861
- ...img,
102862
- originalPrompt: img.originalPrompt || imagePrompts[i],
102863
- productImageUrl: img.productImageUrl || productImage.url
102864
- })));
102865
- const runOne = async (i, isRetry) => {
102866
- const imageIndex = i + 1;
102867
- const task = tasks[i];
102868
- const approachName = task?.approach.name || 'Unknown';
102869
- const prompt = imagePrompts[i];
102870
- const ratio = task?.ratio || '1:1';
102871
- addLog(formatLogMessage('log', `${isRetry ? '🔄' : '🎨'} Генерация изображения ${imageIndex}/${tasks.length} (${approachName} ${ratio})...`));
102872
- // Reset slot state for this run
102873
- setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
102874
- ? {
102875
- ...img,
102876
- generating: true,
102877
- failed: false,
102878
- errorMessage: undefined,
102879
- imageUrl: undefined,
102880
- checking: false,
102881
- checkStatus: 'pending',
102882
- checkResult: undefined,
102883
- checkErrors: undefined,
102884
- originalPrompt: prompt,
102885
- productImageUrl: productImage.url
102886
- }
102887
- : img));
102888
- try {
102889
- const imageUrl = await generateImageWithDALLE(prompt, productImage.url, ratio);
102890
- if (!imageUrl || typeof imageUrl !== 'string' || imageUrl.trim() === '') {
102891
- throw new Error('Generated image URL is empty or invalid');
102892
- }
102893
- // 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
102894
102871
  setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
102895
102872
  ? {
102896
102873
  ...img,
102897
- imageUrl,
102898
- generating: false,
102874
+ generating: true,
102899
102875
  failed: false,
102900
- checking: true,
102901
- checkStatus: 'checking',
102876
+ errorMessage: undefined,
102877
+ imageUrl: undefined,
102878
+ checking: false,
102879
+ checkStatus: 'pending',
102880
+ checkResult: undefined,
102881
+ checkErrors: undefined,
102902
102882
  originalPrompt: prompt,
102903
102883
  productImageUrl: productImage.url
102904
102884
  }
102905
102885
  : img));
102906
- addLog(formatLogMessage('log', `✅ Изображение ${imageIndex}/${tasks.length} сгенерировано`));
102907
- // Validate right after generation (skip if validation disabled)
102908
- const validationResult = validationDisabled
102909
- ? { status: 'ok', result: 'Проверка отключена', errors: [], checkFailed: false }
102910
- : await (async () => {
102911
- try {
102912
- const res = await validateCreativeImage(imageUrl, generateProduct, generateGeo, addLog, approachName);
102913
- if (!res)
102914
- throw new Error('No validation result');
102915
- return { ...res, checkFailed: false };
102916
- }
102917
- catch (validationErr) {
102918
- const msg = validationErr?.message || String(validationErr);
102919
- addLog(formatLogMessage('error', `❌ Ошибка проверки изображения ${imageIndex}: ${msg}`));
102920
- return {
102921
- status: 'needs_rebuild',
102922
- result: `Ошибка проверки: ${msg}`,
102923
- errors: [msg],
102924
- checkFailed: true
102925
- };
102926
- }
102927
- })();
102928
- setGeneratedImagesData(prev => {
102929
- const updated = prev.map(img => img.index === imageIndex
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
102930
102893
  ? {
102931
102894
  ...img,
102932
- checking: false,
102933
- checkFailed: validationResult.checkFailed ?? false,
102934
- checkStatus: validationResult.status,
102935
- checkResult: validationResult.result,
102936
- checkErrors: validationResult.errors,
102937
- customRegeneratePrompt: validationResult.errors.length > 0
102938
- ? validationResult.errors.map(err => err.replace(/^ОШИБКА:\s*/i, '')).join('\n')
102939
- : '',
102940
- originalPrompt: img.originalPrompt || prompt,
102941
- 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
102942
102902
  }
102943
- : img);
102944
- return updated;
102945
- });
102946
- return {
102947
- index: imageIndex,
102948
- imageUrl,
102949
- success: true,
102950
- approach: approachName,
102951
- originalPrompt: prompt,
102952
- productImageUrl: productImage.url
102953
- };
102954
- }
102955
- catch (err) {
102956
- const errorMessage = err?.message || String(err);
102957
- addLog(formatLogMessage('error', `❌ Не удалось сгенерировать изображение ${imageIndex}/${tasks.length}: ${errorMessage}`));
102958
- setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
102959
- ? {
102960
- ...img,
102961
- generating: false,
102962
- failed: true,
102963
- errorMessage,
102964
- 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,
102965
102949
  originalPrompt: prompt,
102966
102950
  productImageUrl: productImage.url
102967
- }
102968
- : img));
102969
- return {
102970
- index: imageIndex,
102971
- imageUrl: null,
102972
- success: false,
102973
- error: err,
102974
- approach: approachName,
102975
- originalPrompt: prompt,
102976
- productImageUrl: productImage.url
102977
- };
102978
- }
102979
- finally {
102980
- // Small yield to keep UI responsive in Electron
102981
- await new Promise(resolve => setTimeout(resolve, 150));
102982
- }
102983
- };
102984
- // First pass (limited concurrency)
102985
- let cursor = 0;
102986
- const workerCount = Math.min(maxParallel, imagePrompts.length);
102987
- const workers = Array.from({ length: workerCount }, () => (async () => {
102988
- while (true) {
102989
- const i = cursor++;
102990
- if (i >= imagePrompts.length)
102991
- break;
102992
- const res = await runOne(i, false);
102993
- resultsMap.set(res.index, res);
102994
- }
102995
- })());
102996
- await Promise.all(workers);
102997
- let generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
102998
- // Safety: fill any missing slots as failed
102999
- if (generationResults.length !== imagePrompts.length) {
103000
- for (let i = 0; i < imagePrompts.length; i++) {
103001
- const idx = i + 1;
103002
- if (!resultsMap.has(idx)) {
103003
- generationResults.push({
103004
- 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,
103005
102969
  imageUrl: null,
103006
102970
  success: false,
103007
- error: new Error('Generation did not complete'),
103008
- approach: approaches[i]?.name || 'Unknown',
103009
- originalPrompt: imagePrompts[i],
102971
+ error: err,
102972
+ approach: approachName,
102973
+ originalPrompt: prompt,
103010
102974
  productImageUrl: productImage.url
103011
- });
102975
+ };
103012
102976
  }
103013
- }
103014
- generationResults = generationResults.sort((a, b) => a.index - b.index);
103015
- }
103016
- // Retry once for all transient failures including refusals (model sometimes randomly refuses)
103017
- const retryableErrors = generationResults.filter(r => {
103018
- if (r.success)
103019
- return false;
103020
- const errorMsg = r.error?.message || '';
103021
- const isRefusal = r.error?.isRefusal === true;
103022
- // isRefusal и hasReasoning — случайные отказы модели → ретраим с логом
103023
- // Всё остальное (сеть/JSON/таймаут/Provider) тоже ретраим
103024
- return (isRefusal ||
103025
- r.error?.hasReasoning === true ||
103026
- errorMsg.includes('Invalid JSON response') ||
103027
- errorMsg.includes('Unexpected end of JSON input') ||
103028
- (errorMsg.includes('JSON') && errorMsg.includes('parse')) ||
103029
- errorMsg.includes('Failed to fetch') ||
103030
- errorMsg.includes('Network error') ||
103031
- errorMsg.includes('timeout') ||
103032
- errorMsg.includes('Request timeout') ||
103033
- errorMsg.includes('Provider returned error') ||
103034
- errorMsg.toLowerCase().includes('provider error'));
103035
- });
103036
- if (retryableErrors.length > 0) {
103037
- const refusalCount = retryableErrors.filter(r => r.error?.isRefusal === true).length;
103038
- const otherCount = retryableErrors.length - refusalCount;
103039
- const parts = [];
103040
- if (refusalCount > 0)
103041
- parts.push(`${refusalCount} отказ${refusalCount > 1 ? 'а' : ''} модели`);
103042
- if (otherCount > 0)
103043
- parts.push(`${otherCount} сеть/таймаут/JSON`);
103044
- addLog(formatLogMessage('log', `🔄 Повтор: ${parts.join(', ')}. Пробую ещё раз...`));
103045
- const retryIndices = retryableErrors.map(r => r.index).sort((a, b) => a - b);
103046
- let retryCursor = 0;
103047
- 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 () => {
103048
102986
  while (true) {
103049
- const idx = retryIndices[retryCursor++];
103050
- if (idx === undefined)
102987
+ const i = cursor++;
102988
+ if (i >= imagePrompts.length)
103051
102989
  break;
103052
- // backoff
103053
- await new Promise(resolve => setTimeout(resolve, 1200));
103054
- const i = idx - 1;
103055
- const res = await runOne(i, true);
102990
+ const res = await runOne(i, false);
103056
102991
  resultsMap.set(res.index, res);
103057
102992
  }
103058
102993
  })());
103059
- await Promise.all(retryWorkers);
103060
- generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
103061
- }
103062
- const generationDuration = Date.now() - generationStartTime;
103063
- const successfulImages = generationResults.filter(r => r.success);
103064
- const finalFailedImages = generationResults.filter(r => !r.success);
103065
- addLog(formatLogMessage('log', `⏱️ Генерация завершена за ${Math.round(generationDuration / 1000)}s`));
103066
- addLog(formatLogMessage('log', `✅ Успешно: ${successfulImages.length}/${imagePrompts.length}, ошибок: ${finalFailedImages.length}`));
103067
- if (successfulImages.length === 0) {
103068
- const allErrors = finalFailedImages.map(f => f.error?.message || String(f.error || 'Unknown error')).join('; ');
103069
- addLog(formatLogMessage('warn', `⚠️ No images were successfully generated. All ${imagePrompts.length} slots are available for regeneration.`));
103070
- // Don't throw error - just show empty slots with regenerate buttons
103071
- }
103072
- const overallDuration = Date.now() - overallStartTime;
103073
- addLog(formatLogMessage('log', `🎉 === Image generation process completed ===`));
103074
- addLog(formatLogMessage('log', `⏱️ Total time: ${overallDuration}ms (${(overallDuration / 1000).toFixed(2)}s)`));
103075
- addLog(formatLogMessage('log', `✅ Successfully generated ${successfulImages.length}/${imagePrompts.length} images`));
103076
- // Stop generating state to hide backdrop and show images
103077
- setGeneratingImages(false);
103078
- setCheckingImages(false);
103079
- // Save generated images to Google Drive
103080
- if (driveFolderUrl) {
103081
- try {
103082
- const folderId = extractFolderId(driveFolderUrl);
103083
- if (folderId) {
103084
- addLog(formatLogMessage('log', '💾 Saving generated images data to Google Drive...'));
103085
- // Get latest state and save
103086
- setGeneratedImagesData(prev => {
103087
- // Save all images to separate files
103088
- saveGeneratedImagesToDrive(folderId, prev).then(() => {
103089
- addLog(formatLogMessage('log', '✅ Generated images data saved to Google Drive'));
103090
- }).catch((err) => {
103091
- 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
103092
103009
  });
103093
- return prev;
103094
- });
103010
+ }
103095
103011
  }
103012
+ generationResults = generationResults.sort((a, b) => a.index - b.index);
103096
103013
  }
103097
- catch (err) {
103098
- 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
103099
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);
103100
103087
  }
103101
- // Validation is done individually per image; no final summary block
103102
103088
  }
103103
103089
  catch (err) {
103104
- const errorMessage = err.message || 'Unknown error';
103105
- addLog(formatLogMessage('error', '❌ === Error in image generation process ==='));
103106
- addLog(formatLogMessage('error', errorMessage));
103107
- alert('Error generating images: ' + errorMessage);
103108
- }
103109
- finally {
103110
103090
  setGeneratingImages(false);
103091
+ logToTerminal('error', '❌ Error:', err?.message || err);
103111
103092
  }
103112
103093
  };
103113
103094
  const handleRegenerateImage = async (imageData) => {
@@ -103517,23 +103498,8 @@ ${imageData.originalPrompt}
103517
103498
  addLog(formatLogMessage('log', `📤 Uploading image ${imageData.index} to Drive: ${filename}`));
103518
103499
  const driveUrl = await uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog);
103519
103500
  addLog(formatLogMessage('log', `✅ Image ${imageData.index} uploaded: ${driveUrl}`));
103520
- // Update uploaded status and save single image to Drive
103521
- setGeneratedImagesData(prev => {
103522
- const updated = prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img);
103523
- // Save single image to its own file
103524
- if (driveFolderUrl) {
103525
- const currentFolderId = extractFolderId(driveFolderUrl);
103526
- if (currentFolderId) {
103527
- const updatedImage = updated.find(img => img.index === imageData.index);
103528
- if (updatedImage) {
103529
- saveSingleImageToDrive(currentFolderId, updatedImage).catch((err) => {
103530
- console.log(`Failed to save image ${imageData.index} to Drive:`, err);
103531
- });
103532
- }
103533
- }
103534
- }
103535
- return updated;
103536
- });
103501
+ // Update uploaded status
103502
+ setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img));
103537
103503
  // (Removed legacy Generated Images links list)
103538
103504
  }
103539
103505
  catch (err) {
@@ -103556,7 +103522,7 @@ ${imageData.originalPrompt}
103556
103522
  alert('Invalid Google Drive Folder URL');
103557
103523
  return;
103558
103524
  }
103559
- const notUploaded = generatedImagesData.filter(img => !img.uploaded);
103525
+ const notUploaded = generatedImagesData.filter(img => !img.uploaded && img.imageUrl);
103560
103526
  if (notUploaded.length === 0) {
103561
103527
  alert('All images are already uploaded');
103562
103528
  return;
@@ -103566,57 +103532,53 @@ ${imageData.originalPrompt}
103566
103532
  setImagesGenerationLogs(prev => [...prev, msg]);
103567
103533
  logToTerminal('log', msg.replace(/\[.*?\]\s*/, ''));
103568
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));
103569
103539
  try {
103570
- addLog(formatLogMessage('log', `📤 Uploading ${notUploaded.length} image(s) to Drive...`));
103571
- let uploadedCount = 0;
103572
- for (const imageData of notUploaded) {
103573
- if (!imageData.imageUrl) {
103574
- addLog(formatLogMessage('warn', `⚠️ Skipping image ${imageData.index} - no image URL`));
103575
- 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);
103576
103557
  }
103577
- try {
103578
- const filename = `${generateProduct.replace(/\s+/g, '_')}_${imageData.index}_${Date.now()}.png`;
103579
- addLog(formatLogMessage('log', `📤 Uploading image ${imageData.index} to Drive: ${filename}`));
103580
- const driveUrl = await uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog);
103581
- addLog(formatLogMessage('log', `✅ Image ${imageData.index} uploaded: ${driveUrl}`));
103582
- // Update uploaded status
103583
- setGeneratedImagesData(prev => {
103584
- const updated = prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img);
103585
- // Save to Google Drive after upload
103586
- saveGeneratedImagesToDrive(folderId, updated).catch((err) => {
103587
- console.log('Failed to save images data after upload:', err);
103588
- });
103589
- return updated;
103590
- });
103591
- // (Removed legacy Generated Images links list)
103592
- 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));
103593
103561
  }
103594
- catch (err) {
103595
- 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
103596
103571
  }
103597
- }
103598
- addLog(formatLogMessage('log', `✅ Uploaded ${uploadedCount} image(s) successfully`));
103599
- // Save images data to Google Drive after all uploads complete
103600
- try {
103601
- addLog(formatLogMessage('log', '💾 Saving images data to Google Drive...'));
103602
- // Get latest state and save all images
103603
- setGeneratedImagesData(prev => {
103604
- saveGeneratedImagesToDrive(folderId, prev).then(() => {
103605
- addLog(formatLogMessage('log', '✅ Images data saved to Google Drive'));
103606
- }).catch((err) => {
103607
- addLog(formatLogMessage('warn', `⚠️ Failed to save images data to Google Drive: ${err.message || 'Unknown error'}`));
103608
- });
103609
- return prev;
103610
- });
103611
- }
103612
- catch (err) {
103613
- addLog(formatLogMessage('warn', `⚠️ Failed to save images data to Google Drive: ${err.message || 'Unknown error'}`));
103614
- }
103615
- 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!`);
103616
103575
  }
103617
103576
  catch (err) {
103618
103577
  addLog(formatLogMessage('error', `❌ Error uploading images:`, err.message));
103619
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));
103620
103582
  }
103621
103583
  finally {
103622
103584
  setUploadingImages(false);
@@ -103858,11 +103820,9 @@ ${imageData.originalPrompt}
103858
103820
  errorMessage: item.errorMessage
103859
103821
  }));
103860
103822
  };
103861
- // Save generated content to Google Drive
103862
- const saveGeneratedContentToDrive = async (folderId, titlesData, textsData, aiSettings, brandValue, linkValue) => {
103863
- const sanitizedTitlesData = sanitizeGeneratedTitlesData(titlesData);
103864
- const sanitizedTextsData = sanitizeGeneratedTextsData(textsData);
103865
- 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);
103866
103826
  const validToken = await getValidAccessToken();
103867
103827
  if (!validToken) {
103868
103828
  logToTerminal('error', '[Save Content] No valid token');
@@ -103871,8 +103831,6 @@ ${imageData.originalPrompt}
103871
103831
  // Get or create 'env' folder
103872
103832
  const envFolderId = await getOrCreateEnvFolder(folderId);
103873
103833
  const dataToSave = {
103874
- generatedTitlesData: sanitizedTitlesData,
103875
- generatedTextsData: sanitizedTextsData,
103876
103834
  aiGenerationSettings: aiSettings || {
103877
103835
  generateProduct: generateProduct || '',
103878
103836
  generateGeo: generateGeo || '',
@@ -104025,38 +103983,8 @@ ${imageData.originalPrompt}
104025
103983
  logToTerminal('log', '[Load Content] File downloaded, length:', jsonContent.length);
104026
103984
  try {
104027
103985
  const loadedData = JSON.parse(jsonContent);
104028
- logToTerminal('log', '[Load Content] Parsed data, titles:', loadedData.generatedTitlesData?.length || 0, 'texts:', loadedData.generatedTextsData?.length || 0);
104029
- if (loadedData.generatedTitlesData && Array.isArray(loadedData.generatedTitlesData)) {
104030
- const sanitizedTitlesData = sanitizeGeneratedTitlesData(loadedData.generatedTitlesData);
104031
- logToTerminal('log', '[Load Content] Loading titles, count:', sanitizedTitlesData.length);
104032
- setGeneratedTitlesData(sanitizedTitlesData);
104033
- // Also update titles field
104034
- const titlesString = sanitizedTitlesData
104035
- .map((t) => t.title)
104036
- .filter((t) => t)
104037
- .join('\n');
104038
- if (titlesString) {
104039
- setTitles(titlesString);
104040
- }
104041
- }
104042
- else {
104043
- setGeneratedTitlesData([]);
104044
- }
104045
- if (loadedData.generatedTextsData && Array.isArray(loadedData.generatedTextsData)) {
104046
- const sanitizedTextsData = sanitizeGeneratedTextsData(loadedData.generatedTextsData);
104047
- logToTerminal('log', '[Load Content] Loading texts, count:', sanitizedTextsData.length);
104048
- setGeneratedTextsData(sanitizedTextsData);
104049
- // Also update texts field
104050
- const textsArray = sanitizedTextsData
104051
- .map((t) => t.text)
104052
- .filter((t) => t);
104053
- if (textsArray.length > 0) {
104054
- setTexts(textsArray);
104055
- }
104056
- }
104057
- else {
104058
- setGeneratedTextsData([]);
104059
- }
103986
+ logToTerminal('log', '[Load Content] Parsed settings');
103987
+ // Titles and texts are not loaded from Drive (no longer saved there)
104060
103988
  // Load AI Generation Settings
104061
103989
  if (loadedData.aiGenerationSettings) {
104062
103990
  logToTerminal('log', '[Load Content] Loading AI Generation Settings');
@@ -104093,250 +104021,7 @@ ${imageData.originalPrompt}
104093
104021
  return { found: false };
104094
104022
  }
104095
104023
  };
104096
- // Save single image to Google Drive as separate file
104097
- const saveSingleImageToDrive = async (folderId, imageData) => {
104098
- logToTerminal('log', '[Save Image] Saving image', imageData.index, 'to folderId:', folderId);
104099
- const validToken = await getValidAccessToken();
104100
- if (!validToken) {
104101
- logToTerminal('error', '[Save Image] No valid token');
104102
- throw new Error('Not logged in to Google Drive');
104103
- }
104104
- // Get or create 'env' folder
104105
- const envFolderId = await getOrCreateEnvFolder(folderId);
104106
- const jsonContent = JSON.stringify(imageData, null, 2);
104107
- const filename = `temp-image-${imageData.index}.json`;
104108
- logToTerminal('log', '[Save Image] Filename:', filename, 'content length:', jsonContent.length);
104109
- // Check if file already exists in 'env' folder
104110
- const q = `'${envFolderId}' in parents and trashed = false and name = '${filename}'`;
104111
- const fields = 'files(id, name)';
104112
- const searchUrl = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent(fields)}`;
104113
- const searchResponse = await fetch(searchUrl, {
104114
- headers: {
104115
- 'Authorization': `Bearer ${validToken}`
104116
- }
104117
- });
104118
- let fileId = null;
104119
- if (searchResponse.ok) {
104120
- const searchData = await searchResponse.json();
104121
- logToTerminal('log', '[Save Image] Search response, files found:', searchData.files?.length || 0);
104122
- if (searchData.files && searchData.files.length > 0) {
104123
- // Filter out duplicate files (with (1), (2), etc.) - prefer exact match
104124
- const exactMatch = searchData.files.find((f) => f.name === filename);
104125
- if (exactMatch) {
104126
- fileId = exactMatch.id;
104127
- logToTerminal('log', '[Save Image] Found exact match, will update, fileId:', fileId);
104128
- }
104129
- else {
104130
- // If no exact match, use first file but log warning
104131
- fileId = searchData.files[0].id;
104132
- logToTerminal('warn', '[Save Image] Found duplicate file, will update first one, fileId:', fileId, 'name:', searchData.files[0].name);
104133
- }
104134
- }
104135
- else {
104136
- logToTerminal('log', '[Save Image] File does not exist, will create new');
104137
- }
104138
- }
104139
- else {
104140
- logToTerminal('warn', '[Save Image] Search failed, status:', searchResponse.status);
104141
- }
104142
- if (fileId) {
104143
- // Update existing file - don't include parents in metadata for updates
104144
- logToTerminal('log', '[Save Image] Updating existing file');
104145
- const updateMetadata = {
104146
- name: filename,
104147
- mimeType: 'application/json'
104148
- };
104149
- const updateForm = new FormData();
104150
- updateForm.append('metadata', new Blob([JSON.stringify(updateMetadata)], { type: 'application/json' }));
104151
- updateForm.append('file', new Blob([jsonContent], { type: 'application/json' }));
104152
- const updateUrl = `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`;
104153
- const updateResponse = await fetch(updateUrl, {
104154
- method: 'PATCH',
104155
- headers: {
104156
- 'Authorization': `Bearer ${validToken}`
104157
- },
104158
- body: updateForm
104159
- });
104160
- if (!updateResponse.ok) {
104161
- const err = await updateResponse.json();
104162
- logToTerminal('error', '[Save Image] Update failed:', JSON.stringify(err));
104163
- throw new Error(err.error?.message || 'Failed to update file in Google Drive');
104164
- }
104165
- logToTerminal('log', '[Save Image] Successfully updated file');
104166
- }
104167
- else {
104168
- // Create new file - include parents for new files (in 'env' folder)
104169
- logToTerminal('log', '[Save Image] Creating new file in env folder');
104170
- const createMetadata = {
104171
- name: filename,
104172
- mimeType: 'application/json',
104173
- parents: [envFolderId]
104174
- };
104175
- const createForm = new FormData();
104176
- createForm.append('metadata', new Blob([JSON.stringify(createMetadata)], { type: 'application/json' }));
104177
- createForm.append('file', new Blob([jsonContent], { type: 'application/json' }));
104178
- const createUrl = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart';
104179
- const createResponse = await fetch(createUrl, {
104180
- method: 'POST',
104181
- headers: {
104182
- 'Authorization': `Bearer ${validToken}`
104183
- },
104184
- body: createForm
104185
- });
104186
- if (!createResponse.ok) {
104187
- const err = await createResponse.json();
104188
- logToTerminal('error', '[Save Image] Create failed:', JSON.stringify(err));
104189
- throw new Error(err.error?.message || 'Failed to create file in Google Drive');
104190
- }
104191
- const result = await createResponse.json();
104192
- logToTerminal('log', '[Save Image] Successfully created file, id:', result.id);
104193
- }
104194
- };
104195
- // Save all images to Google Drive (each in separate file)
104196
- const saveGeneratedImagesToDrive = async (folderId, imagesData) => {
104197
- const validToken = await getValidAccessToken();
104198
- if (!validToken) {
104199
- throw new Error('Not logged in to Google Drive');
104200
- }
104201
- // Save each image to its own file
104202
- for (const imageData of imagesData) {
104203
- try {
104204
- await saveSingleImageToDrive(folderId, imageData);
104205
- }
104206
- catch (err) {
104207
- console.error(`Failed to save image ${imageData.index} to Drive:`, err);
104208
- // Continue with other images even if one fails
104209
- }
104210
- }
104211
- };
104212
- // Load generated images data from Google Drive (from separate files)
104213
- const loadGeneratedImagesFromDrive = async (folderId) => {
104214
- logToTerminal('log', '[Load Images] Starting load from Google Drive, folderId:', folderId);
104215
- const validToken = await getValidAccessToken();
104216
- if (!validToken) {
104217
- logToTerminal('warn', '[Load Images] No valid token, skipping load');
104218
- setDriveFilesFound(prev => ({ ...prev, images: false }));
104219
- return { found: false };
104220
- }
104221
- // Get 'env' folder
104222
- let envFolderId;
104223
- try {
104224
- envFolderId = await getOrCreateEnvFolder(folderId);
104225
- }
104226
- catch (err) {
104227
- logToTerminal('warn', '[Load Images] Failed to get env folder:', err.message || 'Unknown error');
104228
- return { found: false };
104229
- }
104230
- // Find all temp-image-*.json files in 'env' folder
104231
- const q = `'${envFolderId}' in parents and trashed = false and name contains 'temp-image-'`;
104232
- const fields = 'files(id, name)';
104233
- const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent(fields)}`;
104234
- logToTerminal('log', '[Load Images] Searching for image files');
104235
- const response = await fetch(url, {
104236
- headers: {
104237
- 'Authorization': `Bearer ${validToken}`
104238
- }
104239
- });
104240
- if (!response.ok) {
104241
- const errorText = await response.text();
104242
- logToTerminal('warn', '[Load Images] Search failed, status:', response.status, 'error:', errorText);
104243
- setDriveFilesFound(prev => ({ ...prev, images: false }));
104244
- return { found: false };
104245
- }
104246
- const data = await response.json();
104247
- logToTerminal('log', '[Load Images] Search response, files found:', data.files?.length || 0);
104248
- if (!data.files || data.files.length === 0) {
104249
- logToTerminal('log', '[Load Images] No image files found - no temp-image-*.json files in folder');
104250
- return { found: false };
104251
- }
104252
- // Filter out duplicate files (with (1), (2), etc.) - only keep exact matches
104253
- const exactMatchFiles = data.files.filter((f) => {
104254
- const match = f.name.match(/^temp-image-(\d+)\.json$/);
104255
- return match !== null;
104256
- });
104257
- logToTerminal('log', '[Load Images] Found', exactMatchFiles.length, 'exact match files:', exactMatchFiles.map((f) => f.name).join(', '));
104258
- // Group files by index and take only one file per index (prefer exact match, then first one)
104259
- const filesByIndex = new Map();
104260
- for (const file of exactMatchFiles) {
104261
- const match = file.name.match(/^temp-image-(\d+)\.json$/);
104262
- if (match) {
104263
- const index = parseInt(match[1], 10);
104264
- if (!filesByIndex.has(index)) {
104265
- filesByIndex.set(index, file);
104266
- }
104267
- }
104268
- }
104269
- const uniqueFiles = Array.from(filesByIndex.values());
104270
- logToTerminal('log', '[Load Images] After deduplication:', uniqueFiles.length, 'unique files, indices:', Array.from(filesByIndex.keys()).sort((a, b) => a - b).join(', '));
104271
- // Helper function to download file with retries
104272
- const downloadFileWithRetry = async (file, maxRetries = 3, delayMs = 1000) => {
104273
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
104274
- try {
104275
- if (attempt > 1) {
104276
- logToTerminal('log', `[Load Images] Retry ${attempt}/${maxRetries} for file:`, file.name);
104277
- await new Promise(resolve => setTimeout(resolve, delayMs * (attempt - 1))); // Exponential backoff
104278
- }
104279
- else {
104280
- logToTerminal('log', '[Load Images] Loading file:', file.name);
104281
- }
104282
- const downloadUrl = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`;
104283
- const downloadResponse = await fetch(downloadUrl, {
104284
- headers: {
104285
- 'Authorization': `Bearer ${validToken}`
104286
- }
104287
- });
104288
- if (downloadResponse.ok) {
104289
- const jsonContent = await downloadResponse.text();
104290
- logToTerminal('log', '[Load Images] File', file.name, 'downloaded, length:', jsonContent.length);
104291
- const imageData = JSON.parse(jsonContent);
104292
- logToTerminal('log', '[Load Images] Parsed image data for index:', imageData.index);
104293
- return imageData;
104294
- }
104295
- else {
104296
- const errorText = await downloadResponse.text();
104297
- logToTerminal('warn', `[Load Images] Download failed for ${file.name} (attempt ${attempt}/${maxRetries}), status:`, downloadResponse.status);
104298
- if (attempt === maxRetries) {
104299
- logToTerminal('error', `[Load Images] Failed to load ${file.name} after ${maxRetries} attempts`);
104300
- return null;
104301
- }
104302
- }
104303
- }
104304
- catch (err) {
104305
- logToTerminal('error', `[Load Images] Failed to load image file ${file.name} (attempt ${attempt}/${maxRetries}):`, err);
104306
- if (attempt === maxRetries) {
104307
- logToTerminal('error', `[Load Images] Failed to load ${file.name} after ${maxRetries} attempts`);
104308
- return null;
104309
- }
104310
- }
104311
- }
104312
- return null;
104313
- };
104314
- // Load all image files in parallel
104315
- logToTerminal('log', '[Load Images] Starting parallel download of', uniqueFiles.length, 'files');
104316
- const downloadPromises = uniqueFiles.map(file => downloadFileWithRetry(file));
104317
- // Wait for all downloads to complete
104318
- const results = await Promise.all(downloadPromises);
104319
- const imagesData = results.filter((data) => data !== null);
104320
- // Sort by index and update state
104321
- if (imagesData.length > 0) {
104322
- imagesData.sort((a, b) => a.index - b.index);
104323
- logToTerminal('log', '[Load Images] Successfully loaded', imagesData.length, 'images, indices:', imagesData.map(img => img.index).join(', '));
104324
- setGeneratedImagesData(imagesData);
104325
- setDriveFilesFound(prev => ({ ...prev, images: false }));
104326
- return { found: true };
104327
- }
104328
- else {
104329
- logToTerminal('log', '[Load Images] No images loaded');
104330
- setDriveFilesFound(prev => ({ ...prev, images: false }));
104331
- return { found: false };
104332
- }
104333
- };
104334
104024
  const handleGenerate = async () => {
104335
- const validToken = await getValidAccessToken();
104336
- if (!validToken) {
104337
- alert('Please log in with Google first');
104338
- return;
104339
- }
104340
104025
  if (!driveFolderUrl || !brand || !link) {
104341
104026
  alert('Please fill all required fields');
104342
104027
  return;
@@ -104348,18 +104033,28 @@ ${imageData.originalPrompt}
104348
104033
  alert('Invalid Link URL');
104349
104034
  return;
104350
104035
  }
104351
- 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);
104352
104039
  if (titleList.length === 0) {
104353
104040
  alert('Please add at least one title');
104354
104041
  return;
104355
104042
  }
104356
- 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);
104357
104046
  if (textList.length === 0) {
104358
104047
  alert('Please add at least one text variant');
104359
104048
  return;
104360
104049
  }
104361
104050
  setLoading(true);
104362
104051
  try {
104052
+ const validToken = await getValidAccessToken();
104053
+ if (!validToken) {
104054
+ alert('Please log in with Google first');
104055
+ setLoading(false);
104056
+ return;
104057
+ }
104363
104058
  const folderId = extractFolderId(driveFolderUrl);
104364
104059
  if (!folderId) {
104365
104060
  throw new Error('Invalid Google Drive Folder URL');
@@ -104686,7 +104381,24 @@ ${imageData.originalPrompt}
104686
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"))))),
104687
104382
  !driveFolderUrl.trim() ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "info", sx: { mt: 2 } },
104688
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"),
104689
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2" }, "\u0414\u043B\u044F \u0440\u0430\u0431\u043E\u0442\u044B \u0441 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0435\u0439 \u043A\u043E\u043D\u0442\u0435\u043D\u0442\u0430 \u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u0430\u043F\u043A\u0443 Google Drive. \u0412\u0441\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C\u0441\u044F \u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044C\u0441\u044F \u0438\u0437 \u044D\u0442\u043E\u0439 \u043F\u0430\u043F\u043A\u0438."))) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
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,
104690
104402
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: { xs: 'column', sm: 'row' }, spacing: 2 },
104691
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" }),
104692
104404
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { flex: 1, minWidth: 0 } },
@@ -104749,7 +104461,6 @@ ${imageData.originalPrompt}
104749
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"),
104750
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")))),
104751
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 ||
104752
- loadingImagesFromDrive ||
104753
104464
  !openaiApiKey ||
104754
104465
  (!accessToken && !refreshToken) ||
104755
104466
  !generateProduct.trim() ||
@@ -104767,6 +104478,18 @@ ${imageData.originalPrompt}
104767
104478
  : !driveFolderUrl.trim()
104768
104479
  ? 'Заполните URL папки Google Drive'
104769
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!"))))),
104770
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\".")),
104771
104494
  !generatingImages && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
104772
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")),
@@ -104784,7 +104507,7 @@ ${imageData.originalPrompt}
104784
104507
  (folderFilesInfo !== null && !folderFilesInfo.hasProduct), sx: { flexGrow: 1 }, size: "large", title: folderFilesInfo !== null && !folderFilesInfo.hasProduct
104785
104508
  ? 'product.png/jpg не найден в папке. Загрузите его с помощью кнопки «Загрузить product.png/jpg»'
104786
104509
  : undefined }, generatingLanding ? 'Creating Landing...' : 'Создать лендинг')),
104787
- (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 } },
104788
104511
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } },
104789
104512
  "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u043A\u0440\u0435\u043E (",
104790
104513
  generatedImagesData.filter(img => img.imageUrl).length,
@@ -104794,22 +104517,6 @@ ${imageData.originalPrompt}
104794
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 } },
104795
104518
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }),
104796
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..."))),
104797
- loadingImagesFromDrive && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
104798
- display: 'flex',
104799
- flexDirection: 'column',
104800
- alignItems: 'center',
104801
- justifyContent: 'center',
104802
- gap: 2,
104803
- p: 4,
104804
- border: (theme) => `2px dashed ${theme.palette.primary.main}`,
104805
- borderRadius: 1,
104806
- backgroundColor: (theme) => theme.palette.mode === 'dark'
104807
- ? 'rgba(25, 118, 210, 0.1)'
104808
- : 'rgba(25, 118, 210, 0.05)',
104809
- minHeight: 200
104810
- } },
104811
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 40, sx: { color: 'primary.main' } }),
104812
- 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..."))),
104813
104520
  generatedImagesData.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
104814
104521
  display: 'grid',
104815
104522
  gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
@@ -105118,20 +104825,7 @@ ${imageData.originalPrompt}
105118
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 } },
105119
104826
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }),
105120
104827
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'text.secondary' } }, formatElapsedTime(elapsedTime.landing)))),
105121
- !generatingLanding && generatedLandingHTML && generatedLandingImageBlob && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "primary", onClick: handlePreviewLanding, startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_47__["default"], null), fullWidth: true }, "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043B\u0435\u043D\u0434\u0438\u043D\u0433\u0430")))),
105122
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2 },
105123
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", size: "large", onClick: handleGenerate, disabled: loading || !accessToken, startIcon: loading ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null), sx: { py: 1.5, fontSize: '1.1rem', flexGrow: 1 } }, loading ? 'Generating...' : 'Generate Table')),
105124
- uploadedLink && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "success", sx: { mt: 2 }, action: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { "aria-label": "copy link", color: "inherit", size: "small", onClick: handleCopyLink, sx: { ml: 1 } },
105125
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_39__["default"], { fontSize: "small" })) },
105126
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
105127
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
105128
- "File Uploaded!",
105129
- ' ',
105130
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { href: "#", onClick: (e) => {
105131
- e.preventDefault();
105132
- getElectronAPI().openExternal(uploadedLink);
105133
- }, style: { cursor: 'pointer', textDecoration: 'underline', color: 'inherit' } }, "Click here to open in Google Drive")),
105134
- linkCopied && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'success.dark', fontWeight: 'bold' } }, "Copied!")))))))))),
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"))))))))),
105135
104829
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_PromptManagerDialog__WEBPACK_IMPORTED_MODULE_48__["default"], { open: promptManagerOpen, onClose: () => setPromptManagerOpen(false) }))));
105136
104830
  }
105137
104831
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (App);