docs-combiner 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/renderer.js +826 -1473
- package/dist/renderer.js.map +1 -1
- package/package.json +1 -1
package/dist/renderer.js
CHANGED
|
@@ -99622,33 +99622,28 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
99622
99622
|
/* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButtonGroup/ToggleButtonGroup.js");
|
|
99623
99623
|
/* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButton/ToggleButton.js");
|
|
99624
99624
|
/* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Paper/Paper.js");
|
|
99625
|
-
/* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/
|
|
99626
|
-
/* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/
|
|
99627
|
-
/* harmony import */ var
|
|
99628
|
-
/* harmony import */ var
|
|
99629
|
-
/* harmony import */ var
|
|
99630
|
-
/* harmony import */ var
|
|
99631
|
-
/* harmony import */ var
|
|
99632
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99633
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99634
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99635
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99636
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99637
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99638
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99639
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99640
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99641
|
-
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/
|
|
99642
|
-
/* harmony import */ var
|
|
99643
|
-
/* harmony import */ var
|
|
99644
|
-
/* harmony import */ var
|
|
99645
|
-
/* harmony import */ var
|
|
99646
|
-
/* harmony import */ var
|
|
99647
|
-
/* harmony import */ var _PromptManagerDialog__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(/*! ./PromptManagerDialog */ "./src/PromptManagerDialog.tsx");
|
|
99648
|
-
/* harmony import */ var _promptOverrides__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(/*! ./promptOverrides */ "./src/promptOverrides.ts");
|
|
99649
|
-
/* harmony import */ var xlsx__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(/*! xlsx */ "./node_modules/xlsx/xlsx.mjs");
|
|
99650
|
-
/* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(/*! jszip */ "./node_modules/jszip/dist/jszip.min.js");
|
|
99651
|
-
/* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_56___default = /*#__PURE__*/__webpack_require__.n(jszip__WEBPACK_IMPORTED_MODULE_56__);
|
|
99625
|
+
/* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/styles/createTheme.js");
|
|
99626
|
+
/* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/styles/ThemeProvider.js");
|
|
99627
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/AccountBalance.js");
|
|
99628
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/AutoAwesome.js");
|
|
99629
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Brightness4.js");
|
|
99630
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Brightness7.js");
|
|
99631
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/CheckCircle.js");
|
|
99632
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/CloudDownload.js");
|
|
99633
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/ContentCopy.js");
|
|
99634
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/ExpandMore.js");
|
|
99635
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/InfoOutlined.js");
|
|
99636
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Login.js");
|
|
99637
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Logout.js");
|
|
99638
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/NoteAdd.js");
|
|
99639
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Replay.js");
|
|
99640
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Settings.js");
|
|
99641
|
+
/* harmony import */ var _mui_icons_material__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(/*! @mui/icons-material */ "./node_modules/@mui/icons-material/esm/Visibility.js");
|
|
99642
|
+
/* harmony import */ var _PromptManagerDialog__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(/*! ./PromptManagerDialog */ "./src/PromptManagerDialog.tsx");
|
|
99643
|
+
/* harmony import */ var _promptOverrides__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(/*! ./promptOverrides */ "./src/promptOverrides.ts");
|
|
99644
|
+
/* harmony import */ var xlsx__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(/*! xlsx */ "./node_modules/xlsx/xlsx.mjs");
|
|
99645
|
+
/* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(/*! jszip */ "./node_modules/jszip/dist/jszip.min.js");
|
|
99646
|
+
/* harmony import */ var jszip__WEBPACK_IMPORTED_MODULE_51___default = /*#__PURE__*/__webpack_require__.n(jszip__WEBPACK_IMPORTED_MODULE_51__);
|
|
99652
99647
|
|
|
99653
99648
|
|
|
99654
99649
|
|
|
@@ -99724,7 +99719,6 @@ function App() {
|
|
|
99724
99719
|
const [pairTranslations, setPairTranslations] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({});
|
|
99725
99720
|
const [translatingPairs, setTranslatingPairs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99726
99721
|
const [driveFolderUrl, setDriveFolderUrl] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
|
|
99727
|
-
const [brand, setBrand] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
|
|
99728
99722
|
const [link, setLink] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
|
|
99729
99723
|
const [openaiApiKey, setOpenaiApiKey] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
|
|
99730
99724
|
const [openRouterBalance, setOpenRouterBalance] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
@@ -99876,28 +99870,66 @@ function App() {
|
|
|
99876
99870
|
const { price, currency } = parsePriceAndCurrency(value);
|
|
99877
99871
|
return price.trim() !== '' && currency.trim() !== '';
|
|
99878
99872
|
};
|
|
99873
|
+
// Transliterate to Latin and slugify (lowercase, dashes)
|
|
99874
|
+
const transliterateToSlug = (text) => {
|
|
99875
|
+
const cyrillicMap = {
|
|
99876
|
+
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'e', 'ж': 'zh', 'з': 'z',
|
|
99877
|
+
'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r',
|
|
99878
|
+
'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'shch',
|
|
99879
|
+
'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya',
|
|
99880
|
+
'А': 'a', 'Б': 'b', 'В': 'v', 'Г': 'g', 'Д': 'd', 'Е': 'e', 'Ё': 'e', 'Ж': 'zh', 'З': 'z',
|
|
99881
|
+
'И': 'i', 'Й': 'y', 'К': 'k', 'Л': 'l', 'М': 'm', 'Н': 'n', 'О': 'o', 'П': 'p', 'Р': 'r',
|
|
99882
|
+
'С': 's', 'Т': 't', 'У': 'u', 'Ф': 'f', 'Х': 'kh', 'Ц': 'ts', 'Ч': 'ch', 'Ш': 'sh', 'Щ': 'shch',
|
|
99883
|
+
'Ъ': '', 'Ы': 'y', 'Ь': '', 'Э': 'e', 'Ю': 'yu', 'Я': 'ya'
|
|
99884
|
+
};
|
|
99885
|
+
let result = '';
|
|
99886
|
+
for (const char of text) {
|
|
99887
|
+
result += cyrillicMap[char] ?? char;
|
|
99888
|
+
}
|
|
99889
|
+
return result
|
|
99890
|
+
.toLowerCase()
|
|
99891
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
99892
|
+
.replace(/\s+/g, '-')
|
|
99893
|
+
.replace(/-+/g, '-')
|
|
99894
|
+
.replace(/^-|-$/g, '');
|
|
99895
|
+
};
|
|
99896
|
+
// Auto-generate brand from product, geo, price
|
|
99897
|
+
const brand = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => {
|
|
99898
|
+
const productPart = (() => {
|
|
99899
|
+
const p = generateProduct.trim();
|
|
99900
|
+
if (!p)
|
|
99901
|
+
return '';
|
|
99902
|
+
const dashIdx = p.search(/[-–—]/);
|
|
99903
|
+
const base = dashIdx >= 0 ? p.slice(0, dashIdx).trim() : p.split(/\s+/)[0] || p;
|
|
99904
|
+
return transliterateToSlug(base);
|
|
99905
|
+
})();
|
|
99906
|
+
const geoPart = generateGeo.trim() ? transliterateToSlug(generateGeo.trim()) : '';
|
|
99907
|
+
const pricePart = (() => {
|
|
99908
|
+
const { price, currency } = parsePriceAndCurrency(generatePriceWithCurrency);
|
|
99909
|
+
if (!price && !currency)
|
|
99910
|
+
return '';
|
|
99911
|
+
const p = price ? transliterateToSlug(price) : '';
|
|
99912
|
+
const c = currency ? transliterateToSlug(currency) : '';
|
|
99913
|
+
return [p, c].filter(Boolean).join('-');
|
|
99914
|
+
})();
|
|
99915
|
+
return [productPart, geoPart, pricePart].filter(Boolean).join('-');
|
|
99916
|
+
}, [generateProduct, generateGeo, generatePriceWithCurrency]);
|
|
99879
99917
|
const [generating, setGenerating] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99880
99918
|
const [generatingImages, setGeneratingImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99881
99919
|
const [imagesGenerationLogs, setImagesGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
99882
99920
|
const [generatedImagesData, setGeneratedImagesData] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
99883
99921
|
const [checkingImages, setCheckingImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99884
99922
|
const [uploadingImages, setUploadingImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99885
|
-
const [generatingProduct, setGeneratingProduct] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99886
99923
|
const [uploadingProduct, setUploadingProduct] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99887
|
-
const [productModalOpen, setProductModalOpen] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99888
|
-
const [productSourceImages, setProductSourceImages] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
99889
|
-
const [productGeneratedImage, setProductGeneratedImage] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
99890
|
-
const [productRegeneratePrompt, setProductRegeneratePrompt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
|
|
99891
|
-
const [productGenerationLogs, setProductGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
99892
99924
|
const [folderFilesInfo, setFolderFilesInfo] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
99925
|
+
const productFileInputRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null);
|
|
99893
99926
|
const [checkingFolderFiles, setCheckingFolderFiles] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99894
99927
|
const permissionCheckedFoldersRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(new Set());
|
|
99895
99928
|
const folderCheckRunningRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(false);
|
|
99896
|
-
const [productProcessStartTime, setProductProcessStartTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
99897
99929
|
const [imagesProcessStartTime, setImagesProcessStartTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
99898
99930
|
const [contentProcessStartTime, setContentProcessStartTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
99899
99931
|
const [contentGenerationLogs, setContentGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
99900
|
-
const [elapsedTime, setElapsedTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({
|
|
99932
|
+
const [elapsedTime, setElapsedTime] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({ images: 0, content: 0, landing: 0 });
|
|
99901
99933
|
const [uiNow, setUiNow] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => Date.now());
|
|
99902
99934
|
const [generatingLanding, setGeneratingLanding] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99903
99935
|
const [landingGenerationLogs, setLandingGenerationLogs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
@@ -99916,8 +99948,7 @@ function App() {
|
|
|
99916
99948
|
const [linkCopied, setLinkCopied] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99917
99949
|
const [openRouterKeyCopied, setOpenRouterKeyCopied] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99918
99950
|
const [loadingContentFromDrive, setLoadingContentFromDrive] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
99919
|
-
const [
|
|
99920
|
-
const [driveFilesFound, setDriveFilesFound] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({ content: false, images: false });
|
|
99951
|
+
const [driveFilesFound, setDriveFilesFound] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({ content: false });
|
|
99921
99952
|
// Theme state
|
|
99922
99953
|
const [darkMode, setDarkMode] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => {
|
|
99923
99954
|
const saved = localStorage.getItem('themeMode');
|
|
@@ -99964,7 +99995,7 @@ function App() {
|
|
|
99964
99995
|
}
|
|
99965
99996
|
};
|
|
99966
99997
|
// Create theme based on mode
|
|
99967
|
-
const theme = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => (0,
|
|
99998
|
+
const theme = react__WEBPACK_IMPORTED_MODULE_0___default().useMemo(() => (0,_mui_material__WEBPACK_IMPORTED_MODULE_31__["default"])({
|
|
99968
99999
|
palette: {
|
|
99969
100000
|
mode: darkMode ? 'dark' : 'light',
|
|
99970
100001
|
...(darkMode
|
|
@@ -100056,7 +100087,7 @@ function App() {
|
|
|
100056
100087
|
};
|
|
100057
100088
|
loadKey();
|
|
100058
100089
|
// Load prompt overrides from Electron config
|
|
100059
|
-
(0,
|
|
100090
|
+
(0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.loadOverridesFromElectron)();
|
|
100060
100091
|
}, []);
|
|
100061
100092
|
// Save form fields to localStorage whenever they change
|
|
100062
100093
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
@@ -100085,7 +100116,7 @@ function App() {
|
|
|
100085
100116
|
const timeoutId = setTimeout(async () => {
|
|
100086
100117
|
try {
|
|
100087
100118
|
logToTerminal('log', '[Auto Save] Auto-saving settings to Google Drive');
|
|
100088
|
-
await saveGeneratedContentToDrive(folderId,
|
|
100119
|
+
await saveGeneratedContentToDrive(folderId, {
|
|
100089
100120
|
generateProduct,
|
|
100090
100121
|
generateGeo,
|
|
100091
100122
|
generateAdditionalInfo,
|
|
@@ -100098,21 +100129,19 @@ function App() {
|
|
|
100098
100129
|
}
|
|
100099
100130
|
}, 1000);
|
|
100100
100131
|
return () => clearTimeout(timeoutId);
|
|
100101
|
-
}, [generateProduct, generateGeo, generateAdditionalInfo, generatePriceWithCurrency, brand, link, driveFolderUrl,
|
|
100132
|
+
}, [generateProduct, generateGeo, generateAdditionalInfo, generatePriceWithCurrency, brand, link, driveFolderUrl, loadingContentFromDrive]);
|
|
100102
100133
|
// Load generated content from Google Drive when driveFolderUrl changes
|
|
100103
100134
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
100104
100135
|
if (!driveFolderUrl) {
|
|
100105
100136
|
logToTerminal('log', '[Load] No driveFolderUrl, skipping load');
|
|
100106
100137
|
setLoadingContentFromDrive(false);
|
|
100107
|
-
|
|
100108
|
-
setDriveFilesFound({ content: false, images: false });
|
|
100138
|
+
setDriveFilesFound({ content: false });
|
|
100109
100139
|
// Clear all data when driveFolderUrl is cleared
|
|
100110
100140
|
setGeneratedTitlesData([]);
|
|
100111
100141
|
setGeneratedTextsData([]);
|
|
100112
100142
|
setGeneratedImagesData([]);
|
|
100113
100143
|
setTitles('');
|
|
100114
100144
|
setTexts(['']);
|
|
100115
|
-
setBrand('');
|
|
100116
100145
|
setLink('');
|
|
100117
100146
|
return;
|
|
100118
100147
|
}
|
|
@@ -100120,15 +100149,13 @@ function App() {
|
|
|
100120
100149
|
if (!folderId) {
|
|
100121
100150
|
logToTerminal('warn', '[Load] Invalid driveFolderUrl, cannot extract folderId:', driveFolderUrl);
|
|
100122
100151
|
setLoadingContentFromDrive(false);
|
|
100123
|
-
|
|
100124
|
-
setDriveFilesFound({ content: false, images: false });
|
|
100152
|
+
setDriveFilesFound({ content: false });
|
|
100125
100153
|
// Clear all data when folderId cannot be extracted
|
|
100126
100154
|
setGeneratedTitlesData([]);
|
|
100127
100155
|
setGeneratedTextsData([]);
|
|
100128
100156
|
setGeneratedImagesData([]);
|
|
100129
100157
|
setTitles('');
|
|
100130
100158
|
setTexts(['']);
|
|
100131
|
-
setBrand('');
|
|
100132
100159
|
setLink('');
|
|
100133
100160
|
return;
|
|
100134
100161
|
}
|
|
@@ -100139,11 +100166,9 @@ function App() {
|
|
|
100139
100166
|
setGeneratedImagesData([]);
|
|
100140
100167
|
setTitles('');
|
|
100141
100168
|
setTexts(['']);
|
|
100142
|
-
setBrand('');
|
|
100143
100169
|
setLink('');
|
|
100144
100170
|
setLoadingContentFromDrive(true);
|
|
100145
|
-
|
|
100146
|
-
setDriveFilesFound({ content: false, images: false });
|
|
100171
|
+
setDriveFilesFound({ content: false });
|
|
100147
100172
|
// Load content from Google Drive
|
|
100148
100173
|
loadGeneratedContentFromDrive(folderId).then((result) => {
|
|
100149
100174
|
logToTerminal('log', '[Load] Content loading completed, found:', result.found);
|
|
@@ -100152,8 +100177,6 @@ function App() {
|
|
|
100152
100177
|
logToTerminal('error', '[Load] Error loading content from Google Drive:', err);
|
|
100153
100178
|
setLoadingContentFromDrive(false);
|
|
100154
100179
|
});
|
|
100155
|
-
// Креативы не кешируются — не загружаем изображения при открытии
|
|
100156
|
-
setLoadingImagesFromDrive(false);
|
|
100157
100180
|
}, [driveFolderUrl]);
|
|
100158
100181
|
// Sync generatedTitlesData with titles when titles changes (except when user is editing in UI)
|
|
100159
100182
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
@@ -100179,12 +100202,6 @@ function App() {
|
|
|
100179
100202
|
}, [titles]); // Only depend on titles, not generatedTitlesData to avoid loops
|
|
100180
100203
|
// Auto-scroll logs when new logs are added
|
|
100181
100204
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
100182
|
-
if (productGenerationLogs.length > 0 && (generatingProduct || uploadingProduct)) {
|
|
100183
|
-
const logContainer = document.getElementById('product-generation-logs');
|
|
100184
|
-
if (logContainer) {
|
|
100185
|
-
logContainer.scrollTop = logContainer.scrollHeight;
|
|
100186
|
-
}
|
|
100187
|
-
}
|
|
100188
100205
|
if (imagesGenerationLogs.length > 0 && (generatingImages || uploadingImages)) {
|
|
100189
100206
|
const logContainer = document.getElementById('images-generation-logs');
|
|
100190
100207
|
if (logContainer) {
|
|
@@ -100197,30 +100214,7 @@ function App() {
|
|
|
100197
100214
|
logContainer.scrollTop = logContainer.scrollHeight;
|
|
100198
100215
|
}
|
|
100199
100216
|
}
|
|
100200
|
-
}, [
|
|
100201
|
-
// Timer for product generation
|
|
100202
|
-
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
100203
|
-
let interval = null;
|
|
100204
|
-
if (generatingProduct || uploadingProduct) {
|
|
100205
|
-
interval = setInterval(() => {
|
|
100206
|
-
setElapsedTime(prev => {
|
|
100207
|
-
if (productProcessStartTime) {
|
|
100208
|
-
const elapsed = Math.floor((Date.now() - productProcessStartTime) / 1000);
|
|
100209
|
-
return { ...prev, product: elapsed };
|
|
100210
|
-
}
|
|
100211
|
-
return prev;
|
|
100212
|
-
});
|
|
100213
|
-
}, 1000);
|
|
100214
|
-
}
|
|
100215
|
-
else {
|
|
100216
|
-
setProductProcessStartTime(null);
|
|
100217
|
-
setElapsedTime(prev => ({ ...prev, product: 0 }));
|
|
100218
|
-
}
|
|
100219
|
-
return () => {
|
|
100220
|
-
if (interval)
|
|
100221
|
-
clearInterval(interval);
|
|
100222
|
-
};
|
|
100223
|
-
}, [generatingProduct, uploadingProduct, productProcessStartTime]);
|
|
100217
|
+
}, [imagesGenerationLogs, generatingImages, uploadingImages, contentGenerationLogs, generating]);
|
|
100224
100218
|
// Timer for images generation
|
|
100225
100219
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
100226
100220
|
let interval = null;
|
|
@@ -100441,19 +100435,16 @@ function App() {
|
|
|
100441
100435
|
return;
|
|
100442
100436
|
if (response.ok) {
|
|
100443
100437
|
const data = await response.json();
|
|
100444
|
-
const bankaFiles = data.files.filter((f) => {
|
|
100445
|
-
const name = f.name?.toLowerCase() || '';
|
|
100446
|
-
return name.startsWith('banka') && (name.endsWith('.png') || name.endsWith('.jpg') || name.endsWith('.jpeg'));
|
|
100447
|
-
});
|
|
100448
100438
|
const hasProduct = data.files.some((f) => {
|
|
100449
100439
|
const name = f.name?.toLowerCase() || '';
|
|
100450
100440
|
return name === 'product.png' || name === 'product.jpg';
|
|
100451
100441
|
});
|
|
100442
|
+
const hasCreativeImages = data.files.some((f) => {
|
|
100443
|
+
const name = f.name?.toLowerCase() || '';
|
|
100444
|
+
return name !== 'product.png' && name !== 'product.jpg';
|
|
100445
|
+
});
|
|
100452
100446
|
if (!cancelled) {
|
|
100453
|
-
setFolderFilesInfo({
|
|
100454
|
-
bankaCount: bankaFiles.length,
|
|
100455
|
-
hasProduct
|
|
100456
|
-
});
|
|
100447
|
+
setFolderFilesInfo({ hasProduct, hasCreativeImages });
|
|
100457
100448
|
// If product not found, poll every 10 seconds until found
|
|
100458
100449
|
if (!hasProduct) {
|
|
100459
100450
|
pollTimeoutId = setTimeout(() => {
|
|
@@ -101540,7 +101531,7 @@ function App() {
|
|
|
101540
101531
|
setTexts(['']);
|
|
101541
101532
|
setPairTranslations({});
|
|
101542
101533
|
// Read pairs count from settings (3–10, default 3)
|
|
101543
|
-
const pairsCountInit = (0,
|
|
101534
|
+
const pairsCountInit = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.getPairsCount)();
|
|
101544
101535
|
// Initialize placeholders
|
|
101545
101536
|
const initialTitles = Array.from({ length: pairsCountInit }, (_, index) => ({
|
|
101546
101537
|
index: index + 1,
|
|
@@ -101569,7 +101560,7 @@ function App() {
|
|
|
101569
101560
|
}
|
|
101570
101561
|
// Generate all pairs (title + text) in a single request
|
|
101571
101562
|
addLog(formatLogMessage('log', '📋 Generating title+text pairs in a single request...'));
|
|
101572
|
-
const selectedIndices = (0,
|
|
101563
|
+
const selectedIndices = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_49__.getSelectedPairApproaches)();
|
|
101573
101564
|
setLastUsedApproachIndices(selectedIndices);
|
|
101574
101565
|
const pairsCount = selectedIndices.length;
|
|
101575
101566
|
addLog(formatLogMessage('log', `⚙️ Generating ${pairsCount} pairs (approaches: ${selectedIndices.join(', ')})`));
|
|
@@ -101606,20 +101597,8 @@ function App() {
|
|
|
101606
101597
|
try {
|
|
101607
101598
|
const folderId = extractFolderId(driveFolderUrl);
|
|
101608
101599
|
if (folderId) {
|
|
101609
|
-
addLog(formatLogMessage('log', '💾 Saving
|
|
101610
|
-
|
|
101611
|
-
index: index + 1,
|
|
101612
|
-
title: p.title,
|
|
101613
|
-
generating: false,
|
|
101614
|
-
failed: !p.title
|
|
101615
|
-
}));
|
|
101616
|
-
const currentTextsData = pairsResult.map((p, index) => ({
|
|
101617
|
-
index: index + 1,
|
|
101618
|
-
text: p.text,
|
|
101619
|
-
generating: false,
|
|
101620
|
-
failed: !p.text
|
|
101621
|
-
}));
|
|
101622
|
-
await saveGeneratedContentToDrive(folderId, currentTitlesData, currentTextsData, {
|
|
101600
|
+
addLog(formatLogMessage('log', '💾 Saving settings to Google Drive...'));
|
|
101601
|
+
await saveGeneratedContentToDrive(folderId, {
|
|
101623
101602
|
generateProduct,
|
|
101624
101603
|
generateGeo,
|
|
101625
101604
|
generateAdditionalInfo,
|
|
@@ -102202,362 +102181,54 @@ function App() {
|
|
|
102202
102181
|
errors: finalErrors
|
|
102203
102182
|
};
|
|
102204
102183
|
};
|
|
102205
|
-
const
|
|
102206
|
-
if (!openaiApiKey) {
|
|
102207
|
-
const errorMsg = 'OpenRouter API key is not set';
|
|
102208
|
-
logToTerminal('error', errorMsg);
|
|
102209
|
-
if (addLog)
|
|
102210
|
-
addLog(formatLogMessage('error', errorMsg));
|
|
102211
|
-
throw new Error(errorMsg);
|
|
102212
|
-
}
|
|
102213
|
-
const logMsg = (level, ...args) => {
|
|
102214
|
-
logToTerminal(level, ...args);
|
|
102215
|
-
if (addLog)
|
|
102216
|
-
addLog(formatLogMessage(level, ...args));
|
|
102217
|
-
};
|
|
102218
|
-
const isRegeneration = !!currentProductImageUrl;
|
|
102219
|
-
logMsg('log', `=== Starting product image ${isRegeneration ? 'regeneration' : 'generation'} from source images ===`);
|
|
102220
|
-
logMsg('log', '📸 Source images count:', bankaImageUrls.length);
|
|
102221
|
-
if (isRegeneration) {
|
|
102222
|
-
logMsg('log', '🔄 Regenerating with current product image as reference');
|
|
102223
|
-
}
|
|
102224
|
-
// Ensure we have a valid access token before proceeding
|
|
102225
|
-
const validToken = await getValidAccessToken();
|
|
102226
|
-
if (!validToken) {
|
|
102227
|
-
const errorMsg = 'Not logged in to Google Drive or token expired';
|
|
102228
|
-
logMsg('error', errorMsg);
|
|
102229
|
-
throw new Error(errorMsg);
|
|
102230
|
-
}
|
|
102231
|
-
logMsg('log', '✅ Valid access token confirmed');
|
|
102232
|
-
// Base prompt for product generation
|
|
102233
|
-
let basePrompt = `Прикрепил фото упаковки продукта. Сгенерируй пожалуйста её упрощенное изображение для креативов для рекламы в Facebook.
|
|
102234
|
-
ВАЖНО: Сохрани точно такую же форму упаковки, как на исходном фото (если это упаковка таблеток - оставь упаковку таблеток, если банка - оставь банку, если тюбик - оставь тюбик).
|
|
102235
|
-
Убери мелкий текст. Нужна только сама упаковка продукта, не коробка. Оставь логотип, название, примерно тот же тон.`;
|
|
102236
|
-
// If regenerating, update prompt to mention current image
|
|
102237
|
-
if (isRegeneration) {
|
|
102238
|
-
basePrompt = `${basePrompt}
|
|
102239
|
-
|
|
102240
|
-
🚨 ПЕРЕГЕНЕРАЦИЯ: Прикреплены два типа референсов:
|
|
102241
|
-
1. Исходные фото упаковки продукта (используй их как основу для формы и деталей)
|
|
102242
|
-
2. Текущий вариант сгенерированного продукта (НЕПРАВИЛЬНЫЙ, требует ОБЯЗАТЕЛЬНЫХ изменений согласно требованиям ниже)
|
|
102243
|
-
|
|
102244
|
-
═══════════════════════════════════════════════════════════════
|
|
102245
|
-
🔥 ОБЯЗАТЕЛЬНЫЕ ТРЕБОВАНИЯ ПОЛЬЗОВАТЕЛЯ (ПРИМЕНИ ВСЕ БЕЗ ИСКЛЮЧЕНИЯ):
|
|
102246
|
-
═══════════════════════════════════════════════════════════════`;
|
|
102247
|
-
}
|
|
102248
|
-
const fullPrompt = additionalPrompt.trim()
|
|
102249
|
-
? `${basePrompt}${isRegeneration ? '' : '\nДополнительные требования: '}${additionalPrompt.split('\n').filter(line => line.trim()).map(line => `• ${line.trim()}`).join('\n')}${isRegeneration ? '\n\n⚠️ ВАЖНО: ОБЯЗАТЕЛЬНО примени ВСЕ требования выше в новом варианте изображения. НЕ копируй предыдущий вариант - создай НОВОЕ изображение с учетом всех требований.' : ''}`
|
|
102250
|
-
: basePrompt;
|
|
102251
|
-
logMsg('log', '📝 Prompt:', fullPrompt);
|
|
102252
|
-
// Convert Drive URLs to direct image URLs for API
|
|
102253
|
-
// For Google Drive, convert view URLs to direct download URLs
|
|
102254
|
-
// Note: Token is refreshed above to ensure files are accessible
|
|
102255
|
-
const convertImageUrl = (url) => {
|
|
102256
|
-
const fileIdMatch = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);
|
|
102257
|
-
if (fileIdMatch) {
|
|
102258
|
-
const fileId = fileIdMatch[1];
|
|
102259
|
-
return `https://drive.google.com/uc?export=view&id=${fileId}`;
|
|
102260
|
-
}
|
|
102261
|
-
return url;
|
|
102262
|
-
};
|
|
102263
|
-
const sourceImageUrls = bankaImageUrls.map(convertImageUrl);
|
|
102264
|
-
// Build image URLs array: current product image first (if regenerating), then source images
|
|
102265
|
-
const imageUrls = [];
|
|
102266
|
-
if (currentProductImageUrl) {
|
|
102267
|
-
imageUrls.push(convertImageUrl(currentProductImageUrl));
|
|
102268
|
-
}
|
|
102269
|
-
imageUrls.push(...sourceImageUrls);
|
|
102270
|
-
logMsg('log', '🖼️ Total reference images:', imageUrls.length, isRegeneration ? '(current product + source images)' : '(source images only)');
|
|
102271
|
-
const requestBody = {
|
|
102272
|
-
model: _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.imageGeneration,
|
|
102273
|
-
messages: [
|
|
102274
|
-
{
|
|
102275
|
-
role: 'user',
|
|
102276
|
-
content: [
|
|
102277
|
-
{ type: 'text', text: fullPrompt },
|
|
102278
|
-
...imageUrls.map(url => ({ type: 'image_url', image_url: { url } }))
|
|
102279
|
-
]
|
|
102280
|
-
}
|
|
102281
|
-
],
|
|
102282
|
-
modalities: ['image', 'text'],
|
|
102283
|
-
};
|
|
102284
|
-
logMsg('log', '📦 Request body prepared with', imageUrls.length, 'source images');
|
|
102285
|
-
logMsg('log', '🚀 Sending request to OpenRouter API...');
|
|
102286
|
-
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
102287
|
-
method: 'POST',
|
|
102288
|
-
headers: {
|
|
102289
|
-
'Content-Type': 'application/json',
|
|
102290
|
-
'Authorization': `Bearer ${openaiApiKey}`,
|
|
102291
|
-
'HTTP-Referer': window.location.origin || 'https://docs-combiner.app',
|
|
102292
|
-
'X-Title': 'Docs Combiner'
|
|
102293
|
-
},
|
|
102294
|
-
body: JSON.stringify(requestBody)
|
|
102295
|
-
});
|
|
102296
|
-
const responseText = await response.text();
|
|
102297
|
-
logMsg('log', '📊 Response status:', response.status, response.statusText);
|
|
102298
|
-
if (!response.ok) {
|
|
102299
|
-
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
102300
|
-
try {
|
|
102301
|
-
const error = JSON.parse(responseText);
|
|
102302
|
-
errorMessage = error.error?.message || error.message || errorMessage;
|
|
102303
|
-
logMsg('error', '🔍 Parsed error:', errorMessage);
|
|
102304
|
-
}
|
|
102305
|
-
catch (e) {
|
|
102306
|
-
logMsg('error', '❌ Failed to parse error response as JSON');
|
|
102307
|
-
}
|
|
102308
|
-
throw new Error(errorMessage);
|
|
102309
|
-
}
|
|
102310
|
-
logMsg('log', '📥 Parsing response...');
|
|
102311
|
-
let data;
|
|
102312
|
-
try {
|
|
102313
|
-
data = JSON.parse(responseText);
|
|
102314
|
-
logMsg('log', '✅ JSON parsed successfully');
|
|
102315
|
-
}
|
|
102316
|
-
catch (e) {
|
|
102317
|
-
logMsg('error', '❌ Failed to parse response as JSON');
|
|
102318
|
-
throw new Error(`Invalid JSON response: ${responseText.substring(0, 200)}`);
|
|
102319
|
-
}
|
|
102320
|
-
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
|
|
102321
|
-
logMsg('error', '❌ Invalid response structure');
|
|
102322
|
-
throw new Error(`Invalid response structure. Expected choices[0].message`);
|
|
102323
|
-
}
|
|
102324
|
-
const message = data.choices[0].message;
|
|
102325
|
-
if (!message.images || !message.images[0] || !message.images[0].image_url) {
|
|
102326
|
-
logMsg('error', '❌ No images in response');
|
|
102327
|
-
throw new Error(`No images found in response`);
|
|
102328
|
-
}
|
|
102329
|
-
const imageUrl = message.images[0].image_url.url;
|
|
102330
|
-
logMsg('log', '✅ Product image generated successfully');
|
|
102331
|
-
logMsg('log', '=== End product image generation ===');
|
|
102332
|
-
// Update balance after successful API call
|
|
102333
|
-
fetchOpenRouterBalance();
|
|
102334
|
-
return imageUrl;
|
|
102335
|
-
};
|
|
102336
|
-
const handleGenerateProduct = async () => {
|
|
102337
|
-
const validToken = await getValidAccessToken();
|
|
102338
|
-
if (!validToken) {
|
|
102339
|
-
alert('Please log in with Google first');
|
|
102340
|
-
return;
|
|
102341
|
-
}
|
|
102184
|
+
const handleUploadProductFile = () => {
|
|
102342
102185
|
if (!driveFolderUrl.trim()) {
|
|
102343
|
-
alert('
|
|
102186
|
+
alert('Укажите папку Google Drive');
|
|
102344
102187
|
return;
|
|
102345
102188
|
}
|
|
102346
|
-
|
|
102347
|
-
const folderId = extractFolderId(driveFolderUrl);
|
|
102348
|
-
if (!folderId) {
|
|
102349
|
-
throw new Error('Invalid Google Drive Folder URL');
|
|
102350
|
-
}
|
|
102351
|
-
// Check if product.png exists in the folder
|
|
102352
|
-
logToTerminal('log', '🔍 Checking for product.png in folder...');
|
|
102353
|
-
const existingProductImage = await fetchProductImage(folderId);
|
|
102354
|
-
if (existingProductImage) {
|
|
102355
|
-
// Product.png exists - open regeneration modal without generating
|
|
102356
|
-
logToTerminal('log', '✅ product.png found in folder, opening regeneration modal');
|
|
102357
|
-
// Load product image as blob and convert to data URL for display
|
|
102358
|
-
if (!productGeneratedImage) {
|
|
102359
|
-
try {
|
|
102360
|
-
logToTerminal('log', '📥 Downloading product.png for display...');
|
|
102361
|
-
let token = await getValidAccessToken();
|
|
102362
|
-
if (!token) {
|
|
102363
|
-
throw new Error('Not logged in to Google Drive');
|
|
102364
|
-
}
|
|
102365
|
-
let imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${existingProductImage.id}?alt=media`, {
|
|
102366
|
-
headers: {
|
|
102367
|
-
'Authorization': `Bearer ${token}`
|
|
102368
|
-
}
|
|
102369
|
-
});
|
|
102370
|
-
// Handle 401 - try to refresh token and retry
|
|
102371
|
-
if (imageResponse.status === 401 && refreshToken) {
|
|
102372
|
-
logToTerminal('log', '🔄 Got 401, refreshing token...');
|
|
102373
|
-
const newToken = await refreshAccessToken(refreshToken);
|
|
102374
|
-
if (newToken) {
|
|
102375
|
-
token = newToken;
|
|
102376
|
-
logToTerminal('log', '✅ Token refreshed, retrying download...');
|
|
102377
|
-
imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${existingProductImage.id}?alt=media`, {
|
|
102378
|
-
headers: {
|
|
102379
|
-
'Authorization': `Bearer ${newToken}`
|
|
102380
|
-
}
|
|
102381
|
-
});
|
|
102382
|
-
}
|
|
102383
|
-
}
|
|
102384
|
-
if (!imageResponse.ok) {
|
|
102385
|
-
throw new Error(`Failed to download product image: ${imageResponse.status} ${imageResponse.statusText}`);
|
|
102386
|
-
}
|
|
102387
|
-
const blob = await imageResponse.blob();
|
|
102388
|
-
// Convert blob to data URL using Promise
|
|
102389
|
-
const dataUrl = await new Promise((resolve, reject) => {
|
|
102390
|
-
const reader = new FileReader();
|
|
102391
|
-
reader.onloadend = () => resolve(reader.result);
|
|
102392
|
-
reader.onerror = reject;
|
|
102393
|
-
reader.readAsDataURL(blob);
|
|
102394
|
-
});
|
|
102395
|
-
setProductGeneratedImage(dataUrl);
|
|
102396
|
-
logToTerminal('log', '✅ Product image loaded and displayed');
|
|
102397
|
-
}
|
|
102398
|
-
catch (err) {
|
|
102399
|
-
logToTerminal('error', '❌ Failed to load product image:', err.message);
|
|
102400
|
-
// Fallback to direct URL if blob download fails
|
|
102401
|
-
const productImageUrl = `https://drive.google.com/uc?export=view&id=${existingProductImage.id}`;
|
|
102402
|
-
setProductGeneratedImage(productImageUrl);
|
|
102403
|
-
}
|
|
102404
|
-
}
|
|
102405
|
-
// If we have source images, just open modal
|
|
102406
|
-
if (productSourceImages.length > 0) {
|
|
102407
|
-
logToTerminal('log', '✅ Source images available, opening modal');
|
|
102408
|
-
setProductModalOpen(true);
|
|
102409
|
-
return;
|
|
102410
|
-
}
|
|
102411
|
-
// If no source images, load them without generating
|
|
102412
|
-
logToTerminal('log', '📥 Loading source images without generating');
|
|
102413
|
-
setProductModalOpen(true);
|
|
102414
|
-
setProductGenerationLogs([]);
|
|
102415
|
-
const addLog = (msg) => {
|
|
102416
|
-
setProductGenerationLogs(prev => [...prev, msg]);
|
|
102417
|
-
};
|
|
102418
|
-
try {
|
|
102419
|
-
addLog(formatLogMessage('log', '🔍 Загрузка исходных изображений...'));
|
|
102420
|
-
logToTerminal('log', '🔍 Loading source images...');
|
|
102421
|
-
const bankaImages = await fetchBankaImages(folderId);
|
|
102422
|
-
if (bankaImages.length === 0) {
|
|
102423
|
-
addLog(formatLogMessage('warn', '⚠️ Исходные изображения не найдены, но есть product.png'));
|
|
102424
|
-
logToTerminal('warn', '⚠️ No source images found, but product.png exists');
|
|
102425
|
-
}
|
|
102426
|
-
else {
|
|
102427
|
-
addLog(formatLogMessage('log', `✅ Загружено ${bankaImages.length} исходных изображений`));
|
|
102428
|
-
logToTerminal('log', `✅ Loaded ${bankaImages.length} source image(s)`);
|
|
102429
|
-
setProductSourceImages(bankaImages.map(img => img.url));
|
|
102430
|
-
}
|
|
102431
|
-
}
|
|
102432
|
-
catch (err) {
|
|
102433
|
-
const errorMsg = 'Error loading source images: ' + err.message;
|
|
102434
|
-
addLog(formatLogMessage('error', errorMsg));
|
|
102435
|
-
logToTerminal('error', '❌ Error loading source images:', err);
|
|
102436
|
-
// Don't alert, just log - user can still regenerate with existing product image
|
|
102437
|
-
}
|
|
102438
|
-
return; // IMPORTANT: Return here to prevent generation
|
|
102439
|
-
}
|
|
102440
|
-
// Product.png doesn't exist - proceed with full generation
|
|
102441
|
-
logToTerminal('log', '🎨 product.png not found, starting full generation');
|
|
102442
|
-
}
|
|
102443
|
-
catch (err) {
|
|
102444
|
-
// If check fails, proceed with generation (better to try than to block)
|
|
102445
|
-
logToTerminal('warn', '⚠️ Failed to check for product.png, proceeding with generation:', err.message);
|
|
102446
|
-
}
|
|
102447
|
-
// Full generation flow - only when product image doesn't exist
|
|
102448
|
-
setGeneratingProduct(true);
|
|
102449
|
-
setProductGenerationLogs([]);
|
|
102450
|
-
setProductProcessStartTime(Date.now());
|
|
102451
|
-
setProductModalOpen(true);
|
|
102452
|
-
const addLog = (msg) => {
|
|
102453
|
-
setProductGenerationLogs(prev => [...prev, msg]);
|
|
102454
|
-
};
|
|
102455
|
-
try {
|
|
102456
|
-
const folderId = extractFolderId(driveFolderUrl);
|
|
102457
|
-
if (!folderId) {
|
|
102458
|
-
throw new Error('Invalid Google Drive Folder URL');
|
|
102459
|
-
}
|
|
102460
|
-
addLog(formatLogMessage('log', '🔍 Searching for source images (banka*.png/jpg)...'));
|
|
102461
|
-
logToTerminal('log', '🔍 Searching for banka images...');
|
|
102462
|
-
const bankaImages = await fetchBankaImages(folderId);
|
|
102463
|
-
if (bankaImages.length === 0) {
|
|
102464
|
-
addLog(formatLogMessage('error', 'No banka*.png/jpg files found in the folder'));
|
|
102465
|
-
alert('No banka*.png/jpg files found in the folder');
|
|
102466
|
-
return;
|
|
102467
|
-
}
|
|
102468
|
-
addLog(formatLogMessage('log', `✅ Found ${bankaImages.length} source image(s)`));
|
|
102469
|
-
logToTerminal('log', `✅ Found ${bankaImages.length} banka image(s)`);
|
|
102470
|
-
setProductSourceImages(bankaImages.map(img => img.url));
|
|
102471
|
-
// Generate product image
|
|
102472
|
-
addLog(formatLogMessage('log', '🎨 Generating product image...'));
|
|
102473
|
-
logToTerminal('log', '🎨 Generating product image...');
|
|
102474
|
-
const generatedImageUrl = await generateProductFromBanka(bankaImages.map(img => img.url), '', addLog);
|
|
102475
|
-
setProductGeneratedImage(generatedImageUrl);
|
|
102476
|
-
}
|
|
102477
|
-
catch (err) {
|
|
102478
|
-
const errorMsg = 'Error generating product: ' + err.message;
|
|
102479
|
-
addLog(formatLogMessage('error', errorMsg));
|
|
102480
|
-
logToTerminal('error', '❌ Error generating product:', err);
|
|
102481
|
-
alert(errorMsg);
|
|
102482
|
-
}
|
|
102483
|
-
finally {
|
|
102484
|
-
setGeneratingProduct(false);
|
|
102485
|
-
}
|
|
102189
|
+
productFileInputRef.current?.click();
|
|
102486
102190
|
};
|
|
102487
|
-
const
|
|
102488
|
-
|
|
102191
|
+
const handleProductFileSelected = async (e) => {
|
|
102192
|
+
const file = e.target.files?.[0];
|
|
102193
|
+
if (!file)
|
|
102489
102194
|
return;
|
|
102490
|
-
|
|
102491
|
-
|
|
102492
|
-
if (
|
|
102493
|
-
|
|
102494
|
-
}
|
|
102495
|
-
const addLog = (msg) => {
|
|
102496
|
-
setProductGenerationLogs(prev => [...prev, msg]);
|
|
102497
|
-
};
|
|
102498
|
-
try {
|
|
102499
|
-
if (productRegeneratePrompt.trim()) {
|
|
102500
|
-
addLog(formatLogMessage('log', '🔄 Переделка изображения продукта с пользовательскими требованиями:'));
|
|
102501
|
-
productRegeneratePrompt.split('\n').filter(line => line.trim()).forEach((line, idx) => {
|
|
102502
|
-
addLog(formatLogMessage('log', ` ${idx + 1}. ${line.trim()}`));
|
|
102503
|
-
});
|
|
102504
|
-
}
|
|
102505
|
-
else {
|
|
102506
|
-
addLog(formatLogMessage('log', '🔄 Переделка изображения продукта (без дополнительных требований)'));
|
|
102507
|
-
}
|
|
102508
|
-
logToTerminal('log', '🔄 Regenerating product image with additional prompt:', productRegeneratePrompt);
|
|
102509
|
-
// Pass current product image as reference for regeneration
|
|
102510
|
-
const generatedImageUrl = await generateProductFromBanka(productSourceImages, productRegeneratePrompt, addLog, productGeneratedImage || undefined);
|
|
102511
|
-
setProductGeneratedImage(generatedImageUrl);
|
|
102512
|
-
setProductRegeneratePrompt(''); // Clear prompt after regeneration
|
|
102513
|
-
}
|
|
102514
|
-
catch (err) {
|
|
102515
|
-
const errorMsg = 'Error regenerating product: ' + err.message;
|
|
102516
|
-
addLog(formatLogMessage('error', errorMsg));
|
|
102517
|
-
logToTerminal('error', '❌ Error regenerating product:', err);
|
|
102518
|
-
alert(errorMsg);
|
|
102519
|
-
}
|
|
102520
|
-
finally {
|
|
102521
|
-
setGeneratingProduct(false);
|
|
102522
|
-
}
|
|
102523
|
-
};
|
|
102524
|
-
const handleUploadProduct = async () => {
|
|
102525
|
-
if (!productGeneratedImage || !driveFolderUrl.trim()) {
|
|
102526
|
-
alert('No product image to upload');
|
|
102195
|
+
e.target.value = '';
|
|
102196
|
+
const ext = file.name.toLowerCase().split('.').pop();
|
|
102197
|
+
if (ext !== 'png' && ext !== 'jpg' && ext !== 'jpeg') {
|
|
102198
|
+
alert('Выберите изображение PNG или JPG');
|
|
102527
102199
|
return;
|
|
102528
102200
|
}
|
|
102529
102201
|
setUploadingProduct(true);
|
|
102530
|
-
setProductGenerationLogs([]);
|
|
102531
|
-
if (!productProcessStartTime) {
|
|
102532
|
-
setProductProcessStartTime(Date.now());
|
|
102533
|
-
}
|
|
102534
|
-
const addLog = (msg) => {
|
|
102535
|
-
setProductGenerationLogs(prev => [...prev, msg]);
|
|
102536
|
-
};
|
|
102537
102202
|
try {
|
|
102203
|
+
const validToken = await getValidAccessToken();
|
|
102204
|
+
if (!validToken) {
|
|
102205
|
+
alert('Войдите в Google Drive');
|
|
102206
|
+
setUploadingProduct(false);
|
|
102207
|
+
return;
|
|
102208
|
+
}
|
|
102538
102209
|
const folderId = extractFolderId(driveFolderUrl);
|
|
102539
102210
|
if (!folderId) {
|
|
102540
|
-
|
|
102541
|
-
|
|
102542
|
-
|
|
102543
|
-
logToTerminal('log', '📤 Uploading product.png...');
|
|
102544
|
-
await uploadImageToDrive(productGeneratedImage, 'product.png', folderId, addLog);
|
|
102545
|
-
addLog(formatLogMessage('log', '✅ product.png uploaded successfully'));
|
|
102546
|
-
logToTerminal('log', '✅ product.png uploaded successfully');
|
|
102547
|
-
// Update folder files info
|
|
102548
|
-
if (folderFilesInfo) {
|
|
102549
|
-
setFolderFilesInfo({
|
|
102550
|
-
...folderFilesInfo,
|
|
102551
|
-
hasProduct: true
|
|
102552
|
-
});
|
|
102211
|
+
alert('Некорректная ссылка на папку Google Drive');
|
|
102212
|
+
setUploadingProduct(false);
|
|
102213
|
+
return;
|
|
102553
102214
|
}
|
|
102554
|
-
|
|
102555
|
-
|
|
102215
|
+
const addLog = () => { };
|
|
102216
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
102217
|
+
const reader = new FileReader();
|
|
102218
|
+
reader.onloadend = () => resolve(reader.result);
|
|
102219
|
+
reader.onerror = reject;
|
|
102220
|
+
reader.readAsDataURL(file);
|
|
102221
|
+
});
|
|
102222
|
+
const filename = ext === 'png' ? 'product.png' : 'product.jpg';
|
|
102223
|
+
logToTerminal('log', '📤 Загрузка product.png/jpg...');
|
|
102224
|
+
await uploadImageToDrive(dataUrl, filename, folderId, addLog);
|
|
102225
|
+
logToTerminal('log', '✅ product.png/jpg загружен');
|
|
102226
|
+
setFolderFilesInfo(prev => prev ? { ...prev, hasProduct: true } : { hasProduct: true });
|
|
102227
|
+
alert('product.png/jpg успешно загружен в папку!');
|
|
102556
102228
|
}
|
|
102557
102229
|
catch (err) {
|
|
102558
|
-
const errorMsg = '
|
|
102559
|
-
|
|
102560
|
-
logToTerminal('error', '❌ Error uploading product:', err);
|
|
102230
|
+
const errorMsg = 'Ошибка загрузки: ' + err.message;
|
|
102231
|
+
logToTerminal('error', '❌', errorMsg);
|
|
102561
102232
|
alert(errorMsg);
|
|
102562
102233
|
}
|
|
102563
102234
|
finally {
|
|
@@ -102801,7 +102472,7 @@ function App() {
|
|
|
102801
102472
|
addLog(formatLogMessage(level, ...args));
|
|
102802
102473
|
};
|
|
102803
102474
|
logMsg('log', '📦 Creating ZIP archive with HTML and product image...');
|
|
102804
|
-
const zip = new (
|
|
102475
|
+
const zip = new (jszip__WEBPACK_IMPORTED_MODULE_51___default())();
|
|
102805
102476
|
zip.file('index.html', htmlContent);
|
|
102806
102477
|
zip.file('product.png', productImageBlob);
|
|
102807
102478
|
logMsg('log', '✅ Files added to archive: index.html, product.png');
|
|
@@ -102948,477 +102619,476 @@ function App() {
|
|
|
102948
102619
|
alert('OpenRouter API key is not set');
|
|
102949
102620
|
return;
|
|
102950
102621
|
}
|
|
102951
|
-
const validToken = await getValidAccessToken();
|
|
102952
|
-
if (!validToken) {
|
|
102953
|
-
alert('Please log in with Google first');
|
|
102954
|
-
return;
|
|
102955
|
-
}
|
|
102956
102622
|
setGeneratingLanding(true);
|
|
102957
102623
|
setLandingGenerationLogs([]);
|
|
102958
102624
|
setLandingProcessStartTime(Date.now());
|
|
102959
|
-
const addLog = (msg) => {
|
|
102960
|
-
setLandingGenerationLogs(prev => [...prev, msg]);
|
|
102961
|
-
};
|
|
102962
102625
|
try {
|
|
102963
|
-
const
|
|
102964
|
-
if (!
|
|
102965
|
-
|
|
102966
|
-
|
|
102967
|
-
|
|
102968
|
-
addLog(formatLogMessage('log', '🔍 Получение изображения продукта...'));
|
|
102969
|
-
logToTerminal('log', '🔍 Получение изображения продукта...');
|
|
102970
|
-
const productImage = await fetchProductImage(folderId);
|
|
102971
|
-
if (!productImage) {
|
|
102972
|
-
throw new Error('product.png/jpg not found in the folder');
|
|
102973
|
-
}
|
|
102974
|
-
// Download product image as blob
|
|
102975
|
-
addLog(formatLogMessage('log', '📥 Скачивание product.png...'));
|
|
102976
|
-
logToTerminal('log', '📥 Скачивание product.png...');
|
|
102977
|
-
let token = await getValidAccessToken();
|
|
102978
|
-
if (!token) {
|
|
102979
|
-
throw new Error('Not logged in to Google Drive');
|
|
102626
|
+
const validToken = await getValidAccessToken();
|
|
102627
|
+
if (!validToken) {
|
|
102628
|
+
alert('Please log in with Google first');
|
|
102629
|
+
setGeneratingLanding(false);
|
|
102630
|
+
return;
|
|
102980
102631
|
}
|
|
102981
|
-
|
|
102982
|
-
|
|
102983
|
-
|
|
102632
|
+
const addLog = (msg) => {
|
|
102633
|
+
setLandingGenerationLogs(prev => [...prev, msg]);
|
|
102634
|
+
};
|
|
102635
|
+
try {
|
|
102636
|
+
const folderId = extractFolderId(driveFolderUrl);
|
|
102637
|
+
if (!folderId) {
|
|
102638
|
+
throw new Error('Invalid Google Drive Folder URL');
|
|
102984
102639
|
}
|
|
102985
|
-
|
|
102986
|
-
|
|
102987
|
-
|
|
102988
|
-
|
|
102989
|
-
|
|
102990
|
-
|
|
102991
|
-
|
|
102992
|
-
|
|
102993
|
-
|
|
102994
|
-
|
|
102995
|
-
|
|
102996
|
-
|
|
102640
|
+
// Get product image
|
|
102641
|
+
addLog(formatLogMessage('log', '🔍 Получение изображения продукта...'));
|
|
102642
|
+
logToTerminal('log', '🔍 Получение изображения продукта...');
|
|
102643
|
+
const productImage = await fetchProductImage(folderId);
|
|
102644
|
+
if (!productImage) {
|
|
102645
|
+
throw new Error('product.png/jpg not found in the folder');
|
|
102646
|
+
}
|
|
102647
|
+
// Download product image as blob
|
|
102648
|
+
addLog(formatLogMessage('log', '📥 Скачивание product.png...'));
|
|
102649
|
+
logToTerminal('log', '📥 Скачивание product.png...');
|
|
102650
|
+
let token = await getValidAccessToken();
|
|
102651
|
+
if (!token) {
|
|
102652
|
+
throw new Error('Not logged in to Google Drive');
|
|
102653
|
+
}
|
|
102654
|
+
let imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${productImage.id}?alt=media`, {
|
|
102655
|
+
headers: {
|
|
102656
|
+
'Authorization': `Bearer ${token}`
|
|
102657
|
+
}
|
|
102658
|
+
});
|
|
102659
|
+
// Handle 401 - try to refresh token and retry
|
|
102660
|
+
if (imageResponse.status === 401 && refreshToken) {
|
|
102661
|
+
addLog(formatLogMessage('log', '🔄 Got 401, refreshing token...'));
|
|
102662
|
+
const newToken = await refreshAccessToken(refreshToken);
|
|
102663
|
+
if (newToken) {
|
|
102664
|
+
token = newToken;
|
|
102665
|
+
addLog(formatLogMessage('log', '✅ Token refreshed, retrying download...'));
|
|
102666
|
+
imageResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${productImage.id}?alt=media`, {
|
|
102667
|
+
headers: {
|
|
102668
|
+
'Authorization': `Bearer ${newToken}`
|
|
102669
|
+
}
|
|
102670
|
+
});
|
|
102671
|
+
}
|
|
102672
|
+
}
|
|
102673
|
+
if (!imageResponse.ok) {
|
|
102674
|
+
throw new Error(`Failed to download product image: ${imageResponse.status} ${imageResponse.statusText}`);
|
|
102675
|
+
}
|
|
102676
|
+
const productImageBlob = await imageResponse.blob();
|
|
102677
|
+
addLog(formatLogMessage('log', '✅ Изображение продукта скачано'));
|
|
102678
|
+
logToTerminal('log', '✅ Изображение продукта скачано, размер:', (productImageBlob.size / 1024).toFixed(2), 'KB');
|
|
102679
|
+
// Generate HTML landing page
|
|
102680
|
+
if (!generateProduct.trim() || !generateGeo.trim()) {
|
|
102681
|
+
throw new Error('Please fill in Product and Geo fields');
|
|
102682
|
+
}
|
|
102683
|
+
addLog(formatLogMessage('log', '🎨 Генерация HTML лендинга...'));
|
|
102684
|
+
logToTerminal('log', '🎨 Генерация HTML лендинга...');
|
|
102685
|
+
const htmlContent = await generateLandingHTML(generateProduct, generateGeo, addLog);
|
|
102686
|
+
addLog(formatLogMessage('log', '✅ HTML лендинг сгенерирован'));
|
|
102687
|
+
// Save HTML and image for preview
|
|
102688
|
+
setGeneratedLandingHTML(htmlContent);
|
|
102689
|
+
setGeneratedLandingImageBlob(productImageBlob);
|
|
102690
|
+
// Open preview in browser immediately
|
|
102691
|
+
try {
|
|
102692
|
+
addLog(formatLogMessage('log', '👁️ Открытие предпросмотра в браузере...'));
|
|
102693
|
+
logToTerminal('log', '👁️ Открытие предпросмотра в браузере...');
|
|
102694
|
+
// Convert image blob to base64
|
|
102695
|
+
const imageBase64 = await new Promise((resolve, reject) => {
|
|
102696
|
+
const reader = new FileReader();
|
|
102697
|
+
reader.onloadend = () => {
|
|
102698
|
+
const base64String = reader.result;
|
|
102699
|
+
resolve(base64String);
|
|
102700
|
+
};
|
|
102701
|
+
reader.onerror = reject;
|
|
102702
|
+
reader.readAsDataURL(productImageBlob);
|
|
102997
102703
|
});
|
|
102704
|
+
// Replace relative image path with base64 data URL in HTML
|
|
102705
|
+
const htmlWithImage = htmlContent.replace(/src=["']product\.(png|jpe?g)["']/gi, `src="${imageBase64}"`);
|
|
102706
|
+
// Create blob URL and open in new window
|
|
102707
|
+
const blob = new Blob([htmlWithImage], { type: 'text/html' });
|
|
102708
|
+
const url = URL.createObjectURL(blob);
|
|
102709
|
+
window.open(url, '_blank');
|
|
102710
|
+
addLog(formatLogMessage('log', '✅ Предпросмотр открыт в браузере'));
|
|
102711
|
+
logToTerminal('log', '✅ Предпросмотр открыт в браузере');
|
|
102712
|
+
// Clean up URL after a delay (give browser time to load)
|
|
102713
|
+
setTimeout(() => {
|
|
102714
|
+
URL.revokeObjectURL(url);
|
|
102715
|
+
}, 1000);
|
|
102998
102716
|
}
|
|
102717
|
+
catch (previewErr) {
|
|
102718
|
+
addLog(formatLogMessage('warn', '⚠️ Не удалось открыть предпросмотр: ' + previewErr.message));
|
|
102719
|
+
logToTerminal('warn', '⚠️ Не удалось открыть предпросмотр:', previewErr);
|
|
102720
|
+
}
|
|
102721
|
+
// Create ZIP archive with HTML and product image
|
|
102722
|
+
addLog(formatLogMessage('log', '📦 Создание ZIP архива с HTML и изображением...'));
|
|
102723
|
+
logToTerminal('log', '📦 Создание ZIP архива...');
|
|
102724
|
+
const zipBlob = await createZipArchive(htmlContent, productImageBlob, addLog);
|
|
102725
|
+
addLog(formatLogMessage('log', '✅ ZIP архив создан'));
|
|
102726
|
+
// Upload ZIP to Google Drive
|
|
102727
|
+
const zipFilename = `landing_${Date.now()}.zip`;
|
|
102728
|
+
addLog(formatLogMessage('log', '📤 Загрузка ZIP архива на Google Drive...'));
|
|
102729
|
+
logToTerminal('log', '📤 Загрузка ZIP архива на Google Drive...');
|
|
102730
|
+
const driveLink = await uploadZipToDrive(zipBlob, zipFilename, folderId, addLog);
|
|
102731
|
+
addLog(formatLogMessage('log', '✅ ZIP архив загружен на Google Drive'));
|
|
102732
|
+
logToTerminal('log', '✅ ZIP архив загружен:', driveLink);
|
|
102999
102733
|
}
|
|
103000
|
-
|
|
103001
|
-
|
|
103002
|
-
|
|
103003
|
-
|
|
103004
|
-
|
|
103005
|
-
logToTerminal('log', '✅ Изображение продукта скачано, размер:', (productImageBlob.size / 1024).toFixed(2), 'KB');
|
|
103006
|
-
// Generate HTML landing page
|
|
103007
|
-
if (!generateProduct.trim() || !generateGeo.trim()) {
|
|
103008
|
-
throw new Error('Please fill in Product and Geo fields');
|
|
103009
|
-
}
|
|
103010
|
-
addLog(formatLogMessage('log', '🎨 Генерация HTML лендинга...'));
|
|
103011
|
-
logToTerminal('log', '🎨 Генерация HTML лендинга...');
|
|
103012
|
-
const htmlContent = await generateLandingHTML(generateProduct, generateGeo, addLog);
|
|
103013
|
-
addLog(formatLogMessage('log', '✅ HTML лендинг сгенерирован'));
|
|
103014
|
-
// Save HTML and image for preview
|
|
103015
|
-
setGeneratedLandingHTML(htmlContent);
|
|
103016
|
-
setGeneratedLandingImageBlob(productImageBlob);
|
|
103017
|
-
// Open preview in browser immediately
|
|
103018
|
-
try {
|
|
103019
|
-
addLog(formatLogMessage('log', '👁️ Открытие предпросмотра в браузере...'));
|
|
103020
|
-
logToTerminal('log', '👁️ Открытие предпросмотра в браузере...');
|
|
103021
|
-
// Convert image blob to base64
|
|
103022
|
-
const imageBase64 = await new Promise((resolve, reject) => {
|
|
103023
|
-
const reader = new FileReader();
|
|
103024
|
-
reader.onloadend = () => {
|
|
103025
|
-
const base64String = reader.result;
|
|
103026
|
-
resolve(base64String);
|
|
103027
|
-
};
|
|
103028
|
-
reader.onerror = reject;
|
|
103029
|
-
reader.readAsDataURL(productImageBlob);
|
|
103030
|
-
});
|
|
103031
|
-
// Replace relative image path with base64 data URL in HTML
|
|
103032
|
-
const htmlWithImage = htmlContent.replace(/src=["']product\.(png|jpe?g)["']/gi, `src="${imageBase64}"`);
|
|
103033
|
-
// Create blob URL and open in new window
|
|
103034
|
-
const blob = new Blob([htmlWithImage], { type: 'text/html' });
|
|
103035
|
-
const url = URL.createObjectURL(blob);
|
|
103036
|
-
window.open(url, '_blank');
|
|
103037
|
-
addLog(formatLogMessage('log', '✅ Предпросмотр открыт в браузере'));
|
|
103038
|
-
logToTerminal('log', '✅ Предпросмотр открыт в браузере');
|
|
103039
|
-
// Clean up URL after a delay (give browser time to load)
|
|
103040
|
-
setTimeout(() => {
|
|
103041
|
-
URL.revokeObjectURL(url);
|
|
103042
|
-
}, 1000);
|
|
102734
|
+
catch (err) {
|
|
102735
|
+
const errorMsg = 'Ошибка создания лендинга: ' + err.message;
|
|
102736
|
+
addLog(formatLogMessage('error', errorMsg));
|
|
102737
|
+
logToTerminal('error', '❌ Ошибка создания лендинга:', err);
|
|
102738
|
+
alert(errorMsg);
|
|
103043
102739
|
}
|
|
103044
|
-
|
|
103045
|
-
|
|
103046
|
-
logToTerminal('warn', '⚠️ Не удалось открыть предпросмотр:', previewErr);
|
|
102740
|
+
finally {
|
|
102741
|
+
setGeneratingLanding(false);
|
|
103047
102742
|
}
|
|
103048
|
-
// Create ZIP archive with HTML and product image
|
|
103049
|
-
addLog(formatLogMessage('log', '📦 Создание ZIP архива с HTML и изображением...'));
|
|
103050
|
-
logToTerminal('log', '📦 Создание ZIP архива...');
|
|
103051
|
-
const zipBlob = await createZipArchive(htmlContent, productImageBlob, addLog);
|
|
103052
|
-
addLog(formatLogMessage('log', '✅ ZIP архив создан'));
|
|
103053
|
-
// Upload ZIP to Google Drive
|
|
103054
|
-
const zipFilename = `landing_${Date.now()}.zip`;
|
|
103055
|
-
addLog(formatLogMessage('log', '📤 Загрузка ZIP архива на Google Drive...'));
|
|
103056
|
-
logToTerminal('log', '📤 Загрузка ZIP архива на Google Drive...');
|
|
103057
|
-
const driveLink = await uploadZipToDrive(zipBlob, zipFilename, folderId, addLog);
|
|
103058
|
-
addLog(formatLogMessage('log', '✅ ZIP архив загружен на Google Drive'));
|
|
103059
|
-
logToTerminal('log', '✅ ZIP архив загружен:', driveLink);
|
|
103060
102743
|
}
|
|
103061
102744
|
catch (err) {
|
|
103062
|
-
const errorMsg = 'Ошибка создания лендинга: ' + err.message;
|
|
103063
|
-
addLog(formatLogMessage('error', errorMsg));
|
|
103064
|
-
logToTerminal('error', '❌ Ошибка создания лендинга:', err);
|
|
103065
|
-
alert(errorMsg);
|
|
103066
|
-
}
|
|
103067
|
-
finally {
|
|
103068
102745
|
setGeneratingLanding(false);
|
|
102746
|
+
logToTerminal('error', '❌ Error:', err?.message || err);
|
|
103069
102747
|
}
|
|
103070
102748
|
};
|
|
103071
102749
|
const handleGenerateImages = async () => {
|
|
103072
|
-
logToTerminal('log', '🎨 === Starting image generation process ===');
|
|
103073
|
-
logToTerminal('log', '📅 Timestamp:', new Date().toISOString());
|
|
103074
|
-
const validToken = await getValidAccessToken();
|
|
103075
|
-
if (!validToken) {
|
|
103076
|
-
logToTerminal('error', '❌ Not logged in to Google');
|
|
103077
|
-
alert('Please log in with Google first');
|
|
103078
|
-
return;
|
|
103079
|
-
}
|
|
103080
|
-
logToTerminal('log', '✅ Google authentication OK');
|
|
103081
|
-
if (!generateProduct.trim() || !generateGeo.trim()) {
|
|
103082
|
-
logToTerminal('error', '❌ Missing product or geo');
|
|
103083
|
-
alert('Please fill in Product and Geo fields');
|
|
103084
|
-
return;
|
|
103085
|
-
}
|
|
103086
|
-
const { price: generatePrice, currency: generateCurrency, currencySymbol } = parsePriceAndCurrency(generatePriceWithCurrency);
|
|
103087
|
-
// Use original currency symbol if available, otherwise use currency code
|
|
103088
|
-
const currencyForPrompt = currencySymbol || generateCurrency;
|
|
103089
|
-
logToTerminal('log', '📦 Product:', generateProduct);
|
|
103090
|
-
logToTerminal('log', '🌍 Geo:', generateGeo);
|
|
103091
|
-
logToTerminal('log', '💰 Price:', generatePrice, currencyForPrompt);
|
|
103092
|
-
if (!driveFolderUrl.trim()) {
|
|
103093
|
-
logToTerminal('error', '❌ Missing Drive folder URL');
|
|
103094
|
-
alert('Please fill in Google Drive Folder URL');
|
|
103095
|
-
return;
|
|
103096
|
-
}
|
|
103097
|
-
logToTerminal('log', '📁 Drive folder URL:', driveFolderUrl);
|
|
103098
102750
|
setGeneratingImages(true);
|
|
103099
102751
|
setImagesGenerationLogs([]);
|
|
103100
102752
|
setImagesProcessStartTime(Date.now());
|
|
103101
102753
|
setCheckingImages(false);
|
|
103102
|
-
|
|
103103
|
-
|
|
103104
|
-
setImagesGenerationLogs(prev => [...prev, msg]);
|
|
103105
|
-
};
|
|
102754
|
+
logToTerminal('log', '🎨 === Starting image generation process ===');
|
|
102755
|
+
logToTerminal('log', '📅 Timestamp:', new Date().toISOString());
|
|
103106
102756
|
try {
|
|
103107
|
-
const
|
|
103108
|
-
if (!
|
|
103109
|
-
|
|
103110
|
-
|
|
103111
|
-
|
|
102757
|
+
const validToken = await getValidAccessToken();
|
|
102758
|
+
if (!validToken) {
|
|
102759
|
+
logToTerminal('error', '❌ Not logged in to Google');
|
|
102760
|
+
alert('Please log in with Google first');
|
|
102761
|
+
setGeneratingImages(false);
|
|
102762
|
+
return;
|
|
103112
102763
|
}
|
|
103113
|
-
|
|
103114
|
-
|
|
103115
|
-
|
|
103116
|
-
|
|
103117
|
-
|
|
103118
|
-
|
|
103119
|
-
addLog(formatLogMessage('error', errorMsg));
|
|
103120
|
-
throw new Error(errorMsg);
|
|
102764
|
+
logToTerminal('log', '✅ Google authentication OK');
|
|
102765
|
+
if (!generateProduct.trim() || !generateGeo.trim()) {
|
|
102766
|
+
logToTerminal('error', '❌ Missing product or geo');
|
|
102767
|
+
alert('Please fill in Product and Geo fields');
|
|
102768
|
+
setGeneratingImages(false);
|
|
102769
|
+
return;
|
|
103121
102770
|
}
|
|
103122
|
-
|
|
103123
|
-
|
|
103124
|
-
|
|
103125
|
-
|
|
103126
|
-
|
|
103127
|
-
|
|
102771
|
+
const { price: generatePrice, currency: generateCurrency, currencySymbol } = parsePriceAndCurrency(generatePriceWithCurrency);
|
|
102772
|
+
const currencyForPrompt = currencySymbol || generateCurrency;
|
|
102773
|
+
logToTerminal('log', '📦 Product:', generateProduct);
|
|
102774
|
+
logToTerminal('log', '🌍 Geo:', generateGeo);
|
|
102775
|
+
logToTerminal('log', '💰 Price:', generatePrice, currencyForPrompt);
|
|
102776
|
+
if (!driveFolderUrl.trim()) {
|
|
102777
|
+
logToTerminal('error', '❌ Missing Drive folder URL');
|
|
102778
|
+
alert('Please fill in Google Drive Folder URL');
|
|
103128
102779
|
setGeneratingImages(false);
|
|
103129
102780
|
return;
|
|
103130
102781
|
}
|
|
103131
|
-
|
|
103132
|
-
const
|
|
103133
|
-
const
|
|
103134
|
-
|
|
103135
|
-
|
|
103136
|
-
|
|
103137
|
-
|
|
103138
|
-
|
|
103139
|
-
|
|
103140
|
-
|
|
103141
|
-
|
|
103142
|
-
|
|
103143
|
-
|
|
103144
|
-
|
|
103145
|
-
|
|
103146
|
-
|
|
103147
|
-
|
|
103148
|
-
|
|
103149
|
-
|
|
103150
|
-
|
|
103151
|
-
|
|
103152
|
-
|
|
103153
|
-
|
|
103154
|
-
|
|
103155
|
-
|
|
103156
|
-
|
|
103157
|
-
|
|
103158
|
-
|
|
103159
|
-
|
|
103160
|
-
|
|
103161
|
-
|
|
103162
|
-
|
|
103163
|
-
|
|
103164
|
-
|
|
103165
|
-
|
|
103166
|
-
|
|
103167
|
-
|
|
103168
|
-
|
|
103169
|
-
|
|
103170
|
-
|
|
103171
|
-
|
|
103172
|
-
|
|
103173
|
-
|
|
103174
|
-
|
|
103175
|
-
|
|
103176
|
-
|
|
103177
|
-
|
|
103178
|
-
|
|
103179
|
-
|
|
103180
|
-
|
|
103181
|
-
|
|
103182
|
-
|
|
103183
|
-
|
|
103184
|
-
|
|
103185
|
-
|
|
103186
|
-
|
|
103187
|
-
|
|
103188
|
-
|
|
103189
|
-
|
|
103190
|
-
|
|
103191
|
-
|
|
103192
|
-
|
|
103193
|
-
|
|
103194
|
-
|
|
103195
|
-
|
|
103196
|
-
|
|
103197
|
-
|
|
103198
|
-
:
|
|
103199
|
-
|
|
103200
|
-
|
|
103201
|
-
|
|
103202
|
-
|
|
103203
|
-
|
|
103204
|
-
|
|
102782
|
+
logToTerminal('log', '📁 Drive folder URL:', driveFolderUrl);
|
|
102783
|
+
const overallStartTime = Date.now();
|
|
102784
|
+
const addLog = (msg) => {
|
|
102785
|
+
setImagesGenerationLogs(prev => [...prev, msg]);
|
|
102786
|
+
};
|
|
102787
|
+
try {
|
|
102788
|
+
const folderId = extractFolderId(driveFolderUrl);
|
|
102789
|
+
if (!folderId) {
|
|
102790
|
+
const errorMsg = 'Invalid Google Drive Folder URL';
|
|
102791
|
+
addLog(formatLogMessage('error', '❌ Invalid folder ID extracted'));
|
|
102792
|
+
throw new Error(errorMsg);
|
|
102793
|
+
}
|
|
102794
|
+
addLog(formatLogMessage('log', '✅ Folder ID extracted:', folderId));
|
|
102795
|
+
// Check for product.png
|
|
102796
|
+
addLog(formatLogMessage('log', '🔍 Checking for product.png...'));
|
|
102797
|
+
const productImage = await fetchProductImage(folderId);
|
|
102798
|
+
if (!productImage) {
|
|
102799
|
+
const errorMsg = 'product.png/jpg not found in the folder. Please generate it first using "Generate Product from Banka" button.';
|
|
102800
|
+
addLog(formatLogMessage('error', errorMsg));
|
|
102801
|
+
throw new Error(errorMsg);
|
|
102802
|
+
}
|
|
102803
|
+
addLog(formatLogMessage('log', '✅ product.png found'));
|
|
102804
|
+
// Generate images with different approaches (количество по каждому подходу)
|
|
102805
|
+
const approaches = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getCreoApproaches)();
|
|
102806
|
+
if (approaches.length === 0) {
|
|
102807
|
+
addLog(formatLogMessage('error', '❌ Укажите количество изображений (хотя бы 1) в настройках подходов'));
|
|
102808
|
+
alert('Укажите количество изображений (1–4) хотя бы для одного подхода в настройках');
|
|
102809
|
+
setGeneratingImages(false);
|
|
102810
|
+
return;
|
|
102811
|
+
}
|
|
102812
|
+
// При "both" — на каждый подход генерируем и 1:1, и 2:3
|
|
102813
|
+
const useBoth = imageAspectRatio === 'both';
|
|
102814
|
+
const tasks = useBoth
|
|
102815
|
+
? approaches.flatMap(a => [
|
|
102816
|
+
{ approach: a, ratio: '1:1' },
|
|
102817
|
+
{ approach: a, ratio: '2:3' }
|
|
102818
|
+
])
|
|
102819
|
+
: approaches.map(a => ({ approach: a, ratio: imageAspectRatio }));
|
|
102820
|
+
const additionalInfoLine = generateAdditionalInfo.trim()
|
|
102821
|
+
? `\n📋 ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ О ПРОДУКТЕ: ${generateAdditionalInfo.trim()}`
|
|
102822
|
+
: '';
|
|
102823
|
+
const imagePrompts = tasks.map(t => {
|
|
102824
|
+
const basePromptStructure = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getImageGenerationBasePrompt)(generateGeo, generatePrice, currencyForPrompt, undefined, t.ratio);
|
|
102825
|
+
const bulletsLine = t.approach.noBullets
|
|
102826
|
+
? t.approach.bulletsFocus
|
|
102827
|
+
: (() => {
|
|
102828
|
+
const [b1, b2, b3] = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.pickRandomBullets)(t.approach.name);
|
|
102829
|
+
return `ОБЯЗАТЕЛЬНЫЕ БУЛЛЕТЫ (используй именно эти 3, в указанном порядке; переведи на язык ${generateGeo}): 1) ${b1} 2) ${b2} 3) ${b3}`;
|
|
102830
|
+
})();
|
|
102831
|
+
return `🏷️ ПРОДУКТ: ${generateProduct}${additionalInfoLine}\n\n${basePromptStructure}🏷️ ПРОДУКТ (повторение для ясности): ${generateProduct}${additionalInfoLine}\n🎯 ПОДХОД: ${t.approach.name}\n\n${t.approach.prompt}\n\n${t.approach.headlineAngle}\n\n${bulletsLine}\n\nТекст — строго следуй правилам этого подхода (HOOK обязателен; буллиты добавляй только если они разрешены для подхода).`;
|
|
102832
|
+
});
|
|
102833
|
+
// Initialize placeholders for all images
|
|
102834
|
+
const initialPlaceholders = tasks.map((t, index) => ({
|
|
102835
|
+
index: index + 1,
|
|
102836
|
+
imageUrl: undefined,
|
|
102837
|
+
approach: t.approach.name + (useBoth ? ` (${t.ratio})` : ''),
|
|
102838
|
+
aspectRatio: t.ratio,
|
|
102839
|
+
uploaded: false,
|
|
102840
|
+
checking: false,
|
|
102841
|
+
checkStatus: 'pending',
|
|
102842
|
+
checkResult: undefined,
|
|
102843
|
+
checkErrors: undefined,
|
|
102844
|
+
originalPrompt: undefined,
|
|
102845
|
+
productImageUrl: undefined,
|
|
102846
|
+
regenerating: false,
|
|
102847
|
+
customRegeneratePrompt: '',
|
|
102848
|
+
failed: false,
|
|
102849
|
+
generating: false // In sequential mode, images start queued (not generating)
|
|
102850
|
+
}));
|
|
102851
|
+
setGeneratedImagesData(initialPlaceholders);
|
|
102852
|
+
addLog(formatLogMessage('log', `📝 Generated prompts for ${tasks.length} images${useBoth ? ' (1:1 + 2:3 на каждый подход)' : ''}`));
|
|
102853
|
+
const maxParallel = 100; // like infinity
|
|
102854
|
+
addLog(formatLogMessage('log', `🚀 Generating ${tasks.length} images in parallel (up to ${maxParallel} at a time)...`));
|
|
102855
|
+
const generationStartTime = Date.now();
|
|
102856
|
+
const resultsMap = new Map();
|
|
102857
|
+
// Ensure each slot has prompt+product reference from the start (needed for regeneration even on failures)
|
|
102858
|
+
setGeneratedImagesData(prev => prev.map((img, i) => ({
|
|
102859
|
+
...img,
|
|
102860
|
+
originalPrompt: img.originalPrompt || imagePrompts[i],
|
|
102861
|
+
productImageUrl: img.productImageUrl || productImage.url
|
|
102862
|
+
})));
|
|
102863
|
+
const runOne = async (i, isRetry) => {
|
|
102864
|
+
const imageIndex = i + 1;
|
|
102865
|
+
const task = tasks[i];
|
|
102866
|
+
const approachName = task?.approach.name || 'Unknown';
|
|
102867
|
+
const prompt = imagePrompts[i];
|
|
102868
|
+
const ratio = task?.ratio || '1:1';
|
|
102869
|
+
addLog(formatLogMessage('log', `${isRetry ? '🔄' : '🎨'} Генерация изображения ${imageIndex}/${tasks.length} (${approachName} ${ratio})...`));
|
|
102870
|
+
// Reset slot state for this run
|
|
103205
102871
|
setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
|
|
103206
102872
|
? {
|
|
103207
102873
|
...img,
|
|
103208
|
-
|
|
103209
|
-
generating: false,
|
|
102874
|
+
generating: true,
|
|
103210
102875
|
failed: false,
|
|
103211
|
-
|
|
103212
|
-
|
|
102876
|
+
errorMessage: undefined,
|
|
102877
|
+
imageUrl: undefined,
|
|
102878
|
+
checking: false,
|
|
102879
|
+
checkStatus: 'pending',
|
|
102880
|
+
checkResult: undefined,
|
|
102881
|
+
checkErrors: undefined,
|
|
103213
102882
|
originalPrompt: prompt,
|
|
103214
102883
|
productImageUrl: productImage.url
|
|
103215
102884
|
}
|
|
103216
102885
|
: img));
|
|
103217
|
-
|
|
103218
|
-
|
|
103219
|
-
|
|
103220
|
-
|
|
103221
|
-
|
|
103222
|
-
|
|
103223
|
-
|
|
103224
|
-
if (!res)
|
|
103225
|
-
throw new Error('No validation result');
|
|
103226
|
-
return { ...res, checkFailed: false };
|
|
103227
|
-
}
|
|
103228
|
-
catch (validationErr) {
|
|
103229
|
-
const msg = validationErr?.message || String(validationErr);
|
|
103230
|
-
addLog(formatLogMessage('error', `❌ Ошибка проверки изображения ${imageIndex}: ${msg}`));
|
|
103231
|
-
return {
|
|
103232
|
-
status: 'needs_rebuild',
|
|
103233
|
-
result: `Ошибка проверки: ${msg}`,
|
|
103234
|
-
errors: [msg],
|
|
103235
|
-
checkFailed: true
|
|
103236
|
-
};
|
|
103237
|
-
}
|
|
103238
|
-
})();
|
|
103239
|
-
setGeneratedImagesData(prev => {
|
|
103240
|
-
const updated = prev.map(img => img.index === imageIndex
|
|
102886
|
+
try {
|
|
102887
|
+
const imageUrl = await generateImageWithDALLE(prompt, productImage.url, ratio);
|
|
102888
|
+
if (!imageUrl || typeof imageUrl !== 'string' || imageUrl.trim() === '') {
|
|
102889
|
+
throw new Error('Generated image URL is empty or invalid');
|
|
102890
|
+
}
|
|
102891
|
+
// Update UI immediately when image is ready
|
|
102892
|
+
setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
|
|
103241
102893
|
? {
|
|
103242
102894
|
...img,
|
|
103243
|
-
|
|
103244
|
-
|
|
103245
|
-
|
|
103246
|
-
|
|
103247
|
-
|
|
103248
|
-
|
|
103249
|
-
|
|
103250
|
-
: '',
|
|
103251
|
-
originalPrompt: img.originalPrompt || prompt,
|
|
103252
|
-
productImageUrl: img.productImageUrl || productImage.url
|
|
102895
|
+
imageUrl,
|
|
102896
|
+
generating: false,
|
|
102897
|
+
failed: false,
|
|
102898
|
+
checking: true,
|
|
102899
|
+
checkStatus: 'checking',
|
|
102900
|
+
originalPrompt: prompt,
|
|
102901
|
+
productImageUrl: productImage.url
|
|
103253
102902
|
}
|
|
103254
|
-
: img);
|
|
103255
|
-
|
|
103256
|
-
|
|
103257
|
-
|
|
103258
|
-
|
|
103259
|
-
|
|
103260
|
-
|
|
103261
|
-
|
|
103262
|
-
|
|
103263
|
-
|
|
103264
|
-
|
|
103265
|
-
|
|
103266
|
-
|
|
103267
|
-
|
|
103268
|
-
|
|
103269
|
-
|
|
103270
|
-
|
|
103271
|
-
|
|
103272
|
-
|
|
103273
|
-
|
|
103274
|
-
|
|
103275
|
-
|
|
102903
|
+
: img));
|
|
102904
|
+
addLog(formatLogMessage('log', `✅ Изображение ${imageIndex}/${tasks.length} сгенерировано`));
|
|
102905
|
+
// Validate right after generation (skip if validation disabled)
|
|
102906
|
+
const validationResult = validationDisabled
|
|
102907
|
+
? { status: 'ok', result: 'Проверка отключена', errors: [], checkFailed: false }
|
|
102908
|
+
: await (async () => {
|
|
102909
|
+
try {
|
|
102910
|
+
const res = await validateCreativeImage(imageUrl, generateProduct, generateGeo, addLog, approachName);
|
|
102911
|
+
if (!res)
|
|
102912
|
+
throw new Error('No validation result');
|
|
102913
|
+
return { ...res, checkFailed: false };
|
|
102914
|
+
}
|
|
102915
|
+
catch (validationErr) {
|
|
102916
|
+
const msg = validationErr?.message || String(validationErr);
|
|
102917
|
+
addLog(formatLogMessage('error', `❌ Ошибка проверки изображения ${imageIndex}: ${msg}`));
|
|
102918
|
+
return {
|
|
102919
|
+
status: 'needs_rebuild',
|
|
102920
|
+
result: `Ошибка проверки: ${msg}`,
|
|
102921
|
+
errors: [msg],
|
|
102922
|
+
checkFailed: true
|
|
102923
|
+
};
|
|
102924
|
+
}
|
|
102925
|
+
})();
|
|
102926
|
+
setGeneratedImagesData(prev => {
|
|
102927
|
+
const updated = prev.map(img => img.index === imageIndex
|
|
102928
|
+
? {
|
|
102929
|
+
...img,
|
|
102930
|
+
checking: false,
|
|
102931
|
+
checkFailed: validationResult.checkFailed ?? false,
|
|
102932
|
+
checkStatus: validationResult.status,
|
|
102933
|
+
checkResult: validationResult.result,
|
|
102934
|
+
checkErrors: validationResult.errors,
|
|
102935
|
+
customRegeneratePrompt: validationResult.errors.length > 0
|
|
102936
|
+
? validationResult.errors.map(err => err.replace(/^ОШИБКА:\s*/i, '')).join('\n')
|
|
102937
|
+
: '',
|
|
102938
|
+
originalPrompt: img.originalPrompt || prompt,
|
|
102939
|
+
productImageUrl: img.productImageUrl || productImage.url
|
|
102940
|
+
}
|
|
102941
|
+
: img);
|
|
102942
|
+
return updated;
|
|
102943
|
+
});
|
|
102944
|
+
return {
|
|
102945
|
+
index: imageIndex,
|
|
102946
|
+
imageUrl,
|
|
102947
|
+
success: true,
|
|
102948
|
+
approach: approachName,
|
|
103276
102949
|
originalPrompt: prompt,
|
|
103277
102950
|
productImageUrl: productImage.url
|
|
103278
|
-
}
|
|
103279
|
-
|
|
103280
|
-
|
|
103281
|
-
|
|
103282
|
-
|
|
103283
|
-
|
|
103284
|
-
|
|
103285
|
-
|
|
103286
|
-
|
|
103287
|
-
|
|
103288
|
-
|
|
103289
|
-
|
|
103290
|
-
|
|
103291
|
-
|
|
103292
|
-
|
|
103293
|
-
|
|
103294
|
-
|
|
103295
|
-
|
|
103296
|
-
let cursor = 0;
|
|
103297
|
-
const workerCount = Math.min(maxParallel, imagePrompts.length);
|
|
103298
|
-
const workers = Array.from({ length: workerCount }, () => (async () => {
|
|
103299
|
-
while (true) {
|
|
103300
|
-
const i = cursor++;
|
|
103301
|
-
if (i >= imagePrompts.length)
|
|
103302
|
-
break;
|
|
103303
|
-
const res = await runOne(i, false);
|
|
103304
|
-
resultsMap.set(res.index, res);
|
|
103305
|
-
}
|
|
103306
|
-
})());
|
|
103307
|
-
await Promise.all(workers);
|
|
103308
|
-
let generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
|
|
103309
|
-
// Safety: fill any missing slots as failed
|
|
103310
|
-
if (generationResults.length !== imagePrompts.length) {
|
|
103311
|
-
for (let i = 0; i < imagePrompts.length; i++) {
|
|
103312
|
-
const idx = i + 1;
|
|
103313
|
-
if (!resultsMap.has(idx)) {
|
|
103314
|
-
generationResults.push({
|
|
103315
|
-
index: idx,
|
|
102951
|
+
};
|
|
102952
|
+
}
|
|
102953
|
+
catch (err) {
|
|
102954
|
+
const errorMessage = err?.message || String(err);
|
|
102955
|
+
addLog(formatLogMessage('error', `❌ Не удалось сгенерировать изображение ${imageIndex}/${tasks.length}: ${errorMessage}`));
|
|
102956
|
+
setGeneratedImagesData(prev => prev.map(img => img.index === imageIndex
|
|
102957
|
+
? {
|
|
102958
|
+
...img,
|
|
102959
|
+
generating: false,
|
|
102960
|
+
failed: true,
|
|
102961
|
+
errorMessage,
|
|
102962
|
+
checking: false,
|
|
102963
|
+
originalPrompt: prompt,
|
|
102964
|
+
productImageUrl: productImage.url
|
|
102965
|
+
}
|
|
102966
|
+
: img));
|
|
102967
|
+
return {
|
|
102968
|
+
index: imageIndex,
|
|
103316
102969
|
imageUrl: null,
|
|
103317
102970
|
success: false,
|
|
103318
|
-
error:
|
|
103319
|
-
approach:
|
|
103320
|
-
originalPrompt:
|
|
102971
|
+
error: err,
|
|
102972
|
+
approach: approachName,
|
|
102973
|
+
originalPrompt: prompt,
|
|
103321
102974
|
productImageUrl: productImage.url
|
|
103322
|
-
}
|
|
102975
|
+
};
|
|
103323
102976
|
}
|
|
103324
|
-
|
|
103325
|
-
|
|
103326
|
-
|
|
103327
|
-
|
|
103328
|
-
|
|
103329
|
-
|
|
103330
|
-
|
|
103331
|
-
const
|
|
103332
|
-
const
|
|
103333
|
-
// isRefusal и hasReasoning — случайные отказы модели → ретраим с логом
|
|
103334
|
-
// Всё остальное (сеть/JSON/таймаут/Provider) тоже ретраим
|
|
103335
|
-
return (isRefusal ||
|
|
103336
|
-
r.error?.hasReasoning === true ||
|
|
103337
|
-
errorMsg.includes('Invalid JSON response') ||
|
|
103338
|
-
errorMsg.includes('Unexpected end of JSON input') ||
|
|
103339
|
-
(errorMsg.includes('JSON') && errorMsg.includes('parse')) ||
|
|
103340
|
-
errorMsg.includes('Failed to fetch') ||
|
|
103341
|
-
errorMsg.includes('Network error') ||
|
|
103342
|
-
errorMsg.includes('timeout') ||
|
|
103343
|
-
errorMsg.includes('Request timeout') ||
|
|
103344
|
-
errorMsg.includes('Provider returned error') ||
|
|
103345
|
-
errorMsg.toLowerCase().includes('provider error'));
|
|
103346
|
-
});
|
|
103347
|
-
if (retryableErrors.length > 0) {
|
|
103348
|
-
const refusalCount = retryableErrors.filter(r => r.error?.isRefusal === true).length;
|
|
103349
|
-
const otherCount = retryableErrors.length - refusalCount;
|
|
103350
|
-
const parts = [];
|
|
103351
|
-
if (refusalCount > 0)
|
|
103352
|
-
parts.push(`${refusalCount} отказ${refusalCount > 1 ? 'а' : ''} модели`);
|
|
103353
|
-
if (otherCount > 0)
|
|
103354
|
-
parts.push(`${otherCount} сеть/таймаут/JSON`);
|
|
103355
|
-
addLog(formatLogMessage('log', `🔄 Повтор: ${parts.join(', ')}. Пробую ещё раз...`));
|
|
103356
|
-
const retryIndices = retryableErrors.map(r => r.index).sort((a, b) => a - b);
|
|
103357
|
-
let retryCursor = 0;
|
|
103358
|
-
const retryWorkers = Array.from({ length: Math.min(maxParallel, retryIndices.length) }, () => (async () => {
|
|
102977
|
+
finally {
|
|
102978
|
+
// Small yield to keep UI responsive in Electron
|
|
102979
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
102980
|
+
}
|
|
102981
|
+
};
|
|
102982
|
+
// First pass (limited concurrency)
|
|
102983
|
+
let cursor = 0;
|
|
102984
|
+
const workerCount = Math.min(maxParallel, imagePrompts.length);
|
|
102985
|
+
const workers = Array.from({ length: workerCount }, () => (async () => {
|
|
103359
102986
|
while (true) {
|
|
103360
|
-
const
|
|
103361
|
-
if (
|
|
102987
|
+
const i = cursor++;
|
|
102988
|
+
if (i >= imagePrompts.length)
|
|
103362
102989
|
break;
|
|
103363
|
-
|
|
103364
|
-
await new Promise(resolve => setTimeout(resolve, 1200));
|
|
103365
|
-
const i = idx - 1;
|
|
103366
|
-
const res = await runOne(i, true);
|
|
102990
|
+
const res = await runOne(i, false);
|
|
103367
102991
|
resultsMap.set(res.index, res);
|
|
103368
102992
|
}
|
|
103369
102993
|
})());
|
|
103370
|
-
await Promise.all(
|
|
103371
|
-
generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
|
|
103372
|
-
|
|
103373
|
-
|
|
103374
|
-
|
|
103375
|
-
|
|
103376
|
-
|
|
103377
|
-
|
|
103378
|
-
|
|
103379
|
-
|
|
103380
|
-
|
|
103381
|
-
|
|
103382
|
-
|
|
103383
|
-
|
|
103384
|
-
|
|
103385
|
-
addLog(formatLogMessage('log', `⏱️ Total time: ${overallDuration}ms (${(overallDuration / 1000).toFixed(2)}s)`));
|
|
103386
|
-
addLog(formatLogMessage('log', `✅ Successfully generated ${successfulImages.length}/${imagePrompts.length} images`));
|
|
103387
|
-
// Stop generating state to hide backdrop and show images
|
|
103388
|
-
setGeneratingImages(false);
|
|
103389
|
-
setCheckingImages(false);
|
|
103390
|
-
// Save generated images to Google Drive
|
|
103391
|
-
if (driveFolderUrl) {
|
|
103392
|
-
try {
|
|
103393
|
-
const folderId = extractFolderId(driveFolderUrl);
|
|
103394
|
-
if (folderId) {
|
|
103395
|
-
addLog(formatLogMessage('log', '💾 Saving generated images data to Google Drive...'));
|
|
103396
|
-
// Get latest state and save
|
|
103397
|
-
setGeneratedImagesData(prev => {
|
|
103398
|
-
// Save all images to separate files
|
|
103399
|
-
saveGeneratedImagesToDrive(folderId, prev).then(() => {
|
|
103400
|
-
addLog(formatLogMessage('log', '✅ Generated images data saved to Google Drive'));
|
|
103401
|
-
}).catch((err) => {
|
|
103402
|
-
addLog(formatLogMessage('warn', `⚠️ Failed to save images data to Google Drive: ${err.message || 'Unknown error'}`));
|
|
102994
|
+
await Promise.all(workers);
|
|
102995
|
+
let generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
|
|
102996
|
+
// Safety: fill any missing slots as failed
|
|
102997
|
+
if (generationResults.length !== imagePrompts.length) {
|
|
102998
|
+
for (let i = 0; i < imagePrompts.length; i++) {
|
|
102999
|
+
const idx = i + 1;
|
|
103000
|
+
if (!resultsMap.has(idx)) {
|
|
103001
|
+
generationResults.push({
|
|
103002
|
+
index: idx,
|
|
103003
|
+
imageUrl: null,
|
|
103004
|
+
success: false,
|
|
103005
|
+
error: new Error('Generation did not complete'),
|
|
103006
|
+
approach: approaches[i]?.name || 'Unknown',
|
|
103007
|
+
originalPrompt: imagePrompts[i],
|
|
103008
|
+
productImageUrl: productImage.url
|
|
103403
103009
|
});
|
|
103404
|
-
|
|
103405
|
-
});
|
|
103010
|
+
}
|
|
103406
103011
|
}
|
|
103012
|
+
generationResults = generationResults.sort((a, b) => a.index - b.index);
|
|
103407
103013
|
}
|
|
103408
|
-
|
|
103409
|
-
|
|
103014
|
+
// Retry once for all transient failures including refusals (model sometimes randomly refuses)
|
|
103015
|
+
const retryableErrors = generationResults.filter(r => {
|
|
103016
|
+
if (r.success)
|
|
103017
|
+
return false;
|
|
103018
|
+
const errorMsg = r.error?.message || '';
|
|
103019
|
+
const isRefusal = r.error?.isRefusal === true;
|
|
103020
|
+
// isRefusal и hasReasoning — случайные отказы модели → ретраим с логом
|
|
103021
|
+
// Всё остальное (сеть/JSON/таймаут/Provider) тоже ретраим
|
|
103022
|
+
return (isRefusal ||
|
|
103023
|
+
r.error?.hasReasoning === true ||
|
|
103024
|
+
errorMsg.includes('Invalid JSON response') ||
|
|
103025
|
+
errorMsg.includes('Unexpected end of JSON input') ||
|
|
103026
|
+
(errorMsg.includes('JSON') && errorMsg.includes('parse')) ||
|
|
103027
|
+
errorMsg.includes('Failed to fetch') ||
|
|
103028
|
+
errorMsg.includes('Network error') ||
|
|
103029
|
+
errorMsg.includes('timeout') ||
|
|
103030
|
+
errorMsg.includes('Request timeout') ||
|
|
103031
|
+
errorMsg.includes('Provider returned error') ||
|
|
103032
|
+
errorMsg.toLowerCase().includes('provider error'));
|
|
103033
|
+
});
|
|
103034
|
+
if (retryableErrors.length > 0) {
|
|
103035
|
+
const refusalCount = retryableErrors.filter(r => r.error?.isRefusal === true).length;
|
|
103036
|
+
const otherCount = retryableErrors.length - refusalCount;
|
|
103037
|
+
const parts = [];
|
|
103038
|
+
if (refusalCount > 0)
|
|
103039
|
+
parts.push(`${refusalCount} отказ${refusalCount > 1 ? 'а' : ''} модели`);
|
|
103040
|
+
if (otherCount > 0)
|
|
103041
|
+
parts.push(`${otherCount} сеть/таймаут/JSON`);
|
|
103042
|
+
addLog(formatLogMessage('log', `🔄 Повтор: ${parts.join(', ')}. Пробую ещё раз...`));
|
|
103043
|
+
const retryIndices = retryableErrors.map(r => r.index).sort((a, b) => a - b);
|
|
103044
|
+
let retryCursor = 0;
|
|
103045
|
+
const retryWorkers = Array.from({ length: Math.min(maxParallel, retryIndices.length) }, () => (async () => {
|
|
103046
|
+
while (true) {
|
|
103047
|
+
const idx = retryIndices[retryCursor++];
|
|
103048
|
+
if (idx === undefined)
|
|
103049
|
+
break;
|
|
103050
|
+
// backoff
|
|
103051
|
+
await new Promise(resolve => setTimeout(resolve, 1200));
|
|
103052
|
+
const i = idx - 1;
|
|
103053
|
+
const res = await runOne(i, true);
|
|
103054
|
+
resultsMap.set(res.index, res);
|
|
103055
|
+
}
|
|
103056
|
+
})());
|
|
103057
|
+
await Promise.all(retryWorkers);
|
|
103058
|
+
generationResults = Array.from(resultsMap.values()).sort((a, b) => a.index - b.index);
|
|
103059
|
+
}
|
|
103060
|
+
const generationDuration = Date.now() - generationStartTime;
|
|
103061
|
+
const successfulImages = generationResults.filter(r => r.success);
|
|
103062
|
+
const finalFailedImages = generationResults.filter(r => !r.success);
|
|
103063
|
+
addLog(formatLogMessage('log', `⏱️ Генерация завершена за ${Math.round(generationDuration / 1000)}s`));
|
|
103064
|
+
addLog(formatLogMessage('log', `✅ Успешно: ${successfulImages.length}/${imagePrompts.length}, ошибок: ${finalFailedImages.length}`));
|
|
103065
|
+
if (successfulImages.length === 0) {
|
|
103066
|
+
const allErrors = finalFailedImages.map(f => f.error?.message || String(f.error || 'Unknown error')).join('; ');
|
|
103067
|
+
addLog(formatLogMessage('warn', `⚠️ No images were successfully generated. All ${imagePrompts.length} slots are available for regeneration.`));
|
|
103068
|
+
// Don't throw error - just show empty slots with regenerate buttons
|
|
103410
103069
|
}
|
|
103070
|
+
const overallDuration = Date.now() - overallStartTime;
|
|
103071
|
+
addLog(formatLogMessage('log', `🎉 === Image generation process completed ===`));
|
|
103072
|
+
addLog(formatLogMessage('log', `⏱️ Total time: ${overallDuration}ms (${(overallDuration / 1000).toFixed(2)}s)`));
|
|
103073
|
+
addLog(formatLogMessage('log', `✅ Successfully generated ${successfulImages.length}/${imagePrompts.length} images`));
|
|
103074
|
+
// Stop generating state to hide backdrop and show images
|
|
103075
|
+
setGeneratingImages(false);
|
|
103076
|
+
setCheckingImages(false);
|
|
103077
|
+
// Validation is done individually per image; no final summary block
|
|
103078
|
+
}
|
|
103079
|
+
catch (err) {
|
|
103080
|
+
const errorMessage = err.message || 'Unknown error';
|
|
103081
|
+
addLog(formatLogMessage('error', '❌ === Error in image generation process ==='));
|
|
103082
|
+
addLog(formatLogMessage('error', errorMessage));
|
|
103083
|
+
alert('Error generating images: ' + errorMessage);
|
|
103084
|
+
}
|
|
103085
|
+
finally {
|
|
103086
|
+
setGeneratingImages(false);
|
|
103411
103087
|
}
|
|
103412
|
-
// Validation is done individually per image; no final summary block
|
|
103413
103088
|
}
|
|
103414
103089
|
catch (err) {
|
|
103415
|
-
const errorMessage = err.message || 'Unknown error';
|
|
103416
|
-
addLog(formatLogMessage('error', '❌ === Error in image generation process ==='));
|
|
103417
|
-
addLog(formatLogMessage('error', errorMessage));
|
|
103418
|
-
alert('Error generating images: ' + errorMessage);
|
|
103419
|
-
}
|
|
103420
|
-
finally {
|
|
103421
103090
|
setGeneratingImages(false);
|
|
103091
|
+
logToTerminal('error', '❌ Error:', err?.message || err);
|
|
103422
103092
|
}
|
|
103423
103093
|
};
|
|
103424
103094
|
const handleRegenerateImage = async (imageData) => {
|
|
@@ -103828,23 +103498,8 @@ ${imageData.originalPrompt}
|
|
|
103828
103498
|
addLog(formatLogMessage('log', `📤 Uploading image ${imageData.index} to Drive: ${filename}`));
|
|
103829
103499
|
const driveUrl = await uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog);
|
|
103830
103500
|
addLog(formatLogMessage('log', `✅ Image ${imageData.index} uploaded: ${driveUrl}`));
|
|
103831
|
-
// Update uploaded status
|
|
103832
|
-
setGeneratedImagesData(prev => {
|
|
103833
|
-
const updated = prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img);
|
|
103834
|
-
// Save single image to its own file
|
|
103835
|
-
if (driveFolderUrl) {
|
|
103836
|
-
const currentFolderId = extractFolderId(driveFolderUrl);
|
|
103837
|
-
if (currentFolderId) {
|
|
103838
|
-
const updatedImage = updated.find(img => img.index === imageData.index);
|
|
103839
|
-
if (updatedImage) {
|
|
103840
|
-
saveSingleImageToDrive(currentFolderId, updatedImage).catch((err) => {
|
|
103841
|
-
console.log(`Failed to save image ${imageData.index} to Drive:`, err);
|
|
103842
|
-
});
|
|
103843
|
-
}
|
|
103844
|
-
}
|
|
103845
|
-
}
|
|
103846
|
-
return updated;
|
|
103847
|
-
});
|
|
103501
|
+
// Update uploaded status
|
|
103502
|
+
setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img));
|
|
103848
103503
|
// (Removed legacy Generated Images links list)
|
|
103849
103504
|
}
|
|
103850
103505
|
catch (err) {
|
|
@@ -103867,7 +103522,7 @@ ${imageData.originalPrompt}
|
|
|
103867
103522
|
alert('Invalid Google Drive Folder URL');
|
|
103868
103523
|
return;
|
|
103869
103524
|
}
|
|
103870
|
-
const notUploaded = generatedImagesData.filter(img => !img.uploaded);
|
|
103525
|
+
const notUploaded = generatedImagesData.filter(img => !img.uploaded && img.imageUrl);
|
|
103871
103526
|
if (notUploaded.length === 0) {
|
|
103872
103527
|
alert('All images are already uploaded');
|
|
103873
103528
|
return;
|
|
@@ -103877,57 +103532,53 @@ ${imageData.originalPrompt}
|
|
|
103877
103532
|
setImagesGenerationLogs(prev => [...prev, msg]);
|
|
103878
103533
|
logToTerminal('log', msg.replace(/\[.*?\]\s*/, ''));
|
|
103879
103534
|
};
|
|
103535
|
+
// Mark all as uploading
|
|
103536
|
+
setGeneratedImagesData(prev => prev.map(img => notUploaded.some(n => n.index === img.index)
|
|
103537
|
+
? { ...img, uploading: true, uploadStartTime: Date.now() }
|
|
103538
|
+
: img));
|
|
103880
103539
|
try {
|
|
103881
|
-
addLog(formatLogMessage('log', `📤 Uploading ${notUploaded.length} image(s) to Drive...`));
|
|
103882
|
-
|
|
103883
|
-
|
|
103884
|
-
|
|
103885
|
-
|
|
103886
|
-
|
|
103540
|
+
addLog(formatLogMessage('log', `📤 Uploading ${notUploaded.length} image(s) to Drive (parallel)...`));
|
|
103541
|
+
const baseTs = Date.now();
|
|
103542
|
+
const results = await Promise.allSettled(notUploaded.map((imageData, i) => {
|
|
103543
|
+
const filename = `${generateProduct.replace(/\s+/g, '_')}_${imageData.index}_${baseTs}_${i}.png`;
|
|
103544
|
+
addLog(formatLogMessage('log', `📤 Starting upload image ${imageData.index}: ${filename}`));
|
|
103545
|
+
return uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog).then(driveUrl => ({
|
|
103546
|
+
index: imageData.index,
|
|
103547
|
+
driveUrl,
|
|
103548
|
+
success: true
|
|
103549
|
+
}));
|
|
103550
|
+
}));
|
|
103551
|
+
const successfulIndices = new Set();
|
|
103552
|
+
results.forEach((result, i) => {
|
|
103553
|
+
const imageData = notUploaded[i];
|
|
103554
|
+
if (result.status === 'fulfilled' && result.value.success) {
|
|
103555
|
+
addLog(formatLogMessage('log', `✅ Image ${imageData.index} uploaded: ${result.value.driveUrl}`));
|
|
103556
|
+
successfulIndices.add(imageData.index);
|
|
103887
103557
|
}
|
|
103888
|
-
|
|
103889
|
-
const
|
|
103890
|
-
addLog(formatLogMessage('
|
|
103891
|
-
const driveUrl = await uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog);
|
|
103892
|
-
addLog(formatLogMessage('log', `✅ Image ${imageData.index} uploaded: ${driveUrl}`));
|
|
103893
|
-
// Update uploaded status
|
|
103894
|
-
setGeneratedImagesData(prev => {
|
|
103895
|
-
const updated = prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img);
|
|
103896
|
-
// Save to Google Drive after upload
|
|
103897
|
-
saveGeneratedImagesToDrive(folderId, updated).catch((err) => {
|
|
103898
|
-
console.log('Failed to save images data after upload:', err);
|
|
103899
|
-
});
|
|
103900
|
-
return updated;
|
|
103901
|
-
});
|
|
103902
|
-
// (Removed legacy Generated Images links list)
|
|
103903
|
-
uploadedCount++;
|
|
103558
|
+
else {
|
|
103559
|
+
const errMsg = result.status === 'rejected' ? result.reason?.message : 'Unknown error';
|
|
103560
|
+
addLog(formatLogMessage('error', `❌ Failed to upload image ${imageData.index}:`, errMsg));
|
|
103904
103561
|
}
|
|
103905
|
-
|
|
103906
|
-
|
|
103562
|
+
});
|
|
103563
|
+
const uploadedCount = successfulIndices.size;
|
|
103564
|
+
// Update uploaded status and clear uploading state for all
|
|
103565
|
+
setGeneratedImagesData(prev => prev.map(img => notUploaded.some(n => n.index === img.index)
|
|
103566
|
+
? {
|
|
103567
|
+
...img,
|
|
103568
|
+
uploaded: successfulIndices.has(img.index),
|
|
103569
|
+
uploading: false,
|
|
103570
|
+
uploadStartTime: undefined
|
|
103907
103571
|
}
|
|
103908
|
-
|
|
103909
|
-
addLog(formatLogMessage('log', `✅ Uploaded ${uploadedCount} image(s) successfully`));
|
|
103910
|
-
|
|
103911
|
-
try {
|
|
103912
|
-
addLog(formatLogMessage('log', '💾 Saving images data to Google Drive...'));
|
|
103913
|
-
// Get latest state and save all images
|
|
103914
|
-
setGeneratedImagesData(prev => {
|
|
103915
|
-
saveGeneratedImagesToDrive(folderId, prev).then(() => {
|
|
103916
|
-
addLog(formatLogMessage('log', '✅ Images data saved to Google Drive'));
|
|
103917
|
-
}).catch((err) => {
|
|
103918
|
-
addLog(formatLogMessage('warn', `⚠️ Failed to save images data to Google Drive: ${err.message || 'Unknown error'}`));
|
|
103919
|
-
});
|
|
103920
|
-
return prev;
|
|
103921
|
-
});
|
|
103922
|
-
}
|
|
103923
|
-
catch (err) {
|
|
103924
|
-
addLog(formatLogMessage('warn', `⚠️ Failed to save images data to Google Drive: ${err.message || 'Unknown error'}`));
|
|
103925
|
-
}
|
|
103926
|
-
alert(`Successfully uploaded ${uploadedCount} image(s) to Google Drive!`);
|
|
103572
|
+
: img));
|
|
103573
|
+
addLog(formatLogMessage('log', `✅ Uploaded ${uploadedCount}/${notUploaded.length} image(s) successfully`));
|
|
103574
|
+
alert(`Successfully uploaded ${uploadedCount} of ${notUploaded.length} image(s) to Google Drive!`);
|
|
103927
103575
|
}
|
|
103928
103576
|
catch (err) {
|
|
103929
103577
|
addLog(formatLogMessage('error', `❌ Error uploading images:`, err.message));
|
|
103930
103578
|
alert('Error uploading images: ' + err.message);
|
|
103579
|
+
setGeneratedImagesData(prev => prev.map(img => notUploaded.some(n => n.index === img.index)
|
|
103580
|
+
? { ...img, uploading: false, uploadStartTime: undefined }
|
|
103581
|
+
: img));
|
|
103931
103582
|
}
|
|
103932
103583
|
finally {
|
|
103933
103584
|
setUploadingImages(false);
|
|
@@ -104006,41 +103657,13 @@ ${imageData.originalPrompt}
|
|
|
104006
103657
|
throw new Error(err.error?.message || 'Failed to fetch Drive files');
|
|
104007
103658
|
}
|
|
104008
103659
|
const data = await response.json();
|
|
104009
|
-
// Filter out
|
|
103660
|
+
// Filter out product.png/jpg (used for product reference, not creative images)
|
|
104010
103661
|
const filteredFiles = data.files.filter((f) => {
|
|
104011
103662
|
const name = f.name?.toLowerCase() || '';
|
|
104012
|
-
return
|
|
103663
|
+
return name !== 'product.png' && name !== 'product.jpg';
|
|
104013
103664
|
});
|
|
104014
103665
|
return filteredFiles.map((f) => f.id ? `https://drive.google.com/file/d/${f.id}/view?usp=sharing` : '');
|
|
104015
103666
|
};
|
|
104016
|
-
const fetchBankaImages = async (folderId) => {
|
|
104017
|
-
const validToken = await getValidAccessToken();
|
|
104018
|
-
if (!validToken)
|
|
104019
|
-
throw new Error('Not logged in');
|
|
104020
|
-
const q = `'${folderId}' in parents and trashed = false and mimeType contains 'image/'`;
|
|
104021
|
-
const fields = 'files(id, name)';
|
|
104022
|
-
const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent(fields)}`;
|
|
104023
|
-
const response = await fetch(url, {
|
|
104024
|
-
headers: {
|
|
104025
|
-
'Authorization': `Bearer ${validToken}`
|
|
104026
|
-
}
|
|
104027
|
-
});
|
|
104028
|
-
if (!response.ok) {
|
|
104029
|
-
const err = await response.json();
|
|
104030
|
-
throw new Error(err.error?.message || 'Failed to fetch Drive files');
|
|
104031
|
-
}
|
|
104032
|
-
const data = await response.json();
|
|
104033
|
-
// Filter only banka* files (case insensitive)
|
|
104034
|
-
const bankaFiles = data.files.filter((f) => {
|
|
104035
|
-
const name = f.name?.toLowerCase() || '';
|
|
104036
|
-
return name.startsWith('banka') && (name.endsWith('.png') || name.endsWith('.jpg') || name.endsWith('.jpeg'));
|
|
104037
|
-
});
|
|
104038
|
-
return bankaFiles.map((f) => ({
|
|
104039
|
-
id: f.id,
|
|
104040
|
-
name: f.name,
|
|
104041
|
-
url: `https://drive.google.com/file/d/${f.id}/view?usp=sharing`
|
|
104042
|
-
}));
|
|
104043
|
-
};
|
|
104044
103667
|
const fetchProductImage = async (folderId) => {
|
|
104045
103668
|
const validToken = await getValidAccessToken();
|
|
104046
103669
|
if (!validToken)
|
|
@@ -104197,11 +103820,9 @@ ${imageData.originalPrompt}
|
|
|
104197
103820
|
errorMessage: item.errorMessage
|
|
104198
103821
|
}));
|
|
104199
103822
|
};
|
|
104200
|
-
// Save
|
|
104201
|
-
const saveGeneratedContentToDrive = async (folderId,
|
|
104202
|
-
|
|
104203
|
-
const sanitizedTextsData = sanitizeGeneratedTextsData(textsData);
|
|
104204
|
-
logToTerminal('log', '[Save Content] Saving content to folderId:', folderId, 'titles:', sanitizedTitlesData.length, 'texts:', sanitizedTextsData.length);
|
|
103823
|
+
// Save settings, brand, link to Google Drive (titles and texts are not saved)
|
|
103824
|
+
const saveGeneratedContentToDrive = async (folderId, aiSettings, brandValue, linkValue) => {
|
|
103825
|
+
logToTerminal('log', '[Save Content] Saving settings to folderId:', folderId);
|
|
104205
103826
|
const validToken = await getValidAccessToken();
|
|
104206
103827
|
if (!validToken) {
|
|
104207
103828
|
logToTerminal('error', '[Save Content] No valid token');
|
|
@@ -104210,8 +103831,6 @@ ${imageData.originalPrompt}
|
|
|
104210
103831
|
// Get or create 'env' folder
|
|
104211
103832
|
const envFolderId = await getOrCreateEnvFolder(folderId);
|
|
104212
103833
|
const dataToSave = {
|
|
104213
|
-
generatedTitlesData: sanitizedTitlesData,
|
|
104214
|
-
generatedTextsData: sanitizedTextsData,
|
|
104215
103834
|
aiGenerationSettings: aiSettings || {
|
|
104216
103835
|
generateProduct: generateProduct || '',
|
|
104217
103836
|
generateGeo: generateGeo || '',
|
|
@@ -104364,38 +103983,8 @@ ${imageData.originalPrompt}
|
|
|
104364
103983
|
logToTerminal('log', '[Load Content] File downloaded, length:', jsonContent.length);
|
|
104365
103984
|
try {
|
|
104366
103985
|
const loadedData = JSON.parse(jsonContent);
|
|
104367
|
-
logToTerminal('log', '[Load Content] Parsed
|
|
104368
|
-
|
|
104369
|
-
const sanitizedTitlesData = sanitizeGeneratedTitlesData(loadedData.generatedTitlesData);
|
|
104370
|
-
logToTerminal('log', '[Load Content] Loading titles, count:', sanitizedTitlesData.length);
|
|
104371
|
-
setGeneratedTitlesData(sanitizedTitlesData);
|
|
104372
|
-
// Also update titles field
|
|
104373
|
-
const titlesString = sanitizedTitlesData
|
|
104374
|
-
.map((t) => t.title)
|
|
104375
|
-
.filter((t) => t)
|
|
104376
|
-
.join('\n');
|
|
104377
|
-
if (titlesString) {
|
|
104378
|
-
setTitles(titlesString);
|
|
104379
|
-
}
|
|
104380
|
-
}
|
|
104381
|
-
else {
|
|
104382
|
-
setGeneratedTitlesData([]);
|
|
104383
|
-
}
|
|
104384
|
-
if (loadedData.generatedTextsData && Array.isArray(loadedData.generatedTextsData)) {
|
|
104385
|
-
const sanitizedTextsData = sanitizeGeneratedTextsData(loadedData.generatedTextsData);
|
|
104386
|
-
logToTerminal('log', '[Load Content] Loading texts, count:', sanitizedTextsData.length);
|
|
104387
|
-
setGeneratedTextsData(sanitizedTextsData);
|
|
104388
|
-
// Also update texts field
|
|
104389
|
-
const textsArray = sanitizedTextsData
|
|
104390
|
-
.map((t) => t.text)
|
|
104391
|
-
.filter((t) => t);
|
|
104392
|
-
if (textsArray.length > 0) {
|
|
104393
|
-
setTexts(textsArray);
|
|
104394
|
-
}
|
|
104395
|
-
}
|
|
104396
|
-
else {
|
|
104397
|
-
setGeneratedTextsData([]);
|
|
104398
|
-
}
|
|
103986
|
+
logToTerminal('log', '[Load Content] Parsed settings');
|
|
103987
|
+
// Titles and texts are not loaded from Drive (no longer saved there)
|
|
104399
103988
|
// Load AI Generation Settings
|
|
104400
103989
|
if (loadedData.aiGenerationSettings) {
|
|
104401
103990
|
logToTerminal('log', '[Load Content] Loading AI Generation Settings');
|
|
@@ -104417,11 +104006,7 @@ ${imageData.originalPrompt}
|
|
|
104417
104006
|
logToTerminal('log', '[Load Content] Loaded generatePriceWithCurrency:', settings.generatePriceWithCurrency);
|
|
104418
104007
|
}
|
|
104419
104008
|
}
|
|
104420
|
-
// Load brand
|
|
104421
|
-
if (loadedData.brand !== undefined) {
|
|
104422
|
-
setBrand(loadedData.brand || '');
|
|
104423
|
-
logToTerminal('log', '[Load Content] Loaded brand:', loadedData.brand || '(empty)');
|
|
104424
|
-
}
|
|
104009
|
+
// Load link (brand is auto-generated from product, geo, price)
|
|
104425
104010
|
if (loadedData.link !== undefined) {
|
|
104426
104011
|
setLink(loadedData.link || '');
|
|
104427
104012
|
logToTerminal('log', '[Load Content] Loaded link:', loadedData.link || '(empty)');
|
|
@@ -104436,250 +104021,7 @@ ${imageData.originalPrompt}
|
|
|
104436
104021
|
return { found: false };
|
|
104437
104022
|
}
|
|
104438
104023
|
};
|
|
104439
|
-
// Save single image to Google Drive as separate file
|
|
104440
|
-
const saveSingleImageToDrive = async (folderId, imageData) => {
|
|
104441
|
-
logToTerminal('log', '[Save Image] Saving image', imageData.index, 'to folderId:', folderId);
|
|
104442
|
-
const validToken = await getValidAccessToken();
|
|
104443
|
-
if (!validToken) {
|
|
104444
|
-
logToTerminal('error', '[Save Image] No valid token');
|
|
104445
|
-
throw new Error('Not logged in to Google Drive');
|
|
104446
|
-
}
|
|
104447
|
-
// Get or create 'env' folder
|
|
104448
|
-
const envFolderId = await getOrCreateEnvFolder(folderId);
|
|
104449
|
-
const jsonContent = JSON.stringify(imageData, null, 2);
|
|
104450
|
-
const filename = `temp-image-${imageData.index}.json`;
|
|
104451
|
-
logToTerminal('log', '[Save Image] Filename:', filename, 'content length:', jsonContent.length);
|
|
104452
|
-
// Check if file already exists in 'env' folder
|
|
104453
|
-
const q = `'${envFolderId}' in parents and trashed = false and name = '${filename}'`;
|
|
104454
|
-
const fields = 'files(id, name)';
|
|
104455
|
-
const searchUrl = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent(fields)}`;
|
|
104456
|
-
const searchResponse = await fetch(searchUrl, {
|
|
104457
|
-
headers: {
|
|
104458
|
-
'Authorization': `Bearer ${validToken}`
|
|
104459
|
-
}
|
|
104460
|
-
});
|
|
104461
|
-
let fileId = null;
|
|
104462
|
-
if (searchResponse.ok) {
|
|
104463
|
-
const searchData = await searchResponse.json();
|
|
104464
|
-
logToTerminal('log', '[Save Image] Search response, files found:', searchData.files?.length || 0);
|
|
104465
|
-
if (searchData.files && searchData.files.length > 0) {
|
|
104466
|
-
// Filter out duplicate files (with (1), (2), etc.) - prefer exact match
|
|
104467
|
-
const exactMatch = searchData.files.find((f) => f.name === filename);
|
|
104468
|
-
if (exactMatch) {
|
|
104469
|
-
fileId = exactMatch.id;
|
|
104470
|
-
logToTerminal('log', '[Save Image] Found exact match, will update, fileId:', fileId);
|
|
104471
|
-
}
|
|
104472
|
-
else {
|
|
104473
|
-
// If no exact match, use first file but log warning
|
|
104474
|
-
fileId = searchData.files[0].id;
|
|
104475
|
-
logToTerminal('warn', '[Save Image] Found duplicate file, will update first one, fileId:', fileId, 'name:', searchData.files[0].name);
|
|
104476
|
-
}
|
|
104477
|
-
}
|
|
104478
|
-
else {
|
|
104479
|
-
logToTerminal('log', '[Save Image] File does not exist, will create new');
|
|
104480
|
-
}
|
|
104481
|
-
}
|
|
104482
|
-
else {
|
|
104483
|
-
logToTerminal('warn', '[Save Image] Search failed, status:', searchResponse.status);
|
|
104484
|
-
}
|
|
104485
|
-
if (fileId) {
|
|
104486
|
-
// Update existing file - don't include parents in metadata for updates
|
|
104487
|
-
logToTerminal('log', '[Save Image] Updating existing file');
|
|
104488
|
-
const updateMetadata = {
|
|
104489
|
-
name: filename,
|
|
104490
|
-
mimeType: 'application/json'
|
|
104491
|
-
};
|
|
104492
|
-
const updateForm = new FormData();
|
|
104493
|
-
updateForm.append('metadata', new Blob([JSON.stringify(updateMetadata)], { type: 'application/json' }));
|
|
104494
|
-
updateForm.append('file', new Blob([jsonContent], { type: 'application/json' }));
|
|
104495
|
-
const updateUrl = `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`;
|
|
104496
|
-
const updateResponse = await fetch(updateUrl, {
|
|
104497
|
-
method: 'PATCH',
|
|
104498
|
-
headers: {
|
|
104499
|
-
'Authorization': `Bearer ${validToken}`
|
|
104500
|
-
},
|
|
104501
|
-
body: updateForm
|
|
104502
|
-
});
|
|
104503
|
-
if (!updateResponse.ok) {
|
|
104504
|
-
const err = await updateResponse.json();
|
|
104505
|
-
logToTerminal('error', '[Save Image] Update failed:', JSON.stringify(err));
|
|
104506
|
-
throw new Error(err.error?.message || 'Failed to update file in Google Drive');
|
|
104507
|
-
}
|
|
104508
|
-
logToTerminal('log', '[Save Image] Successfully updated file');
|
|
104509
|
-
}
|
|
104510
|
-
else {
|
|
104511
|
-
// Create new file - include parents for new files (in 'env' folder)
|
|
104512
|
-
logToTerminal('log', '[Save Image] Creating new file in env folder');
|
|
104513
|
-
const createMetadata = {
|
|
104514
|
-
name: filename,
|
|
104515
|
-
mimeType: 'application/json',
|
|
104516
|
-
parents: [envFolderId]
|
|
104517
|
-
};
|
|
104518
|
-
const createForm = new FormData();
|
|
104519
|
-
createForm.append('metadata', new Blob([JSON.stringify(createMetadata)], { type: 'application/json' }));
|
|
104520
|
-
createForm.append('file', new Blob([jsonContent], { type: 'application/json' }));
|
|
104521
|
-
const createUrl = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart';
|
|
104522
|
-
const createResponse = await fetch(createUrl, {
|
|
104523
|
-
method: 'POST',
|
|
104524
|
-
headers: {
|
|
104525
|
-
'Authorization': `Bearer ${validToken}`
|
|
104526
|
-
},
|
|
104527
|
-
body: createForm
|
|
104528
|
-
});
|
|
104529
|
-
if (!createResponse.ok) {
|
|
104530
|
-
const err = await createResponse.json();
|
|
104531
|
-
logToTerminal('error', '[Save Image] Create failed:', JSON.stringify(err));
|
|
104532
|
-
throw new Error(err.error?.message || 'Failed to create file in Google Drive');
|
|
104533
|
-
}
|
|
104534
|
-
const result = await createResponse.json();
|
|
104535
|
-
logToTerminal('log', '[Save Image] Successfully created file, id:', result.id);
|
|
104536
|
-
}
|
|
104537
|
-
};
|
|
104538
|
-
// Save all images to Google Drive (each in separate file)
|
|
104539
|
-
const saveGeneratedImagesToDrive = async (folderId, imagesData) => {
|
|
104540
|
-
const validToken = await getValidAccessToken();
|
|
104541
|
-
if (!validToken) {
|
|
104542
|
-
throw new Error('Not logged in to Google Drive');
|
|
104543
|
-
}
|
|
104544
|
-
// Save each image to its own file
|
|
104545
|
-
for (const imageData of imagesData) {
|
|
104546
|
-
try {
|
|
104547
|
-
await saveSingleImageToDrive(folderId, imageData);
|
|
104548
|
-
}
|
|
104549
|
-
catch (err) {
|
|
104550
|
-
console.error(`Failed to save image ${imageData.index} to Drive:`, err);
|
|
104551
|
-
// Continue with other images even if one fails
|
|
104552
|
-
}
|
|
104553
|
-
}
|
|
104554
|
-
};
|
|
104555
|
-
// Load generated images data from Google Drive (from separate files)
|
|
104556
|
-
const loadGeneratedImagesFromDrive = async (folderId) => {
|
|
104557
|
-
logToTerminal('log', '[Load Images] Starting load from Google Drive, folderId:', folderId);
|
|
104558
|
-
const validToken = await getValidAccessToken();
|
|
104559
|
-
if (!validToken) {
|
|
104560
|
-
logToTerminal('warn', '[Load Images] No valid token, skipping load');
|
|
104561
|
-
setDriveFilesFound(prev => ({ ...prev, images: false }));
|
|
104562
|
-
return { found: false };
|
|
104563
|
-
}
|
|
104564
|
-
// Get 'env' folder
|
|
104565
|
-
let envFolderId;
|
|
104566
|
-
try {
|
|
104567
|
-
envFolderId = await getOrCreateEnvFolder(folderId);
|
|
104568
|
-
}
|
|
104569
|
-
catch (err) {
|
|
104570
|
-
logToTerminal('warn', '[Load Images] Failed to get env folder:', err.message || 'Unknown error');
|
|
104571
|
-
return { found: false };
|
|
104572
|
-
}
|
|
104573
|
-
// Find all temp-image-*.json files in 'env' folder
|
|
104574
|
-
const q = `'${envFolderId}' in parents and trashed = false and name contains 'temp-image-'`;
|
|
104575
|
-
const fields = 'files(id, name)';
|
|
104576
|
-
const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent(fields)}`;
|
|
104577
|
-
logToTerminal('log', '[Load Images] Searching for image files');
|
|
104578
|
-
const response = await fetch(url, {
|
|
104579
|
-
headers: {
|
|
104580
|
-
'Authorization': `Bearer ${validToken}`
|
|
104581
|
-
}
|
|
104582
|
-
});
|
|
104583
|
-
if (!response.ok) {
|
|
104584
|
-
const errorText = await response.text();
|
|
104585
|
-
logToTerminal('warn', '[Load Images] Search failed, status:', response.status, 'error:', errorText);
|
|
104586
|
-
setDriveFilesFound(prev => ({ ...prev, images: false }));
|
|
104587
|
-
return { found: false };
|
|
104588
|
-
}
|
|
104589
|
-
const data = await response.json();
|
|
104590
|
-
logToTerminal('log', '[Load Images] Search response, files found:', data.files?.length || 0);
|
|
104591
|
-
if (!data.files || data.files.length === 0) {
|
|
104592
|
-
logToTerminal('log', '[Load Images] No image files found - no temp-image-*.json files in folder');
|
|
104593
|
-
return { found: false };
|
|
104594
|
-
}
|
|
104595
|
-
// Filter out duplicate files (with (1), (2), etc.) - only keep exact matches
|
|
104596
|
-
const exactMatchFiles = data.files.filter((f) => {
|
|
104597
|
-
const match = f.name.match(/^temp-image-(\d+)\.json$/);
|
|
104598
|
-
return match !== null;
|
|
104599
|
-
});
|
|
104600
|
-
logToTerminal('log', '[Load Images] Found', exactMatchFiles.length, 'exact match files:', exactMatchFiles.map((f) => f.name).join(', '));
|
|
104601
|
-
// Group files by index and take only one file per index (prefer exact match, then first one)
|
|
104602
|
-
const filesByIndex = new Map();
|
|
104603
|
-
for (const file of exactMatchFiles) {
|
|
104604
|
-
const match = file.name.match(/^temp-image-(\d+)\.json$/);
|
|
104605
|
-
if (match) {
|
|
104606
|
-
const index = parseInt(match[1], 10);
|
|
104607
|
-
if (!filesByIndex.has(index)) {
|
|
104608
|
-
filesByIndex.set(index, file);
|
|
104609
|
-
}
|
|
104610
|
-
}
|
|
104611
|
-
}
|
|
104612
|
-
const uniqueFiles = Array.from(filesByIndex.values());
|
|
104613
|
-
logToTerminal('log', '[Load Images] After deduplication:', uniqueFiles.length, 'unique files, indices:', Array.from(filesByIndex.keys()).sort((a, b) => a - b).join(', '));
|
|
104614
|
-
// Helper function to download file with retries
|
|
104615
|
-
const downloadFileWithRetry = async (file, maxRetries = 3, delayMs = 1000) => {
|
|
104616
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
104617
|
-
try {
|
|
104618
|
-
if (attempt > 1) {
|
|
104619
|
-
logToTerminal('log', `[Load Images] Retry ${attempt}/${maxRetries} for file:`, file.name);
|
|
104620
|
-
await new Promise(resolve => setTimeout(resolve, delayMs * (attempt - 1))); // Exponential backoff
|
|
104621
|
-
}
|
|
104622
|
-
else {
|
|
104623
|
-
logToTerminal('log', '[Load Images] Loading file:', file.name);
|
|
104624
|
-
}
|
|
104625
|
-
const downloadUrl = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`;
|
|
104626
|
-
const downloadResponse = await fetch(downloadUrl, {
|
|
104627
|
-
headers: {
|
|
104628
|
-
'Authorization': `Bearer ${validToken}`
|
|
104629
|
-
}
|
|
104630
|
-
});
|
|
104631
|
-
if (downloadResponse.ok) {
|
|
104632
|
-
const jsonContent = await downloadResponse.text();
|
|
104633
|
-
logToTerminal('log', '[Load Images] File', file.name, 'downloaded, length:', jsonContent.length);
|
|
104634
|
-
const imageData = JSON.parse(jsonContent);
|
|
104635
|
-
logToTerminal('log', '[Load Images] Parsed image data for index:', imageData.index);
|
|
104636
|
-
return imageData;
|
|
104637
|
-
}
|
|
104638
|
-
else {
|
|
104639
|
-
const errorText = await downloadResponse.text();
|
|
104640
|
-
logToTerminal('warn', `[Load Images] Download failed for ${file.name} (attempt ${attempt}/${maxRetries}), status:`, downloadResponse.status);
|
|
104641
|
-
if (attempt === maxRetries) {
|
|
104642
|
-
logToTerminal('error', `[Load Images] Failed to load ${file.name} after ${maxRetries} attempts`);
|
|
104643
|
-
return null;
|
|
104644
|
-
}
|
|
104645
|
-
}
|
|
104646
|
-
}
|
|
104647
|
-
catch (err) {
|
|
104648
|
-
logToTerminal('error', `[Load Images] Failed to load image file ${file.name} (attempt ${attempt}/${maxRetries}):`, err);
|
|
104649
|
-
if (attempt === maxRetries) {
|
|
104650
|
-
logToTerminal('error', `[Load Images] Failed to load ${file.name} after ${maxRetries} attempts`);
|
|
104651
|
-
return null;
|
|
104652
|
-
}
|
|
104653
|
-
}
|
|
104654
|
-
}
|
|
104655
|
-
return null;
|
|
104656
|
-
};
|
|
104657
|
-
// Load all image files in parallel
|
|
104658
|
-
logToTerminal('log', '[Load Images] Starting parallel download of', uniqueFiles.length, 'files');
|
|
104659
|
-
const downloadPromises = uniqueFiles.map(file => downloadFileWithRetry(file));
|
|
104660
|
-
// Wait for all downloads to complete
|
|
104661
|
-
const results = await Promise.all(downloadPromises);
|
|
104662
|
-
const imagesData = results.filter((data) => data !== null);
|
|
104663
|
-
// Sort by index and update state
|
|
104664
|
-
if (imagesData.length > 0) {
|
|
104665
|
-
imagesData.sort((a, b) => a.index - b.index);
|
|
104666
|
-
logToTerminal('log', '[Load Images] Successfully loaded', imagesData.length, 'images, indices:', imagesData.map(img => img.index).join(', '));
|
|
104667
|
-
setGeneratedImagesData(imagesData);
|
|
104668
|
-
setDriveFilesFound(prev => ({ ...prev, images: false }));
|
|
104669
|
-
return { found: true };
|
|
104670
|
-
}
|
|
104671
|
-
else {
|
|
104672
|
-
logToTerminal('log', '[Load Images] No images loaded');
|
|
104673
|
-
setDriveFilesFound(prev => ({ ...prev, images: false }));
|
|
104674
|
-
return { found: false };
|
|
104675
|
-
}
|
|
104676
|
-
};
|
|
104677
104024
|
const handleGenerate = async () => {
|
|
104678
|
-
const validToken = await getValidAccessToken();
|
|
104679
|
-
if (!validToken) {
|
|
104680
|
-
alert('Please log in with Google first');
|
|
104681
|
-
return;
|
|
104682
|
-
}
|
|
104683
104025
|
if (!driveFolderUrl || !brand || !link) {
|
|
104684
104026
|
alert('Please fill all required fields');
|
|
104685
104027
|
return;
|
|
@@ -104691,18 +104033,28 @@ ${imageData.originalPrompt}
|
|
|
104691
104033
|
alert('Invalid Link URL');
|
|
104692
104034
|
return;
|
|
104693
104035
|
}
|
|
104694
|
-
const titleList = titles.split('\n').map(t => t.trim()).filter(t => t)
|
|
104036
|
+
const titleList = titles.split('\n').map(t => t.trim()).filter(t => t).length > 0
|
|
104037
|
+
? titles.split('\n').map(t => t.trim()).filter(t => t)
|
|
104038
|
+
: generatedTitlesData.map(t => t.title).filter(Boolean);
|
|
104695
104039
|
if (titleList.length === 0) {
|
|
104696
104040
|
alert('Please add at least one title');
|
|
104697
104041
|
return;
|
|
104698
104042
|
}
|
|
104699
|
-
const textList = texts.map(t => t.trim()).filter(t => t)
|
|
104043
|
+
const textList = texts.map(t => t.trim()).filter(t => t).length > 0
|
|
104044
|
+
? texts.map(t => t.trim()).filter(t => t)
|
|
104045
|
+
: generatedTextsData.map(t => t.text).filter(Boolean);
|
|
104700
104046
|
if (textList.length === 0) {
|
|
104701
104047
|
alert('Please add at least one text variant');
|
|
104702
104048
|
return;
|
|
104703
104049
|
}
|
|
104704
104050
|
setLoading(true);
|
|
104705
104051
|
try {
|
|
104052
|
+
const validToken = await getValidAccessToken();
|
|
104053
|
+
if (!validToken) {
|
|
104054
|
+
alert('Please log in with Google first');
|
|
104055
|
+
setLoading(false);
|
|
104056
|
+
return;
|
|
104057
|
+
}
|
|
104706
104058
|
const folderId = extractFolderId(driveFolderUrl);
|
|
104707
104059
|
if (!folderId) {
|
|
104708
104060
|
throw new Error('Invalid Google Drive Folder URL');
|
|
@@ -104739,8 +104091,8 @@ ${imageData.originalPrompt}
|
|
|
104739
104091
|
}
|
|
104740
104092
|
setGeneratedData(rows);
|
|
104741
104093
|
// Create workbook
|
|
104742
|
-
const wb =
|
|
104743
|
-
const ws =
|
|
104094
|
+
const wb = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_new();
|
|
104095
|
+
const ws = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.aoa_to_sheet(rows);
|
|
104744
104096
|
// Set column widths (approximate pixel width / 7)
|
|
104745
104097
|
ws['!cols'] = [
|
|
104746
104098
|
{ wch: 20 }, // id
|
|
@@ -104753,10 +104105,10 @@ ${imageData.originalPrompt}
|
|
|
104753
104105
|
{ wch: 40 }, // image_link
|
|
104754
104106
|
{ wch: 20 } // brand
|
|
104755
104107
|
];
|
|
104756
|
-
|
|
104108
|
+
xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_append_sheet(wb, ws, "Products");
|
|
104757
104109
|
// Generate buffer
|
|
104758
|
-
const wbout =
|
|
104759
|
-
// Upload to Drive
|
|
104110
|
+
const wbout = xlsx__WEBPACK_IMPORTED_MODULE_50__.write(wb, { bookType: 'xlsx', type: 'array' });
|
|
104111
|
+
// Upload to Drive (имя файла по бренду)
|
|
104760
104112
|
const dateStr = new Date().toISOString().split('T')[0];
|
|
104761
104113
|
const fileName = `${brand}-${dateStr}.xlsx`;
|
|
104762
104114
|
const result = await uploadFileToDrive(wbout, fileName, folderId || undefined);
|
|
@@ -104877,13 +104229,13 @@ ${imageData.originalPrompt}
|
|
|
104877
104229
|
setTestLoading(true);
|
|
104878
104230
|
try {
|
|
104879
104231
|
// Create simple test workbook with structure
|
|
104880
|
-
const wb =
|
|
104232
|
+
const wb = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_new();
|
|
104881
104233
|
const rows = [
|
|
104882
104234
|
INSTRUCTION_ROW,
|
|
104883
104235
|
['id', 'title', 'description', 'availability', 'condition', 'price', 'link', 'image_link', 'brand'],
|
|
104884
104236
|
['test1', 'Test Title', 'Test Description', 'in stock', 'new', '10.00 USD', 'http://test.com', 'http://test.com/img.jpg', 'TestBrand']
|
|
104885
104237
|
];
|
|
104886
|
-
const ws =
|
|
104238
|
+
const ws = xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.aoa_to_sheet(rows);
|
|
104887
104239
|
// Set column widths
|
|
104888
104240
|
ws['!cols'] = [
|
|
104889
104241
|
{ wch: 20 }, // id
|
|
@@ -104896,8 +104248,8 @@ ${imageData.originalPrompt}
|
|
|
104896
104248
|
{ wch: 40 }, // image_link
|
|
104897
104249
|
{ wch: 20 } // brand
|
|
104898
104250
|
];
|
|
104899
|
-
|
|
104900
|
-
const wbout =
|
|
104251
|
+
xlsx__WEBPACK_IMPORTED_MODULE_50__.utils.book_append_sheet(wb, ws, "Test");
|
|
104252
|
+
const wbout = xlsx__WEBPACK_IMPORTED_MODULE_50__.write(wb, { bookType: 'xlsx', type: 'array' });
|
|
104901
104253
|
// Try to extract folder ID if available, otherwise upload to root
|
|
104902
104254
|
const folderId = driveFolderUrl ? extractFolderId(driveFolderUrl) : undefined;
|
|
104903
104255
|
const result = await uploadFileToDrive(wbout, 'test_table.xlsx', folderId || undefined);
|
|
@@ -104931,7 +104283,7 @@ ${imageData.originalPrompt}
|
|
|
104931
104283
|
};
|
|
104932
104284
|
// Show lock screen if not unlocked
|
|
104933
104285
|
if (!unlocked) {
|
|
104934
|
-
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104286
|
+
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { theme: theme },
|
|
104935
104287
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_4__["default"], null),
|
|
104936
104288
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { onClick: handleSecretClick, sx: {
|
|
104937
104289
|
width: '100vw',
|
|
@@ -104971,24 +104323,24 @@ ${imageData.originalPrompt}
|
|
|
104971
104323
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("br", null),
|
|
104972
104324
|
"Please contact system administrator"))));
|
|
104973
104325
|
}
|
|
104974
|
-
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104326
|
+
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { theme: theme },
|
|
104975
104327
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_4__["default"], null),
|
|
104976
104328
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_7__["default"], { maxWidth: "lg", sx: { py: 4 } },
|
|
104977
104329
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 } },
|
|
104978
104330
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h4", component: "h1", sx: { fontWeight: 'bold', color: 'primary.main' } }, "Docs Combiner"),
|
|
104979
104331
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
|
|
104980
104332
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { onClick: () => setPromptManagerOpen(true), color: "inherit", "aria-label": "manage prompts", sx: { mr: 1 } },
|
|
104981
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104982
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { onClick: toggleTheme, color: "inherit", "aria-label": "toggle theme" }, darkMode ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104333
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_46__["default"], null)),
|
|
104334
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { onClick: toggleTheme, color: "inherit", "aria-label": "toggle theme" }, darkMode ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_36__["default"], null) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_35__["default"], null)))),
|
|
104983
104335
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_9__["default"], { variant: "outlined", sx: { mb: 4 } },
|
|
104984
104336
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_10__["default"], null,
|
|
104985
104337
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 3 },
|
|
104986
104338
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
|
|
104987
104339
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true }, "Google Drive Authentication"),
|
|
104988
104340
|
accessToken ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_12__["default"], null,
|
|
104989
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_13__["default"], { expandIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104341
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_13__["default"], { expandIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_40__["default"], null) },
|
|
104990
104342
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { sx: { display: 'flex', alignItems: 'center', color: 'success.main' } },
|
|
104991
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104343
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_37__["default"], { sx: { mr: 1 } }),
|
|
104992
104344
|
" Logged In (Credentials Hidden)")),
|
|
104993
104345
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_14__["default"], null,
|
|
104994
104346
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 2 },
|
|
@@ -104999,7 +104351,7 @@ ${imageData.originalPrompt}
|
|
|
104999
104351
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Client ID", variant: "outlined", fullWidth: true, value: clientId, onChange: (e) => handleClientIdChange(e.target.value), helperText: "From Google Cloud Console (OAuth 2.0 Client ID)" }),
|
|
105000
104352
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Client Secret", variant: "outlined", fullWidth: true, value: clientSecret, onChange: (e) => handleClientSecretChange(e.target.value) }))),
|
|
105001
104353
|
openaiApiKey && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 2, mt: 2 } },
|
|
105002
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104354
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_33__["default"], { color: openRouterAccountBalance !== null ? 'primary' : 'disabled' }),
|
|
105003
104355
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", color: openRouterAccountBalance !== null ? 'text.primary' : 'text.secondary' },
|
|
105004
104356
|
"\u0411\u0430\u043B\u0430\u043D\u0441: ",
|
|
105005
104357
|
openRouterBalanceLoading ? 'Loading...' : (openRouterAccountBalance !== null ? `$${openRouterAccountBalance.toFixed(2)}` : 'N/A')),
|
|
@@ -105008,8 +104360,8 @@ ${imageData.originalPrompt}
|
|
|
105008
104360
|
"\u041B\u0438\u043C\u0438\u0442 \u043A\u043B\u044E\u0447\u0430: ",
|
|
105009
104361
|
openRouterBalanceLoading ? 'Loading...' : (openRouterBalance === -1 ? 'Без лимита' : (openRouterBalance !== null ? `${openRouterBalance.toFixed(4)}` : 'N/A'))))),
|
|
105010
104362
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, alignItems: "center", sx: { mt: 2 } },
|
|
105011
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: accessToken ? "success" : "primary", onClick: handleLogin, disabled: authLoading || !clientId || !clientSecret, startIcon: authLoading ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : (accessToken ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
105012
|
-
accessToken && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "error", onClick: handleLogout, startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104363
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: accessToken ? "success" : "primary", onClick: handleLogin, disabled: authLoading || !clientId || !clientSecret, startIcon: authLoading ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : (accessToken ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_37__["default"], null) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_42__["default"], null)), sx: { flexGrow: 1 } }, authLoading ? 'Logging in...' : (accessToken ? 'Logged In' : 'Login with Google')),
|
|
104364
|
+
accessToken && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "error", onClick: handleLogout, startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_43__["default"], null) }, "Logout")))),
|
|
105013
104365
|
!accessToken && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
|
|
105014
104366
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], null),
|
|
105015
104367
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
|
|
@@ -105025,16 +104377,30 @@ ${imageData.originalPrompt}
|
|
|
105025
104377
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 14 }),
|
|
105026
104378
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0444\u0430\u0439\u043B\u043E\u0432...")))),
|
|
105027
104379
|
!checkingFolderFiles && folderFilesInfo && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], null,
|
|
105028
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "span", sx: { display: 'block' } }, folderFilesInfo.
|
|
105029
|
-
"\u2713 \u041D\u0430\u0439\u0434\u0435\u043D\u043E \u0438\u0441\u0445\u043E\u0434\u043D\u044B\u0445 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439: ",
|
|
105030
|
-
folderFilesInfo.bankaCount)) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'warning.main' } }, "\u26A0 \u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F (banka*.png/jpg) \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B"))),
|
|
105031
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "span", sx: { display: 'block', mt: 0.5 } }, folderFilesInfo.hasProduct ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'success.main' } }, "\u2713 product.png/jpg \u043D\u0430\u0439\u0434\u0435\u043D")) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'info.main' } }, "\u2139 product.png/jpg \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"))))),
|
|
104380
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "span", sx: { display: 'block' } }, folderFilesInfo.hasProduct ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'success.main' } }, "\u2713 product.png/jpg \u043D\u0430\u0439\u0434\u0435\u043D")) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { component: "span", sx: { color: 'info.main' } }, "\u2139 product.png/jpg \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u2014 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u043A\u043D\u043E\u043F\u043A\u0443 \u043D\u0438\u0436\u0435"))))),
|
|
105032
104381
|
!checkingFolderFiles && !folderFilesInfo && driveFolderUrl.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], null, "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u0443\u044E \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u043F\u0430\u043F\u043A\u0443 Google Drive"))))),
|
|
105033
104382
|
!driveFolderUrl.trim() ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "info", sx: { mt: 2 } },
|
|
105034
104383
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body1", sx: { fontWeight: 'bold', mb: 1 } }, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043F\u0430\u043F\u043A\u0443 Google Drive \u0434\u043B\u044F \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u044F"),
|
|
105035
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2" }, "\u0414\u043B\u044F \u0440\u0430\u0431\u043E\u0442\u044B \u0441 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0435\u0439 \u043A\u043E\u043D\u0442\u0435\u043D\u0442\u0430 \u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u0430\u043F\u043A\u0443 Google Drive. \u0412\u0441\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C\u0441\u044F \u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044C\u0441\u044F \u0438\u0437 \u044D\u0442\u043E\u0439 \u043F\u0430\u043F\u043A\u0438."))) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104384
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2" }, "\u0414\u043B\u044F \u0440\u0430\u0431\u043E\u0442\u044B \u0441 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0435\u0439 \u043A\u043E\u043D\u0442\u0435\u043D\u0442\u0430 \u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u0430\u043F\u043A\u0443 Google Drive. \u0412\u0441\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C\u0441\u044F \u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044C\u0441\u044F \u0438\u0437 \u044D\u0442\u043E\u0439 \u043F\u0430\u043F\u043A\u0438."))) : !extractFolderId(driveFolderUrl) ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "warning", sx: { mt: 2 } },
|
|
104385
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body1", sx: { fontWeight: 'bold', mb: 1 } }, "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u0443\u044E \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u043F\u0430\u043F\u043A\u0443"),
|
|
104386
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2" }, "\u0421\u0441\u044B\u043B\u043A\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u0432 \u0444\u043E\u0440\u043C\u0430\u0442\u0435: https://drive.google.com/drive/folders/..."))) : loadingContentFromDrive ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
|
|
104387
|
+
display: 'flex',
|
|
104388
|
+
flexDirection: 'column',
|
|
104389
|
+
alignItems: 'center',
|
|
104390
|
+
justifyContent: 'center',
|
|
104391
|
+
gap: 2,
|
|
104392
|
+
p: 4,
|
|
104393
|
+
mt: 2,
|
|
104394
|
+
border: (theme) => `2px dashed ${theme.palette.primary.main}`,
|
|
104395
|
+
borderRadius: 1,
|
|
104396
|
+
backgroundColor: (theme) => theme.palette.mode === 'dark'
|
|
104397
|
+
? 'rgba(25, 118, 210, 0.1)'
|
|
104398
|
+
: 'rgba(25, 118, 210, 0.05)'
|
|
104399
|
+
} },
|
|
104400
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 40, sx: { color: 'primary.main' } }),
|
|
104401
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body1", sx: { color: 'text.secondary', fontWeight: 'bold' } }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u0438\u0437 \u043F\u0430\u043F\u043A\u0438..."))) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
|
|
105036
104402
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: { xs: 'column', sm: 'row' }, spacing: 2 },
|
|
105037
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Brand (Short ID)", variant: "outlined", sx: { flex: '0 0
|
|
104403
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Brand (Short ID)", variant: "outlined", sx: { flex: '0 0 320px', minWidth: 280 }, value: brand, InputProps: { readOnly: true }, placeholder: "\u0410\u0432\u0442\u043E: \u0442\u043E\u0432\u0430\u0440-\u0433\u0435\u043E-\u0446\u0435\u043D\u0430", helperText: "\u0410\u0432\u0442\u043E\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F \u0438\u0437 \u0442\u043E\u0432\u0430\u0440\u0430, \u0433\u0435\u043E \u0438 \u0446\u0435\u043D\u044B" }),
|
|
105038
104404
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { flex: 1, minWidth: 0 } },
|
|
105039
104405
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "Link", variant: "outlined", fullWidth: true, value: link, onChange: (e) => handleLinkChange(e.target.value), onBlur: handleLinkBlur, error: !!linkError, helperText: linkError, placeholder: "https://example.com/product/" }))),
|
|
105040
104406
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { sx: { my: 2 } }),
|
|
@@ -105067,7 +104433,7 @@ ${imageData.originalPrompt}
|
|
|
105067
104433
|
: 'Выбрана модель: ' + (validationModels.find(m => m.id === selectedValidationModel)?.name || selectedValidationModel))),
|
|
105068
104434
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_25__["default"], { control: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_26__["default"], { checked: validationDisabled, onChange: (e) => handleValidationDisabledChange(e.target.checked), color: "primary" }), label: "\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443", sx: { mt: 1 } })))),
|
|
105069
104435
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
|
|
105070
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "primary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104436
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "primary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_34__["default"], null), onClick: handleGenerateContent, disabled: generating || loadingContentFromDrive || !openaiApiKey || !generateProduct.trim() || !generateGeo.trim(), sx: { flexGrow: 1 }, size: "large" }, generating ? 'Generating...' : 'Generate Titles & Descriptions'),
|
|
105071
104437
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 1, alignItems: "center", sx: { flexGrow: 1 } },
|
|
105072
104438
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["default"], { title: imageAspectRatio === '1:1'
|
|
105073
104439
|
? '1:1 — квадрат (1024×1024 px)'
|
|
@@ -105094,8 +104460,7 @@ ${imageData.originalPrompt}
|
|
|
105094
104460
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_29__["default"], { value: "1:1", sx: { px: 1.5, fontWeight: 600, fontSize: '0.8rem' } }, "1:1"),
|
|
105095
104461
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_29__["default"], { value: "2:3", sx: { px: 1.5, fontWeight: 600, fontSize: '0.8rem' } }, "2:3"),
|
|
105096
104462
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_29__["default"], { value: "both", sx: { px: 1.5, fontWeight: 600, fontSize: '0.8rem' } }, "\u041E\u0431\u0430")))),
|
|
105097
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "secondary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
105098
|
-
loadingImagesFromDrive ||
|
|
104463
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "secondary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_34__["default"], null), onClick: handleGenerateImages, disabled: generatingImages ||
|
|
105099
104464
|
!openaiApiKey ||
|
|
105100
104465
|
(!accessToken && !refreshToken) ||
|
|
105101
104466
|
!generateProduct.trim() ||
|
|
@@ -105113,6 +104478,18 @@ ${imageData.originalPrompt}
|
|
|
105113
104478
|
: !driveFolderUrl.trim()
|
|
105114
104479
|
? 'Заполните URL папки Google Drive'
|
|
105115
104480
|
: undefined }, generatingImages ? 'Generating Images...' : 'Generate Images')),
|
|
104481
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "success", size: "large", onClick: handleGenerate, disabled: loading, startIcon: loading ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null), sx: { py: 1.5, fontSize: '1.1rem', flexGrow: 1 } }, loading ? 'Generating...' : 'Generate Catalog'),
|
|
104482
|
+
uploadedLink && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "success", sx: { flexGrow: 1, minWidth: 0 }, action: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { "aria-label": "copy link", color: "inherit", size: "small", onClick: handleCopyLink, sx: { ml: 1 } },
|
|
104483
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_39__["default"], { fontSize: "small" })) },
|
|
104484
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' } },
|
|
104485
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
|
|
104486
|
+
"\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D!",
|
|
104487
|
+
' ',
|
|
104488
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { href: "#", onClick: (e) => {
|
|
104489
|
+
e.preventDefault();
|
|
104490
|
+
getElectronAPI().openExternal(uploadedLink);
|
|
104491
|
+
}, style: { cursor: 'pointer', textDecoration: 'underline', color: 'inherit' } }, "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 Google Drive")),
|
|
104492
|
+
linkCopied && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'success.dark', fontWeight: 'bold' } }, "\u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u043E!"))))),
|
|
105116
104493
|
folderFilesInfo !== null && !folderFilesInfo.hasProduct && driveFolderUrl.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "warning", sx: { mt: 1 } }, "\u0414\u043B\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F product.png/jpg. \u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439\u0442\u0435 \u0435\u0433\u043E \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043A\u043D\u043E\u043F\u043A\u0438 \"Generate Product from Banka\".")),
|
|
105117
104494
|
!generatingImages && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
|
|
105118
104495
|
!openaiApiKey && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "error", sx: { mt: 1 } }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 OpenRouter API Key")),
|
|
@@ -105120,18 +104497,17 @@ ${imageData.originalPrompt}
|
|
|
105120
104497
|
openaiApiKey && (accessToken || refreshToken) && !generateProduct.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "error", sx: { mt: 1 } }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u0435 \u0422\u043E\u0432\u0430\u0440")),
|
|
105121
104498
|
openaiApiKey && (accessToken || refreshToken) && generateProduct.trim() && !generateGeo.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "error", sx: { mt: 1 } }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u0435 \u0413\u0435\u043E")),
|
|
105122
104499
|
openaiApiKey && (accessToken || refreshToken) && generateProduct.trim() && generateGeo.trim() && !driveFolderUrl.trim() && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "error", sx: { mt: 1 } }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 URL \u043F\u0430\u043F\u043A\u0438 Google Drive"))))),
|
|
104500
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { type: "file", ref: productFileInputRef, accept: "image/png,image/jpeg,image/jpg", style: { display: 'none' }, onChange: handleProductFileSelected }),
|
|
105123
104501
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
|
|
105124
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "success", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
105125
|
-
|
|
105126
|
-
: (productGeneratedImage ? 'Перегенерировать Product' : 'Generate Product from Banka')),
|
|
105127
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "info", startIcon: generatingLanding ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_49__["default"], null), onClick: handleCreateLanding, disabled: generatingLanding ||
|
|
104502
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "success", startIcon: uploadingProduct ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null), onClick: handleUploadProductFile, disabled: uploadingProduct || !accessToken || !driveFolderUrl.trim(), sx: { flexGrow: 1 }, size: "large" }, uploadingProduct ? 'Загрузка...' : 'Загрузить product.png/jpg'),
|
|
104503
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "info", startIcon: generatingLanding ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_44__["default"], null), onClick: handleCreateLanding, disabled: generatingLanding ||
|
|
105128
104504
|
!openaiApiKey ||
|
|
105129
104505
|
!accessToken ||
|
|
105130
104506
|
!driveFolderUrl.trim() ||
|
|
105131
104507
|
(folderFilesInfo !== null && !folderFilesInfo.hasProduct), sx: { flexGrow: 1 }, size: "large", title: folderFilesInfo !== null && !folderFilesInfo.hasProduct
|
|
105132
|
-
? 'product.png/jpg не найден в папке.
|
|
104508
|
+
? 'product.png/jpg не найден в папке. Загрузите его с помощью кнопки «Загрузить product.png/jpg»'
|
|
105133
104509
|
: undefined }, generatingLanding ? 'Creating Landing...' : 'Создать лендинг')),
|
|
105134
|
-
|
|
104510
|
+
generatedImagesData.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2, mt: 3 } },
|
|
105135
104511
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } },
|
|
105136
104512
|
"\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u043A\u0440\u0435\u043E (",
|
|
105137
104513
|
generatedImagesData.filter(img => img.imageUrl).length,
|
|
@@ -105141,22 +104517,6 @@ ${imageData.originalPrompt}
|
|
|
105141
104517
|
checkingImages && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1, mb: 2 } },
|
|
105142
104518
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }),
|
|
105143
104519
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary' } }, "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0430..."))),
|
|
105144
|
-
loadingImagesFromDrive && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
|
|
105145
|
-
display: 'flex',
|
|
105146
|
-
flexDirection: 'column',
|
|
105147
|
-
alignItems: 'center',
|
|
105148
|
-
justifyContent: 'center',
|
|
105149
|
-
gap: 2,
|
|
105150
|
-
p: 4,
|
|
105151
|
-
border: (theme) => `2px dashed ${theme.palette.primary.main}`,
|
|
105152
|
-
borderRadius: 1,
|
|
105153
|
-
backgroundColor: (theme) => theme.palette.mode === 'dark'
|
|
105154
|
-
? 'rgba(25, 118, 210, 0.1)'
|
|
105155
|
-
: 'rgba(25, 118, 210, 0.05)',
|
|
105156
|
-
minHeight: 200
|
|
105157
|
-
} },
|
|
105158
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 40, sx: { color: 'primary.main' } }),
|
|
105159
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body1", sx: { color: 'text.secondary', fontWeight: 'bold' } }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439 \u0438\u0437 Google Drive..."))),
|
|
105160
104520
|
generatedImagesData.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
|
|
105161
104521
|
display: 'grid',
|
|
105162
104522
|
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
|
|
@@ -105281,7 +104641,7 @@ ${imageData.originalPrompt}
|
|
|
105281
104641
|
"+",
|
|
105282
104642
|
imageData.checkErrors.length - 2,
|
|
105283
104643
|
" \u0435\u0449\u0451")))),
|
|
105284
|
-
imageData.checkFailed && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", color: "warning", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104644
|
+
imageData.checkFailed && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", color: "warning", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_45__["default"], null), onClick: () => handleRetryCheck(imageData), disabled: imageData.checking, sx: { mt: 0.5, fontSize: '0.7rem', py: 0.25, px: 1 } }, "\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443")))),
|
|
105285
104645
|
imageData.checkStatus === 'checking' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'warning.main', display: 'block' } }, "\uD83D\uDD0D \u041F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442\u0441\u044F...")),
|
|
105286
104646
|
imageData.checkStatus === 'pending' && !imageData.failed && !imageData.generating && imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'text.secondary', display: 'block' } }, "\u23F3 \u041E\u0436\u0438\u0434\u0430\u0435\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438")),
|
|
105287
104647
|
imageData.failed && !imageData.generating && !imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'error.main', display: 'block', fontWeight: 'bold' } }, "\u274C \u0413\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C")),
|
|
@@ -105304,7 +104664,7 @@ ${imageData.originalPrompt}
|
|
|
105304
104664
|
: 'transparent'
|
|
105305
104665
|
}
|
|
105306
104666
|
} }),
|
|
105307
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "contained", color: imageData.failed ? 'error' : imageData.checkStatus === 'needs_rebuild' ? 'warning' : 'primary', startIcon: imageData.regenerating ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104667
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "contained", color: imageData.failed ? 'error' : imageData.checkStatus === 'needs_rebuild' ? 'warning' : 'primary', startIcon: imageData.regenerating ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_34__["default"], null), onClick: () => handleRegenerateImage(imageData), disabled: imageData.regenerating ||
|
|
105308
104668
|
imageData.uploading ||
|
|
105309
104669
|
imageData.checkStatus === 'checking' ||
|
|
105310
104670
|
!imageData.originalPrompt ||
|
|
@@ -105314,12 +104674,12 @@ ${imageData.originalPrompt}
|
|
|
105314
104674
|
: ((generatingImages && !imageData.imageUrl && !imageData.failed && !imageData.generating)
|
|
105315
104675
|
? 'В очереди'
|
|
105316
104676
|
: (!imageData.imageUrl ? 'Сгенерировать' : 'Переделать'))),
|
|
105317
|
-
imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", color: "secondary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104677
|
+
imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", color: "secondary", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_45__["default"], null), onClick: () => handleRegenerateImageFresh(imageData), disabled: imageData.regenerating ||
|
|
105318
104678
|
imageData.uploading ||
|
|
105319
104679
|
imageData.checkStatus === 'checking' ||
|
|
105320
104680
|
!imageData.originalPrompt ||
|
|
105321
104681
|
!imageData.productImageUrl, fullWidth: true }, "\u041F\u0435\u0440\u0435\u0434\u0435\u043B\u0430\u0442\u044C \u0437\u0430\u043D\u043E\u0432\u043E")),
|
|
105322
|
-
!imageData.uploaded && imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104682
|
+
!imageData.uploaded && imageData.imageUrl && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null), onClick: () => {
|
|
105323
104683
|
const folderId = extractFolderId(driveFolderUrl);
|
|
105324
104684
|
if (folderId) {
|
|
105325
104685
|
handleUploadImage(imageData, folderId);
|
|
@@ -105327,7 +104687,7 @@ ${imageData.originalPrompt}
|
|
|
105327
104687
|
}, disabled: imageData.uploading || imageData.regenerating || !driveFolderUrl.trim(), fullWidth: true }, imageData.uploading ? 'Загрузка...' : 'Загрузить'))))));
|
|
105328
104688
|
}))),
|
|
105329
104689
|
generatedImagesData.length > 0 && generatedImagesData.some(img => !img.uploaded && img.imageUrl) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mt: 2 } },
|
|
105330
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "primary", onClick: handleUploadAllImages, disabled: uploadingImages || generatingImages || !driveFolderUrl.trim(), startIcon: uploadingImages ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104690
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "primary", onClick: handleUploadAllImages, disabled: uploadingImages || generatingImages || !driveFolderUrl.trim(), startIcon: uploadingImages ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_38__["default"], null), fullWidth: true }, uploadingImages ? 'Загрузка...' : `Загрузить все (${generatedImagesData.filter(img => !img.uploaded && img.imageUrl).length})`))))),
|
|
105331
104691
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { sx: { my: 2 } }),
|
|
105332
104692
|
(generatedTitlesData.length > 0 || generatedTextsData.length > 0 || loadingContentFromDrive) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2 } },
|
|
105333
104693
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { fontWeight: 'bold' } },
|
|
@@ -105398,7 +104758,7 @@ ${imageData.originalPrompt}
|
|
|
105398
104758
|
tooltip: { sx: { maxWidth: 360, fontSize: 12, lineHeight: 1.6, p: '10px 14px' } },
|
|
105399
104759
|
} },
|
|
105400
104760
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { size: "small", sx: { p: 0.25, color: 'text.disabled', '&:hover': { color: 'primary.main' } } },
|
|
105401
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
104761
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_41__["default"], { sx: { fontSize: 14 } })))) : null),
|
|
105402
104762
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 1.5 },
|
|
105403
104763
|
titleData && (titleData.generating ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
|
|
105404
104764
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16, sx: { color: 'primary.main' } }),
|
|
@@ -105465,102 +104825,8 @@ ${imageData.originalPrompt}
|
|
|
105465
104825
|
generatingLanding && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1, mb: 2 } },
|
|
105466
104826
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }),
|
|
105467
104827
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2", sx: { color: 'text.secondary' } }, formatElapsedTime(elapsedTime.landing)))),
|
|
105468
|
-
!generatingLanding && generatedLandingHTML && generatedLandingImageBlob && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "primary", onClick: handlePreviewLanding, startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
105469
|
-
|
|
105470
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", size: "large", onClick: handleGenerate, disabled: loading || !accessToken, startIcon: loading ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20, color: "inherit" }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_43__["default"], null), sx: { py: 1.5, fontSize: '1.1rem', flexGrow: 1 } }, loading ? 'Generating...' : 'Generate Table')),
|
|
105471
|
-
uploadedLink && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], { severity: "success", sx: { mt: 2 }, action: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { "aria-label": "copy link", color: "inherit", size: "small", onClick: handleCopyLink, sx: { ml: 1 } },
|
|
105472
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_44__["default"], { fontSize: "small" })) },
|
|
105473
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
|
|
105474
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
|
|
105475
|
-
"File Uploaded!",
|
|
105476
|
-
' ',
|
|
105477
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { href: "#", onClick: (e) => {
|
|
105478
|
-
e.preventDefault();
|
|
105479
|
-
getElectronAPI().openExternal(uploadedLink);
|
|
105480
|
-
}, style: { cursor: 'pointer', textDecoration: 'underline', color: 'inherit' } }, "Click here to open in Google Drive")),
|
|
105481
|
-
linkCopied && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { color: 'success.dark', fontWeight: 'bold' } }, "Copied!")))))))))),
|
|
105482
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_31__["default"], { open: productModalOpen, onClose: () => !generatingProduct && !uploadingProduct && setProductModalOpen(false), maxWidth: "lg", fullWidth: true },
|
|
105483
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_32__["default"], { open: generatingProduct || uploadingProduct, sx: {
|
|
105484
|
-
position: 'absolute',
|
|
105485
|
-
zIndex: (theme) => theme.zIndex.modal + 1,
|
|
105486
|
-
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
105487
|
-
display: 'flex',
|
|
105488
|
-
flexDirection: 'column',
|
|
105489
|
-
gap: 3
|
|
105490
|
-
} },
|
|
105491
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 } },
|
|
105492
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 60, sx: { color: 'white' } }),
|
|
105493
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", sx: { color: 'white', fontWeight: 'bold' } }, formatElapsedTime(elapsedTime.product))),
|
|
105494
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_30__["default"], { sx: {
|
|
105495
|
-
p: 3,
|
|
105496
|
-
maxWidth: '80%',
|
|
105497
|
-
maxHeight: '60%',
|
|
105498
|
-
overflow: 'auto',
|
|
105499
|
-
backgroundColor: (theme) => theme.palette.mode === 'dark'
|
|
105500
|
-
? 'rgba(30, 30, 30, 0.95)'
|
|
105501
|
-
: 'rgba(255, 255, 255, 0.95)'
|
|
105502
|
-
} },
|
|
105503
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true, sx: { mb: 2, fontWeight: 'bold' } }, uploadingProduct ? 'Процесс загрузки' : 'Процесс генерации'),
|
|
105504
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { id: "product-generation-logs", sx: {
|
|
105505
|
-
fontFamily: 'monospace',
|
|
105506
|
-
fontSize: '0.875rem',
|
|
105507
|
-
lineHeight: 1.6,
|
|
105508
|
-
whiteSpace: 'pre-wrap',
|
|
105509
|
-
wordBreak: 'break-word',
|
|
105510
|
-
maxHeight: '400px',
|
|
105511
|
-
overflowY: 'auto'
|
|
105512
|
-
} }, productGenerationLogs.length === 0 ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { color: "text.secondary" }, uploadingProduct ? 'Начало загрузки...' : 'Инициализация...')) : (productGenerationLogs.map((log, idx) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { key: idx, sx: {
|
|
105513
|
-
mb: 0.5,
|
|
105514
|
-
color: log.includes('❌') || log.toLowerCase().includes('error') ? 'error.main' :
|
|
105515
|
-
log.includes('⚠️') || log.toLowerCase().includes('warn') ? 'warning.main' :
|
|
105516
|
-
'text.primary'
|
|
105517
|
-
} }, log))))))),
|
|
105518
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_33__["default"], null,
|
|
105519
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6" }, "Product Image Generation")),
|
|
105520
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_34__["default"], null,
|
|
105521
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 3, sx: { mt: 1 } },
|
|
105522
|
-
productSourceImages.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
|
|
105523
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "subtitle1", gutterBottom: true, sx: { fontWeight: 'bold' } },
|
|
105524
|
-
"\u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F (",
|
|
105525
|
-
productSourceImages.length,
|
|
105526
|
-
"):"),
|
|
105527
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: {
|
|
105528
|
-
display: 'grid',
|
|
105529
|
-
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
|
|
105530
|
-
gap: 2,
|
|
105531
|
-
mt: 1
|
|
105532
|
-
} }, productSourceImages.map((url, idx) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { key: idx, component: "img", src: convertDriveUrlToImageUrl(url), alt: `Source ${idx + 1}`, sx: {
|
|
105533
|
-
width: '100%',
|
|
105534
|
-
height: 'auto',
|
|
105535
|
-
maxHeight: 300,
|
|
105536
|
-
objectFit: 'contain',
|
|
105537
|
-
border: (theme) => `1px solid ${theme.palette.mode === 'dark' ? '#444' : '#ddd'}`,
|
|
105538
|
-
borderRadius: 1
|
|
105539
|
-
}, onError: (e) => {
|
|
105540
|
-
// Fallback: try using thumbnail URL if direct view fails
|
|
105541
|
-
const fileIdMatch = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);
|
|
105542
|
-
if (fileIdMatch) {
|
|
105543
|
-
const fileId = fileIdMatch[1];
|
|
105544
|
-
e.target.src = `https://drive.google.com/thumbnail?id=${fileId}&sz=w1000`;
|
|
105545
|
-
}
|
|
105546
|
-
} })))))),
|
|
105547
|
-
productGeneratedImage && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
|
|
105548
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "subtitle1", gutterBottom: true, sx: { fontWeight: 'bold' } }, "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0434\u0443\u043A\u0442\u0430:"),
|
|
105549
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { component: "img", src: productGeneratedImage, alt: "Generated Product", sx: {
|
|
105550
|
-
width: '100%',
|
|
105551
|
-
height: 'auto',
|
|
105552
|
-
maxHeight: 400,
|
|
105553
|
-
objectFit: 'contain',
|
|
105554
|
-
border: (theme) => `2px solid ${theme.palette.primary.main}`,
|
|
105555
|
-
borderRadius: 1,
|
|
105556
|
-
mt: 1
|
|
105557
|
-
} }))),
|
|
105558
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043A \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438", variant: "outlined", fullWidth: true, multiline: true, minRows: 2, value: productRegeneratePrompt, onChange: (e) => setProductRegeneratePrompt(e.target.value), placeholder: "\u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: \u0441\u0434\u0435\u043B\u0430\u0439 \u0444\u043E\u043D \u0431\u0435\u043B\u044B\u043C, \u0443\u0432\u0435\u043B\u0438\u0447\u044C \u043B\u043E\u0433\u043E\u0442\u0438\u043F...", helperText: "\u042D\u0442\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u0431\u0443\u0434\u0443\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u044B \u043A \u0431\u0430\u0437\u043E\u0432\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 (\u043D\u0435 \u0437\u0430\u043C\u0435\u043D\u044F\u044E\u0442 \u0435\u0433\u043E). \u041E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0441\u0442\u044B\u043C \u0434\u043B\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u0430\u0437\u043E\u0432\u043E\u0433\u043E \u043F\u0440\u043E\u043C\u043F\u0442\u0430.", disabled: generatingProduct || uploadingProduct }))),
|
|
105559
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_35__["default"], { sx: { px: 3, pb: 2 } },
|
|
105560
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { onClick: () => setProductModalOpen(false), disabled: generatingProduct || uploadingProduct }, "\u0417\u0430\u043A\u0440\u044B\u0442\u044C"),
|
|
105561
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "secondary", onClick: handleRegenerateProduct, disabled: generatingProduct || uploadingProduct || !productSourceImages.length || !productGeneratedImage, startIcon: generatingProduct ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_39__["default"], null) }, generatingProduct ? 'Перегенерация...' : 'Перегенерировать'),
|
|
105562
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "contained", color: "primary", onClick: handleUploadProduct, disabled: generatingProduct || uploadingProduct || !productGeneratedImage || !driveFolderUrl.trim(), startIcon: uploadingProduct ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 20 }) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_43__["default"], null) }, uploadingProduct ? 'Загрузка...' : 'Загрузить (product.png)'))),
|
|
105563
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_PromptManagerDialog__WEBPACK_IMPORTED_MODULE_53__["default"], { open: promptManagerOpen, onClose: () => setPromptManagerOpen(false) }))));
|
|
104828
|
+
!generatingLanding && generatedLandingHTML && generatedLandingImageBlob && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { variant: "outlined", color: "primary", onClick: handlePreviewLanding, startIcon: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_47__["default"], null), fullWidth: true }, "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043B\u0435\u043D\u0434\u0438\u043D\u0433\u0430"))))))))),
|
|
104829
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_PromptManagerDialog__WEBPACK_IMPORTED_MODULE_48__["default"], { open: promptManagerOpen, onClose: () => setPromptManagerOpen(false) }))));
|
|
105564
104830
|
}
|
|
105565
104831
|
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (App);
|
|
105566
104832
|
|
|
@@ -105995,8 +105261,8 @@ function PromptManagerDialog({ open, onClose }) {
|
|
|
105995
105261
|
const [viewModes, setViewModes] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({});
|
|
105996
105262
|
// selectedApproaches: array of indices from PAIR_APPROACH_POOL, ordered
|
|
105997
105263
|
const [selectedApproaches, setSelectedApproaches] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([0, 1, 2]);
|
|
105998
|
-
// imageApproachCounts:
|
|
105999
|
-
const [imageApproachCounts, setImageApproachCounts] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(
|
|
105264
|
+
// imageApproachCounts: по одному на каждый подход (CREO_APPROACHES), каждый 0–4
|
|
105265
|
+
const [imageApproachCounts, setImageApproachCounts] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => Array(_prompts__WEBPACK_IMPORTED_MODULE_34__.CREO_APPROACHES.length).fill(1));
|
|
106000
105266
|
// Загрузить оверрайды при открытии
|
|
106001
105267
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
106002
105268
|
if (open) {
|
|
@@ -106596,25 +105862,32 @@ function getSelectedPairApproaches() {
|
|
|
106596
105862
|
function getPairsCount() {
|
|
106597
105863
|
return getSelectedPairApproaches().length;
|
|
106598
105864
|
}
|
|
105865
|
+
/** Количество подходов для изображений. Должно совпадать с CREO_APPROACHES.length в prompts.ts */
|
|
105866
|
+
const IMAGE_APPROACH_COUNT = 10;
|
|
106599
105867
|
/**
|
|
106600
|
-
* Получить количество изображений по каждому подходу (
|
|
105868
|
+
* Получить количество изображений по каждому подходу (N элементов, 0–4).
|
|
106601
105869
|
* По умолчанию — по 1 на каждый подход.
|
|
106602
105870
|
*/
|
|
106603
105871
|
function getImageApproachCounts() {
|
|
106604
105872
|
const overrides = loadPromptOverrides();
|
|
106605
|
-
|
|
106606
|
-
|
|
105873
|
+
const defaultCounts = Array(IMAGE_APPROACH_COUNT).fill(1);
|
|
105874
|
+
if (overrides.imageApproachCounts && overrides.imageApproachCounts.length > 0) {
|
|
105875
|
+
const stored = overrides.imageApproachCounts.map(c => Math.max(0, Math.min(4, c)));
|
|
105876
|
+
// backward compat: если было 8, дополняем новыми подходами
|
|
105877
|
+
while (stored.length < IMAGE_APPROACH_COUNT)
|
|
105878
|
+
stored.push(1);
|
|
105879
|
+
return stored.slice(0, IMAGE_APPROACH_COUNT);
|
|
106607
105880
|
}
|
|
106608
105881
|
// backward compat: selectedImageApproaches -> counts (1 for selected, 0 for not)
|
|
106609
105882
|
if (overrides.selectedImageApproaches && overrides.selectedImageApproaches.length >= 1) {
|
|
106610
|
-
const counts = Array(
|
|
105883
|
+
const counts = Array(IMAGE_APPROACH_COUNT).fill(0);
|
|
106611
105884
|
for (const i of overrides.selectedImageApproaches) {
|
|
106612
|
-
if (i >= 0 && i <
|
|
105885
|
+
if (i >= 0 && i < IMAGE_APPROACH_COUNT)
|
|
106613
105886
|
counts[i] = 1;
|
|
106614
105887
|
}
|
|
106615
105888
|
return counts;
|
|
106616
105889
|
}
|
|
106617
|
-
return
|
|
105890
|
+
return defaultCounts;
|
|
106618
105891
|
}
|
|
106619
105892
|
/**
|
|
106620
105893
|
* Загрузить оверрайды из localStorage
|
|
@@ -106778,7 +106051,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
106778
106051
|
/* harmony export */ getTextsSystemPrompt: () => (/* binding */ getTextsSystemPrompt),
|
|
106779
106052
|
/* harmony export */ getTitlesSystemPrompt: () => (/* binding */ getTitlesSystemPrompt),
|
|
106780
106053
|
/* harmony export */ getUserPrompt: () => (/* binding */ getUserPrompt),
|
|
106781
|
-
/* harmony export */ getValidationPrompt: () => (/* binding */ getValidationPrompt)
|
|
106054
|
+
/* harmony export */ getValidationPrompt: () => (/* binding */ getValidationPrompt),
|
|
106055
|
+
/* harmony export */ pickRandomBullets: () => (/* binding */ pickRandomBullets)
|
|
106782
106056
|
/* harmony export */ });
|
|
106783
106057
|
/* harmony import */ var _promptOverrides__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./promptOverrides */ "./src/promptOverrides.ts");
|
|
106784
106058
|
/**
|
|
@@ -106786,6 +106060,56 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
106786
106060
|
* Все промпты на русском языке для консистентности
|
|
106787
106061
|
*/
|
|
106788
106062
|
|
|
106063
|
+
/** Пул шаблонов буллетов по категориям (на русском — модель переводит на язык GEO) */
|
|
106064
|
+
const BULLET_BY_CATEGORY = {
|
|
106065
|
+
action: [
|
|
106066
|
+
'Снижает дискомфорт', 'Снижает вздутие', 'Снижает боль', 'Быстрое облегчение',
|
|
106067
|
+
'Комфорт каждый день', 'Лёгкость движений', 'Меньше симптомов', 'Без боли',
|
|
106068
|
+
'Дни без дискомфорта', 'Спокойный желудок', 'Спокойные ночи', 'Видимый результат'
|
|
106069
|
+
],
|
|
106070
|
+
timeframe: [
|
|
106071
|
+
'Эффект за 7–14 дней', 'Эффект за 2 недели', 'Быстрый результат', 'Облегчение с 1 дня',
|
|
106072
|
+
'Эффект за 7 дней', 'В 3 раза быстрее', 'С первого дня', 'В первые дни'
|
|
106073
|
+
],
|
|
106074
|
+
socialProof: [
|
|
106075
|
+
'9 из 10 рекомендуют', '9 из 10 мужчин рекомендуют', '93% подтверждают',
|
|
106076
|
+
'8 из 10 довольны', 'Тысячи довольных'
|
|
106077
|
+
],
|
|
106078
|
+
formula: [
|
|
106079
|
+
'Натуральная формула', 'Натуральные ингредиенты', 'Без химии', 'Отборный состав',
|
|
106080
|
+
'Дерматологически протестировано'
|
|
106081
|
+
],
|
|
106082
|
+
qualityOfLife: [
|
|
106083
|
+
'Активный комфорт', 'Дни без забот', 'Больше энергии', 'Здоровый сон', 'Лёгкая жизнь'
|
|
106084
|
+
]
|
|
106085
|
+
};
|
|
106086
|
+
/** Какие категории для какого подхода (по 1 из каждой категории, порядок случайный) */
|
|
106087
|
+
const APPROACH_BULLET_CATEGORIES = {
|
|
106088
|
+
'Эксперт / Авторитет': ['action', 'timeframe', 'socialProof'],
|
|
106089
|
+
'Lifestyle / Момент приёма': ['timeframe', 'socialProof', 'action'], // срок + цифра + действие
|
|
106090
|
+
'Эмоция / Портрет': ['qualityOfLife', 'action', 'timeframe'], // качество жизни + действие + срок
|
|
106091
|
+
'Визуализация проблемы': ['action', 'timeframe', 'socialProof'],
|
|
106092
|
+
'Power / Сила решения': ['action', 'timeframe', 'socialProof'], // действие + скорость + цифра
|
|
106093
|
+
'Минимализм / Clean Big Text': ['action', 'timeframe', 'formula'],
|
|
106094
|
+
'Врач / Эксперт': ['action', 'timeframe', 'socialProof']
|
|
106095
|
+
};
|
|
106096
|
+
function pickOne(arr) {
|
|
106097
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
106098
|
+
}
|
|
106099
|
+
function shuffle(arr) {
|
|
106100
|
+
return [...arr].sort(() => Math.random() - 0.5);
|
|
106101
|
+
}
|
|
106102
|
+
/** Выбрать 3 случайных буллета по правилам подхода (1 из каждой категории, порядок случайный) */
|
|
106103
|
+
function pickRandomBullets(approachName) {
|
|
106104
|
+
const categories = APPROACH_BULLET_CATEGORIES[approachName];
|
|
106105
|
+
if (!categories || categories.length < 3) {
|
|
106106
|
+
// fallback: все категории
|
|
106107
|
+
const all = Object.values(BULLET_BY_CATEGORY).flat();
|
|
106108
|
+
return shuffle(all).slice(0, 3);
|
|
106109
|
+
}
|
|
106110
|
+
const bullets = categories.map(cat => pickOne(BULLET_BY_CATEGORY[cat] || []));
|
|
106111
|
+
return shuffle(bullets);
|
|
106112
|
+
}
|
|
106789
106113
|
// Debug logging — пишет в терминал Electron (или в console как fallback)
|
|
106790
106114
|
function _debugLog(...args) {
|
|
106791
106115
|
const api = typeof window !== 'undefined' ? window.electronAPI : null;
|
|
@@ -106917,16 +106241,13 @@ function getTitlesSystemPrompt(geo, noOverride, count = 3) {
|
|
|
106917
106241
|
- НЕ упоминай название продукта в заголовке — оно уже на креативе. Заголовок = только боль + решение/триггер
|
|
106918
106242
|
|
|
106919
106243
|
Технические требования:
|
|
106920
|
-
- Каждый заголовок
|
|
106921
|
-
- Каждый заголовок
|
|
106244
|
+
- Каждый заголовок может быть 1–4 строки. Допускается до 12 слов (при многострочности)
|
|
106245
|
+
- Каждый заголовок предпочтительно до 55 символов в строке. При необходимости для разнообразия допускается до 60 символов в строке
|
|
106922
106246
|
- ЗАПРЕЩЕНО использовать двоеточие (:) в заголовках. Используй тире (—) или переформулируй заголовок без двоеточия. Если в заголовке есть двоеточие — это критическая ошибка, перепиши заголовок
|
|
106923
106247
|
- Используй 1-2 эмодзи естественно внутри заголовка, не только в конце. Эмодзи должны заменять или визуально отмечать смысл (эмоция, действие, предупреждение), а не украшать или заполнять пространство. Максимум 1-2 на строку
|
|
106924
106248
|
- Избегай эмодзи ⚠️ и 🚨 — они ассоциируются с опасностью. Для привлечения внимания используй 👉, ✅, 🌿, ⏳
|
|
106925
|
-
- КРИТИЧНО: Каждый заголовок
|
|
106249
|
+
- КРИТИЧНО: Каждый заголовок ОБЯЗАН содержать ключевое слово проблемы ЯВНО. Примеры по категориям: простата → простатит, простата; похудение → лишний вес, похудение; суставы → боль в суставах, колени, скованность; пищеварение → дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Если ключевое слово отсутствует — перепиши заголовок
|
|
106926
106250
|
- Используй знания лучших практик рекламного копирайтинга
|
|
106927
|
-
- Избегай только жёстких медицинских гарантий («лечит», «вылечивает», «гарантируем»). Мягкие обещания результата разрешены («Nopți fără treziri», «Stomac liniștit», «Efect rapid»). Разрешены: сроки («în 14 zile»), цифры соц. доказательства («9 din 10 bărbați»), мягкие результаты. Запрещены: «100% гарантия», «навсегда», «излечение».
|
|
106928
|
-
- Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
|
|
106929
|
-
- Если в категории продукта есть специфические медицинские термины или диагнозы — используй их осторожно, не в каждом заголовке. Максимум ${maxMedTerms} заголовок из ${n} может содержать прямое упоминание таких терминов. Предпочитай мягкие формулировки, описывающие симптомы и дискомфорт, а не диагнозы. Если все ${n} заголовков содержат прямые медицинские термины — перепиши все кроме ${maxMedTerms} с мягкими альтернативами, фокусируясь на ощущениях и симптомах
|
|
106930
106251
|
- После создания проверь их правильность и разнообразие
|
|
106931
106252
|
- Верни только заголовки, по одному на строку, без нумерации или буллетов
|
|
106932
106253
|
- Заголовки должны быть на языке ${geo}
|
|
@@ -106954,8 +106275,8 @@ function getTextsSystemPrompt(geo, noOverride, count = 3) {
|
|
|
106954
106275
|
.map((p, i) => `- Текст ${i + 1} → [${p.name}]. ${p.textApproach}`)
|
|
106955
106276
|
.join('\n');
|
|
106956
106277
|
const deadlineNote = n >= 2
|
|
106957
|
-
? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO)
|
|
106958
|
-
: `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO)
|
|
106278
|
+
? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO).`
|
|
106279
|
+
: `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (в языке GEO).`;
|
|
106959
106280
|
return `Ты — эксперт-копирайтер, специализирующийся на рекламе в Meta Ads (Facebook/Instagram). Твоя задача — создать ровно ${n} идеальных продающих рекламных текста для Meta Ads на языке ${geo}.
|
|
106960
106281
|
|
|
106961
106282
|
ВАЖНО — СИСТЕМА ПАРЫ (заголовок + текст):
|
|
@@ -106985,24 +106306,18 @@ ${deadlineNote}
|
|
|
106985
106306
|
- Избегай академических, синтетических или звучащих как ИИ слов и фраз
|
|
106986
106307
|
- Избегай искусственного или неестественного формулирования, которое реальные рекламодатели не использовали бы
|
|
106987
106308
|
- Если фраза звучит синтетически или академически — перепиши её
|
|
106988
|
-
- Избегай медицинской терминологии, которую обычный человек не использует в разговоре («secreția biliară», «diureza», «peristaltică»). Пиши так, как человек описал бы проблему другу
|
|
106989
|
-
|
|
106990
106309
|
Требования к содержанию:
|
|
106991
106310
|
- КРИТИЧНО: Держи каждый текст кратким — цель примерно 150-280 символов на текст. Тексты в стиле отзыва могут использовать до 300 символов при необходимости для естественного повествования. Будь эффектным, но коротким
|
|
106992
106311
|
- Если текст длиннее 280 символов (или 300 для стиля отзыва) — сожми и перепиши, сохраняя смысл
|
|
106993
106312
|
- КРИТИЧНО: Каждый текст должен содержать хотя бы одно ясное ключевое слово проблемы, релевантное категории продукта (например, для суставов: боль/скованность/подвижность; для пищеварения: дискомфорт/тяжесть; для сна: бессонница/усталость — примеры только для формата, адаптируй под категорию). Если ключевое слово проблемы отсутствует — перепиши текст
|
|
106994
106313
|
- КРИТИЧНО: Каждый текст должен заканчиваться сильным и явным призывом к действию (кликни, попробуй сейчас, закажи сегодня, узнай больше и т.д.)
|
|
106995
106314
|
- Избегай слабых окончаний или информационного тона. Слабые или нейтральные окончания не допускаются
|
|
106996
|
-
- CTA должен быть “нажимным”: допускаются формулировки «не откладывай», «последний шанс», «только сегодня», «забери -50%» (всё строго на языке GEO)
|
|
106315
|
+
- CTA должен быть “нажимным”: допускаются формулировки «не откладывай», «последний шанс», «только сегодня», «забери -50%» (всё строго на языке GEO)
|
|
106997
106316
|
- В каждом тексте строго следуй заданному формату старта согласно его подходу (см. распределение выше)
|
|
106998
106317
|
- Если ингредиенты предоставлены в дополнительной информации, упоминай их ТОЛЬКО если они естественно подходят выбранному рекламному подходу. Например: подходы авторитета/экспертизы выигрывают от деталей ингредиентов, в то время как подходы срочности/эмоционального триггера могут не нуждаться в них. Упоминай только релевантные ингредиенты, не обязательно все из них. Если ингредиенты не подходят подходу или не предоставлены — полностью пропусти их упоминание и не изобретай и не упоминай никакие ингредиенты
|
|
106999
106318
|
- Добавляй только существенные поддерживающие детали, которые усиливают убеждение или ясность, а не заполнители или общие утверждения. Будь кратким и прямым
|
|
107000
106319
|
- Используй 1-4 эмодзи естественно внутри текста, не только в конце. Эмодзи должны заменять или визуально отмечать смысл (эмоция, действие, предупреждение), а не украшать или заполнять пространство. Максимум 1-2 на строку
|
|
107001
106320
|
- Избегай эмодзи ⚠️ и 🚨 — они ассоциируются с опасностью. Для привлечения внимания используй 👉, ✅, 🌿, ⏳
|
|
107002
|
-
- Разрешены формулировки: «reduce frecvența», «elimină disconfortul», «rezultat vizibil în X zile», «9 din 10 observă diferența». Запрещены только: «лечит/vindecă», «гарантируем 100%», «навсегда/definitiv».
|
|
107003
|
-
- Ты можешь усиливать эффект и результаты, но без прямых медицинских обещаний
|
|
107004
|
-
- Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
|
|
107005
|
-
- Если в категории продукта есть специфические медицинские термины или диагнозы — используй их осторожно, не в каждом тексте. Максимум ${maxMedTerms} текст из ${n} может содержать прямое упоминание таких терминов. Остальные тексты фокусируй на симптомах, дискомфорте и ощущениях пользователя, релевантных категории продукта. Избегай эвфемизмов и искусственных замен — используй естественные формулировки
|
|
107006
106321
|
- Избегай прямых упоминаний интимных или деликатных деталей, связанных с проблемой. Используй мягкие формулировки, описывающие дискомфорт и симптомы, а не физиологические процессы
|
|
107007
106322
|
- Избегай повторения полного названия продукта более 2 раз в одном тексте. Используй местоимения или более короткие ссылки при необходимости
|
|
107008
106323
|
|
|
@@ -107065,8 +106380,8 @@ function getPairsSystemPrompt(geo, noOverride, count = 3, selectedIndices) {
|
|
|
107065
106380
|
.map((p, i) => `Пара ${i + 1} → [${p.name}]:\n Заголовок: ${p.titleApproach}\n Текст: ${p.textApproach}`)
|
|
107066
106381
|
.join('\n');
|
|
107067
106382
|
const deadlineNote = n >= 2
|
|
107068
|
-
? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo})
|
|
107069
|
-
: `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo})
|
|
106383
|
+
? `- В Тексте 1 ИЛИ Тексте 2 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}).`
|
|
106384
|
+
: `- В Тексте 1 ОБЯЗАТЕЛЬНО укажи ожидаемый срок результата 7–14 дней (на языке ${geo}).`;
|
|
107070
106385
|
return `Ты — эксперт-копирайтер, специализирующийся на рекламе в Meta Ads (Facebook/Instagram). Создай ровно ${n} пар «заголовок + текст» для рекламы на языке ${geo}.
|
|
107071
106386
|
|
|
107072
106387
|
ФОРМАТ ОТВЕТА (строго соблюдай, без лишних символов):
|
|
@@ -107092,7 +106407,6 @@ ${distributionLines}
|
|
|
107092
106407
|
- 1–2 эмодзи естественно внутри (не только в конце); избегай ⚠️ и 🚨
|
|
107093
106408
|
- Хотя бы одно ключевое слово проблемы, релевантное продукту
|
|
107094
106409
|
- НЕ упоминай название продукта — заголовок = боль + триггер
|
|
107095
|
-
- Максимум ${maxMedTerms} из ${n} заголовков могут содержать прямые медицинские термины
|
|
107096
106410
|
- Звучит как живая рекламная фраза, а не строка ключевых слов
|
|
107097
106411
|
|
|
107098
106412
|
ТРЕБОВАНИЯ К ТЕКСТУ:
|
|
@@ -107101,7 +106415,6 @@ ${distributionLines}
|
|
|
107101
106415
|
- Сильный явный CTA в конце («кликни», «попробуй сейчас», «закажи», «не жди»)
|
|
107102
106416
|
- Короткие рубленые фразы, императивы, FOMO допустим${hasTestimonial ? '\n- Testimonial: строго от первого лица, начинается с «Имя, возраст:», конкретный результат с таймингом' : ''}
|
|
107103
106417
|
${deadlineNote}
|
|
107104
|
-
- Избегай «гарантированно», «вылечит», «100%», «навсегда»
|
|
107105
106418
|
|
|
107106
106419
|
ОБЩИЕ ТРЕБОВАНИЯ:
|
|
107107
106420
|
- Язык: ${geo} — все тексты строго на этом языке
|
|
@@ -107175,6 +106488,7 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
|
|
|
107175
106488
|
// Resolve no-bullets conditions in code — don't leave "if approach = X" for the LLM
|
|
107176
106489
|
const noBulletsApproachNames = CREO_APPROACHES.filter(a => a.noBullets).map(a => a.name);
|
|
107177
106490
|
const isNoBullets = noBulletsApproachNames.includes(approachName?.trim() ?? '');
|
|
106491
|
+
const isScreenshotReviews = (approachName?.trim() ?? '') === 'Скрин отзывов';
|
|
107178
106492
|
// ШАГ 0 — hint for BULLETS depends on approach
|
|
107179
106493
|
const step0BulletsHint = isNoBullets
|
|
107180
106494
|
? `BULLETS: [] (для данного подхода буллиты запрещены — ожидается 0)`
|
|
@@ -107195,12 +106509,17 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
|
|
|
107195
106509
|
- Буллиты должны быть расположены вертикально (столбиком). Если буллиты идут горизонтально в одну строку — ОШИБКА: плохая читаемость на мобиле
|
|
107196
106510
|
- Цена визуально ОТЛИЧНА от буллитов (цена — компактный блок без иконок; буллеты — с иконками/галочками). Если цена выглядит как буллет — ОШИБКА.`;
|
|
107197
106511
|
// ШАГ 3 — TEXT LIMIT: PVP has no badges allowed, normal allows urgency/trust (без проверок по длине/количеству слов)
|
|
107198
|
-
const step3TextLimit =
|
|
106512
|
+
const step3TextLimit = isScreenshotReviews
|
|
107199
106513
|
? `ШАГ 3 — OTHER_TEXT (критично):
|
|
106514
|
+
- Для «Скрин отзывов» блок отзывов (аватарки, имена, возраст, 5 звёзд, текст отзывов) — это НЕ ошибка, это основной контент.
|
|
106515
|
+
- Допускаются также trust‑печати (0–3 шт). URGENCY — не рекомендуется, но не ошибка.
|
|
106516
|
+
- На креативе допустимы: HOOK, блок отзывов, цена, скидка, CTA, опционально trust‑печати.`
|
|
106517
|
+
: isNoBullets
|
|
106518
|
+
? `ШАГ 3 — OTHER_TEXT (критично):
|
|
107200
106519
|
- OTHER_TEXT должен быть ПУСТЫМ (0 элементов). Никаких дополнительных бейджей, подписей, ярлыков, trust‑печатей/urgency — НИЧЕГО.
|
|
107201
106520
|
- Любой дополнительный текст (бейджи/подписи/пояснения/urgency/trust‑печати) — ОШИБКА: слишком много текста для punch‑креатива.
|
|
107202
106521
|
- На креативе допустимы ТОЛЬКО: HOOK, цена, скидка, кнопка CTA.`
|
|
107203
|
-
|
|
106522
|
+
: `ШАГ 3 — OTHER_TEXT (критично):
|
|
107204
106523
|
- По умолчанию любой другой рекламный текст (бейджи/подписи/пояснения) запрещён.
|
|
107205
106524
|
- НО допускаются (не обязательно) дополнительные бейджи ВНЕ упаковки — это НЕ ошибка, если ВСЕ элементы OTHER_TEXT подпадают под разрешённые типы:
|
|
107206
106525
|
A) URGENCY (0–1 шт): бейдж срочности/дефицита — только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» и подобные — непонятно что означает.
|
|
@@ -107211,7 +106530,7 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
|
|
|
107211
106530
|
* TRUST‑печати — про натуральность/премиальность/безопасность (не про доставку/поддержку)
|
|
107212
106531
|
Примеры стиля (адаптируй к языку GEO, не требуются):
|
|
107213
106532
|
- URGENCY: «Ostatnie sztuki», «Tylko dziś», «Koniec dziś» (НЕ «24h» — непонятно)
|
|
107214
|
-
- TRUST: «Naturalna formuła», «Wysoka jako
|
|
106533
|
+
- TRUST: «Naturalna formuła», «Wysoka jakość» (для PL); «Ingrediente naturale», «Calitate» (для RO). ЗАПРЕЩЕНО: «NATURAL», «QUALITY», «100% NATURAL» — английские слова.
|
|
107215
106534
|
- Если OTHER_TEXT содержит что-то вне этих типов, или URGENCY > 1, или TRUST > 3 (для Минимализма — TRUST > 1) — ОШИБКА.`;
|
|
107216
106535
|
return `Ты — СТРОГИЙ валидатор рекламных креативов (1:1). Не улучшай и не предлагай идеи — только проверка и вердикт.
|
|
107217
106536
|
Продукт: ${product}. GEO/язык: ${geo}.${approachLine}
|
|
@@ -107233,10 +106552,10 @@ URGENCY_BADGE: "..." (бейдж срочности, если есть; «24h»
|
|
|
107233
106552
|
TRUST_BADGES: ["..."] (печати цветные, яркие, в стиле FDA; про натуральность/премиальность/безопасность; размер как у буллитов — крупные, если есть)
|
|
107234
106553
|
|
|
107235
106554
|
ШАГ 1 — HOOK/HEADLINE (критично):
|
|
107236
|
-
- Должен быть
|
|
107237
|
-
- Допускается 1–
|
|
106555
|
+
- Должен быть ВЫШЕ всех элементов и читабелен (хорошо читается на телефоне)
|
|
106556
|
+
- Допускается 1–4 строки (это НЕ ошибка). Ошибка только если текст нечитабелен или есть разрыв/перенос внутри слова.
|
|
107238
106557
|
- Смысл: релевантно проблеме/выгоде продукта. Допускаются и боль/дискомфорт, и обещание/выгода (например «Könnyebb napok»). НЕ считай ошибкой формулировки обещаний результата или клеймы.
|
|
107239
|
-
-
|
|
106558
|
+
- ОБЯЗАТЕЛЬНО: ключевое слово проблемы явно в формулировке. Примеры по категориям: простата → простатит, простата; похудение → лишний вес, похудение; суставы → боль, колени, скованность; пищеварение → дискомфорт, вздутие; сон → бессонница, усталость. Отсутствие ключевого слова — ОШИБКА. Дополнительно для ориентира: ${keywords.join(', ')} (можно синонимы).
|
|
107240
106559
|
- Проверь вторую часть заголовка. Если она состоит только из абстрактных слов («Mai ușor», «Mai bine», «Soluția», «Ajutor») без конкретного бенефита — отметь: РЕКОМЕНДАЦИЯ: заголовок можно усилить конкретикой
|
|
107241
106560
|
- Если заголовок слишком общий/абстрактный («Mai ușor», «Mai bine» без конкретики) — отметь как РЕКОМЕНДАЦИЯ к улучшению (не ошибка, но слабо)
|
|
107242
106561
|
- РЕКОМЕНДАЦИЯ (CTR): верхний текст лучше делать как HOOK — ОЧЕНЬ крупный, жирный, ВСЕ БУКВЫ ПРОПИСНЫЕ, на яркой контрастной плашке. Если это не так — не ошибка, но отметь рекомендацией
|
|
@@ -107251,7 +106570,7 @@ ${step3TextLimit}
|
|
|
107251
106570
|
ШАГ 4 — CTA > PRICE (критично):
|
|
107252
106571
|
- CTA: на языке ${geo}, хорошо заметная кнопка (позиция НЕ важна: можно вправо/влево/по центру). Главное — CTA читабельна и выглядит как кнопка.
|
|
107253
106572
|
- Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (до скидки) + новая. Если только одна цена — ОШИБКА. Старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Тонкое/незаметное зачёркивание — ОШИБКА.
|
|
107254
|
-
- Скидка ОБЯЗАТЕЛЬНА: «-50%» (
|
|
106573
|
+
- Скидка ОБЯЗАТЕЛЬНА: «-50%» отдельным видимым бейджем (процент должен быть явно читаем, не только в цене). Ярко, заметно. Строго НЕ на упаковке/банке.
|
|
107255
106574
|
- Не считать ошибкой, если скидка/цена конкурируют с CTA по акценту — это допустимо, пока CTA остаётся хорошо заметной и читабельной.
|
|
107256
106575
|
- Если скидка указана и она не равна -50% — ошибка.
|
|
107257
106576
|
|
|
@@ -107264,7 +106583,7 @@ ${step3TextLimit}
|
|
|
107264
106583
|
- КОНТРАСТ ПЛАШЕК: если хотя бы одна текстовая плашка (HOOK, буллеты, CTA или цена) визуально сливается с фоном и текст плохо читается — это ОШИБКА.
|
|
107265
106584
|
- ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА. Цена не должна выглядеть как буллет.
|
|
107266
106585
|
- TRUST‑печати: если печати мелкие или текст нечитабелен на телефоне — ОШИБКА (размер как минимум как у буллитов).
|
|
107267
|
-
- СКИДКА «-50%»:
|
|
106586
|
+
- СКИДКА «-50%»: ОБЯЗАТЕЛЬНО отдельным видимым бейджем (процент явно читаем). Если скидка не отображается или только в цене без отдельного «-50%» — ОШИБКА. Ярко, заметно, НЕ на упаковке.
|
|
107268
106587
|
- ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА.
|
|
107269
106588
|
- Бейджи срочности: «24h» и подобные неясные формулировки — ОШИБКА (непонятно что означает). Ясные («только сегодня», «последние штуки») — ок.
|
|
107270
106589
|
|
|
@@ -107331,18 +106650,17 @@ CRITICAL RULES (это ВАЛИДАЦИЯ, не рекомендации; есл
|
|
|
107331
106650
|
|
|
107332
106651
|
ЯЗЫК + ТОН:
|
|
107333
106652
|
- ВСЕ слова строго на языке ${generateGeo} (HOOK/буллеты/CTA/скидка/опциональный бейдж срочности/опциональные trust‑печати). Английские слова запрещены. Исключений нет.
|
|
107334
|
-
- Тон: убедительный, но честный. Лёгкая эмоция и срочность — ок.
|
|
106653
|
+
- Тон: убедительный, но честный. Лёгкая эмоция и срочность — ок.
|
|
107335
106654
|
|
|
107336
106655
|
HOOK / HEADLINE (строгое правило):
|
|
107337
|
-
-
|
|
107338
|
-
-
|
|
107339
|
-
-
|
|
106656
|
+
- 1–4 строки; до 12 слов. Без переноса внутри слова
|
|
106657
|
+
- ОБЯЗАТЕЛЬНО содержит ключевое слово проблемы ЯВНО. Примеры по категориям: простата → простатит, простата; похудение → лишний вес, похудение; суставы → боль в суставах, колени, скованность; пищеварение → дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Без ключевого слова — ОШИБКА
|
|
106658
|
+
- HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). Самый крупный текстовый элемент на креативе
|
|
107340
106659
|
- HOOK должен быть ВИЗУАЛЬНО “тяжёлым”: ОЧЕНЬ крупный, ТОЛСТЫЙ/жирный шрифт, ВСЕ БУКВЫ ПРОПИСНЫЕ (CAPS), на яркой контрастной плашке/подложке. Это верхний главный “thumb‑stop” элемент
|
|
107341
|
-
- Можно (не обязательно) включить СРОК прямо в HOOK (например «7 dni», «14 dni») как
|
|
107342
|
-
- Заголовок должен быть РЕЗКИМ и призывным: короткие рубленые фразы, вопросы и предупреждения допустимы. Можно использовать обращение на «ты» (в языке GEO: «Masz…», «Twój…») и вопросительный знак
|
|
107343
|
-
- заголовок может содержать мягкое обещание облегчения, но не медицинскую гарантию. ПРАВИЛЬНО: «Nopți fără treziri? Posibil». НЕ ТАК: «Vindecă prostata definitiv».
|
|
106660
|
+
- Можно (не обязательно) включить СРОК прямо в HOOK (например «7 dni», «14 dni») как триггер
|
|
106661
|
+
- Заголовок должен быть РЕЗКИМ и призывным: короткие рубленые фразы, вопросы и предупреждения допустимы. Можно использовать обращение на «ты» (в языке GEO: «Masz…», «Twój…») и вопросительный знак
|
|
107344
106662
|
- заголовок НЕ должен быть абстрактным слоганом/лозунгом или метафорой без конкретной боли. Допускается «thumb‑stop» стиль (вызов/вопрос/предупреждение), если это конкретно и релевантно категории
|
|
107345
|
-
- если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт.
|
|
106663
|
+
- если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт. HOOK всегда остаётся выше всех остальных элементов
|
|
107346
106664
|
- Не повторяй один и тот же заголовок в разных подходах: для текущего подхода придумай новый, уникальный заголовок. Варьируй: угол боли, формулировку, акцент (проблема vs облегчение)
|
|
107347
106665
|
- Вторая часть заголовка = конкретное улучшение, не абстрактное слово. ПРАВИЛЬНО: «Zile fără dureri», «Mișcare ușoară», «Confort activ» (для суставов); «Zile fără disconfort», «Nopți liniștite» (для сна) — адаптируй под категорию. НЕ ТАК: «Mai ușor», «Mai bine», «Soluția»
|
|
107348
106666
|
- Специфические медицинские термины или диагнозы разрешены в заголовке, но не обязательны. Варьируй между прямыми формулировками (если релевантно категории) и мягкими формулировками, фокусирующимися на симптомах и дискомфорте, для разных креативов. Не используй один и тот же термин во всех креативах
|
|
@@ -107351,27 +106669,34 @@ HOOK / HEADLINE (строгое правило):
|
|
|
107351
106669
|
- ТАК (пищеварение): «Puffadás gond? Könnyebb napok»
|
|
107352
106670
|
- ТАК (суставы): «Ízületi fájdalom? Mozogj könnyebben»
|
|
107353
106671
|
- ТАК (сон): «Álmatlan éjszakák? Pihenj végre»
|
|
107354
|
-
|
|
106672
|
+
- ТАК (простата): «Prostată? Alinare rapidă»
|
|
106673
|
+
- ТАК (похудение): «Greutate în plus? Fără diete extreme»
|
|
106674
|
+
❗ Если заголовок > 12 слов, похож на длинное предложение или звучит как лозунг без ключевого слова проблемы — ОШИБКА → пересоздай вариант.
|
|
107355
106675
|
|
|
107356
106676
|
BULLETS (жёсткое правило, ровно 3):
|
|
107357
106677
|
- По умолчанию: ровно 3 буллета
|
|
107358
106678
|
- ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллиты ЗАПРЕЩЕНЫ (0 буллитов). Не добавляй буллет‑блок вообще
|
|
107359
106679
|
- каждый 2–3 слова (макс 4); без запятых; буллет НЕ должен выглядеть как предложение
|
|
107360
|
-
- буллеты = свойства ИЛИ ощущаемые преимущества (комфорт, лёгкость, спокойствие). Запрещено: диагнозы, жёсткие медицинские гарантии («лечит», «вылечивает», «избавляет навсегда»). Разрешено: мягкие результаты («Reduce disconfortul», «Mai puține simptome») — примеры формата, адаптируй под категорию, сроки без гарантий («Efect în 7-14 zile», «Rezultat vizibil rapid»), цифры как соц. доказательство («9 din 10 bărbați recomandă»).
|
|
107361
|
-
- Делай буллеты более активными и конкретными: глаголы действия + сроки + цифры. Примеры формата: «Reduce X», «Ulga od 1. dnia», «Efekt w 7 dni», «93% potwierdza», «3× szybciej» (адаптируй под язык GEO и категорию)
|
|
107362
106680
|
- ВИЗУАЛ БУЛЛЕТОВ: крупные, с иконками/галочками (✓), на контрастных подложках. Буллеты ВНЕ упаковки/банки — не на продукте.
|
|
107363
106681
|
- Буллиты должны быть расположены ВЕРТИКАЛЬНО (столбиком), не горизонтально в одну строку. Минимальный размер шрифта буллитов — чтобы читались на экране телефона без зума
|
|
107364
|
-
|
|
107365
|
-
|
|
107366
|
-
|
|
106682
|
+
|
|
106683
|
+
❗ ВЫБОР БУЛЛЕТОВ (КРИТИЧНО): Выбери СЛУЧАЙНЫЕ 3 буллета из пула ниже в ПРОИЗВОЛЬНОМ порядке. Адаптируй формулировки под язык ${generateGeo} и категорию продукта. НЕ используй один и тот же набор для разных подходов — каждый креатив должен иметь УНИКАЛЬНУЮ комбинацию из 3 буллетов.
|
|
106684
|
+
|
|
106685
|
+
ПУЛ ВАРИАНТОВ (выбери 3 случайных, порядок произвольный; адаптируй под GEO и категорию):
|
|
106686
|
+
• Действие/бенефит: Reduce disconfortul, Reduce balonarea, Reduce durerea, Ulga rapidă, Confort zilnic, Mișcare ușoară, Mai puține simptome, Fără dureri, Zile fără disconfort, Stomac liniștit, Nopți liniștite, Rezultat vizibil
|
|
106687
|
+
• Срок/скорость: Efect în 7-14 zile, Efect în 2 săptămâni, Rezultat rapid, Ulga od 1. dnia, Efekt w 7 dni, 3× szybciej, De la prima zi, În primele zile
|
|
106688
|
+
• Соц. доказательство: 9 din 10 recomandă, 9 din 10 bărbați recomandă, 93% potwierdza, 8 din 10 mulțumiți, Mii de clienți mulțumiți
|
|
106689
|
+
• Формула/состав: Formulă naturală, Ingrediente naturale, Fără chimicale, Compoziție selectată, Testat dermatologic
|
|
106690
|
+
• Качество жизни: Confort activ, Zile fără griji, Mai multă energie, Somn odihnitor, Viață mai ușoară
|
|
106691
|
+
|
|
107367
106692
|
❗ Если хоть один буллет > 4 слов или похож на предложение/обещание — ОШИБКА → пересоздай.
|
|
107368
106693
|
|
|
107369
106694
|
TEXT LIMIT:
|
|
107370
106695
|
- кроме: HOOK, 3 буллетов, цены, скидки, кнопки — ЛЮБОЙ другой текст запрещён (ярлыки/подписи/дисклеймеры/пояснения)
|
|
107371
106696
|
- ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → кроме HOOK, цены, скидки, CTA — НИКАКОГО другого текста. Буллиты запрещены. Other_text/urgency/trust‑печати запрещены
|
|
107372
106697
|
- ДОПУСКАЕТСЯ (не обязательно) ОДИН бейдж срочности: только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» — непонятно что означает.
|
|
107373
|
-
- ДОПУСКАЕТСЯ (не обязательно) 1–3 trust‑печати: печати цветные, яркие,
|
|
107374
|
-
- HOOK + буллеты <=
|
|
106698
|
+
- ДОПУСКАЕТСЯ (не обязательно) 1–3 trust‑печати: печати цветные, яркие, в стиле FDA. Текст СТРОГО на языке ${generateGeo} — запрещены английские слова («NATURAL», «QUALITY» и т.д.). Примеры для RO: «naturale», «Ingrediente naturale»; для PL: «naturalna». Размер как минимум как у буллитов. Мелкие печати — ОШИБКА.
|
|
106699
|
+
- HOOK + буллеты <= 24 слова суммарно (HOOK до 12, буллеты до 12); если больше — ОШИБКА → пересоздай
|
|
107375
106700
|
- Если хочешь добавить ощущение срочности — делай это через формулировки в HOOK/BULLETS, через реквизит/иконки, ИЛИ (не обязательно) через один короткий бейдж срочности. Только ясные формулировки («Ostatnie sztuki», «Tylko dziś», «Koniec dziś»). ЗАПРЕЩЕНО «24h» — непонятно что означает.
|
|
107376
106701
|
|
|
107377
106702
|
CTA > PRICE:
|
|
@@ -107382,17 +106707,17 @@ CTA > PRICE:
|
|
|
107382
106707
|
❗ ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → цена и «-50%» могут быть более крупными и заметными, но CTA всё равно должен оставаться очень заметным и выглядеть как кнопка (не теряется на фоне)
|
|
107383
106708
|
|
|
107384
106709
|
ПОКАЗЫВАЙ ТОЛЬКО ЭТИ ТЕКСТОВЫЕ ЭЛЕМЕНТЫ:
|
|
107385
|
-
- HOOK (1
|
|
106710
|
+
- HOOK (1–4 строки; выше всех элементов; самый крупный; CAPS; жирный; на яркой контрастной подложке; ключевое слово проблемы явно)
|
|
107386
106711
|
- 3 буллета (крупные, с иконками/галочками ✓, на контрастных подложках, НЕ на банке/упаковке)
|
|
107387
106712
|
- Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (2×${generatePrice} ${generateCurrency}) зачёркнута ТОЛСТОЙ контрастной линией + новая ${generatePrice} ${generateCurrency} выразительно. Одна цена = ОШИБКА. Тонкая линия зачёркивания = ОШИБКА. Без слов. Не на упаковке.
|
|
107388
|
-
- Скидка: «-50%»
|
|
106713
|
+
- Скидка: «-50%» ОБЯЗАТЕЛЬНО отдельным видимым бейджем (не только в цене!). Процент скидки должен быть явно читаем. Ярко, заметно, строго НЕ на банке/упаковке, визуально слабее CTA
|
|
107389
106714
|
- Кнопка CTA (1-2 слова)
|
|
107390
106715
|
- Опционально: 1 короткий бейдж срочности (ясные формулировки: «только сегодня», «последние штуки»; ЗАПРЕЩЕНО «24h»). Бейдж в углу кадра, контрастный, но меньше CTA. НЕ обязателен
|
|
107391
|
-
- Опционально: 1–3 trust‑печати (цветные, яркие,
|
|
106716
|
+
- Опционально: 1–3 trust‑печати (цветные, яркие, в стиле FDA; натуральность, премиальность, безопасность). Текст СТРОГО на языке ${generateGeo} — никакого английского. Для RO: «naturale», «Ingrediente naturale», «Calitate»; для PL: «naturalna», «naturalne»; НЕ «NATURAL», «QUALITY». Размер как минимум как у буллитов. НЕ обязательны
|
|
107392
106717
|
- ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → показывай только: HOOK + PRICE + DISCOUNT + CTA. Буллиты/urgency/trust‑печати запрещены
|
|
107393
106718
|
|
|
107394
106719
|
ВИЗУАЛ:
|
|
107395
|
-
- Иерархия: HOOK
|
|
106720
|
+
- Иерархия: HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). HOOK — самый крупный текстовый элемент
|
|
107396
106721
|
- Буллеты: крупные, с иконками/галочками (✓), на контрастных подложках. Строго ВНЕ упаковки/банки.
|
|
107397
106722
|
- Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание = ОШИБКА. Визуально ОТЛИЧНА от буллитов.
|
|
107398
106723
|
- Скидка «-50%»: ярко, заметно, на контрастной подложке. Строго ВНЕ упаковки/банки.
|
|
@@ -107402,8 +106727,9 @@ CTA > PRICE:
|
|
|
107402
106727
|
- Без ссылок/доменов и мелкого текста
|
|
107403
106728
|
- LIFESTYLE/CLEAN: продукт в контексте использования. Для пищеварения — кухня, обеденный стол, рядом с едой/чашкой. Для сна/простаты — спальня, тумбочка, стакан воды. Для суставов — стол/тумбочка рядом с эластичным бинтом, ортезом или активным фоном (кроссовки, лестница). Контекст = момент приёма. Просто продукт на белом фоне — НЕ допускается
|
|
107404
106729
|
- Цвета и композиция должны быть «thumb‑stop»: высокий контраст, яркий акцент на продукте (луч света/подсветка/обводка/глоу), избегай бледных пастельных сцен
|
|
107405
|
-
- ЛЮДИ: человек допускается ТОЛЬКО для
|
|
107406
|
-
- Если
|
|
106730
|
+
- ЛЮДИ: человек допускается ТОЛЬКО для подходов «Эмоция / Портрет» и «Врач / Эксперт». Для всех остальных — БЕЗ человека.
|
|
106731
|
+
- Если «Эмоция / Портрет»: прямой взгляд в камеру, эмоция облегчения/надежды. Возраст 50–60 (похудение/фитнес — 35–55). Этнически соответствует GEO/рынку.
|
|
106732
|
+
- Если «Врач / Эксперт»: женщина-врач 40–55 лет. Внешность и этничность СТРОГО соответствуют GEO/рынку (локал — врач выглядит как местный специалист).
|
|
107407
106733
|
|
|
107408
106734
|
ANTI-TEMPLATE DIVERSITY (КРИТИЧНО):
|
|
107409
106735
|
- Ты сейчас создаёшь ОДИН креатив для текущего подхода. В проекте будет серия из ${totalApproaches} креативов, поэтому у каждого подхода должна быть своя узнаваемая композиция.
|
|
@@ -107411,15 +106737,17 @@ ANTI-TEMPLATE DIVERSITY (КРИТИЧНО):
|
|
|
107411
106737
|
- Ориентируйся на строку «🎯 ПОДХОД: ...» и примени соответствующую раскладку ниже (только одну, соответствующую текущему подходу):
|
|
107412
106738
|
* 🎯 ПОДХОД: Эксперт / Авторитет → БЕЗ человека. Профессиональный контекст с реквизитом экспертизы вокруг продукта. Продукт в центре. HOOK сверху справа, BULLETS справа ниже, CTA снизу справа, PRICE/DISCOUNT рядом с CTA (слабее CTA). Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
|
|
107413
106739
|
* 🎯 ПОДХОД: Lifestyle / Момент приёма → FLAT-LAY/СВЕРХУ или 3/4 сверху на столе/тумбочке. Продукт в центре, реквизит "момент приёма" вокруг. HOOK сверху по центру, BULLETS сбоку (вертикально, шрифт достаточно крупный для чтения на телефоне без зума), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Urgency‑бейдж (если есть) — в углу, только ясные формулировки, НЕ «24h». Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
|
|
107414
|
-
* 🎯 ПОДХОД: Эмоция / Портрет →
|
|
106740
|
+
* 🎯 ПОДХОД: Эмоция / Портрет → Один из двух подходов с человеком. ОЧЕНЬ крупный портрет лица (thumb‑stop), взгляд в камеру — лицо занимает верхние 2/3 кадра. Продукт в руке или у подбородка. HOOK сверху по центру. BULLETS, CTA, PRICE/DISCOUNT, trust‑печати — в нижней трети.
|
|
106741
|
+
* 🎯 ПОДХОД: Врач / Эксперт → Женщина-врач (локал по GEO), продукт в руках или рядом. Профессиональный фон. HOOK сверху, BULLETS сбоку/снизу, CTA и PRICE/DISCOUNT внизу. Врач и продукт — главные элементы.
|
|
106742
|
+
* 🎯 ПОДХОД: Скрин отзывов → Имитация скриншота: блок отзывов (аватарки, имена, возраст, 5 звёзд, текст на языке GEO). HOOK ОБЯЗАТЕЛЕН — выше отзывов или поверх, крупно. Продукт виден. CTA, PRICE, DISCOUNT внизу.
|
|
107415
106743
|
* 🎯 ПОДХОД: Визуализация проблемы → Инфографика/схема: слева проблема (иконка/схема зоны тела, релевантной продукту), справа решение + продукт. HOOK сверху по центру, BULLETS справа или снизу (вертикально), CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне). Красный акцент только на проблеме.
|
|
107416
106744
|
* 🎯 ПОДХОД: Power / Сила решения → БЕЗ человека. ДИНАМИЧНЫЙ комикс‑кадр, диагональная композиция. Продукт как “герой” + power‑иконки (щит/молния/бёрст). HOOK сверху слева на яркой плашке, BULLETS слева ниже, CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
|
|
107417
106745
|
* 🎯 ПОДХОД: Минимализм / Clean Big Text → Белый/градиентный фон, минимум элементов. ОГРОМНЫЙ HOOK занимает верх/центр, продукт крупно (центр/право), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Trust‑печати (если есть) — размер как минимум как у буллитов, крупные и читабельные.
|
|
107418
|
-
* 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK
|
|
107419
|
-
* 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — крупный, грубый bold/рукописный, CAPS,
|
|
106746
|
+
* 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK 1–4 строки, до 12 слов, огромный CAPS, выше всех. БЕЗ буллитов. PRICE + «-50%» крупно и заметно. CTA контрастной кнопкой
|
|
106747
|
+
* 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — 1–4 строки, до 12 слов, крупный, грубый bold/рукописный, CAPS, выше всех. Крупные надписи цены/скидки вокруг продукта. CTA — плоская кнопка без градиентов. БЕЗ буллитов, бейджей, теней, иконок.
|
|
107420
106748
|
|
|
107421
106749
|
AUTO-CHECK (перед финалом):
|
|
107422
|
-
- HOOK:
|
|
106750
|
+
- HOOK: 1–4 строки, до 12 слов, CAPS, жирный на контрастной плашке, выше всех элементов, ключевое слово проблемы явно, язык ${generateGeo} без английских слов
|
|
107423
106751
|
- Буллеты: по умолчанию ровно 3, каждый <= 4 слов, без запятых, не предложения. ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллетов 0 (запрещены)
|
|
107424
106752
|
- Нет лишнего текста кроме: HOOK, буллетов, цены, скидки, кнопки (+ опционально 1 бейдж срочности + опционально 1–3 trust‑печати)
|
|
107425
106753
|
- Есть скидка «-50%» (вне упаковки) и она слабее CTA
|
|
@@ -107463,38 +106791,38 @@ const CREO_APPROACHES = [
|
|
|
107463
106791
|
{
|
|
107464
106792
|
name: 'Эксперт / Авторитет',
|
|
107465
106793
|
prompt: `ЭКСПЕРТ / АВТОРИТЕТ (без человека): стиль “рекомендация эксперта”, но БЕЗ человека. Профессиональный контекст — аптечные полки на фоне / медицинский планшет / стетоскоп / рецептурный блокнот как реквизит рядом с продуктом. Продукт в центре как “рекомендованное решение”. Чистый профессиональный фон, высокий контраст. Trust‑печати (1–3 шт): цветные, яркие, очень похожие на печать FDA; подчёркивают натуральность состава, премиальность, безопасность. Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная.`,
|
|
107466
|
-
headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO).
|
|
107467
|
-
bulletsFocus: `БУЛЛИТЫ:
|
|
106794
|
+
headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
|
|
106795
|
+
bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор для этого подхода — не повторяй комбинации из других креативов.`
|
|
107468
106796
|
},
|
|
107469
106797
|
{
|
|
107470
106798
|
name: 'Lifestyle / Момент приёма',
|
|
107471
106799
|
prompt: `LIFESTYLE / МОМЕНТ ПРИЁМА: продукт в контексте использования (кухня/стол/спальня/тумбочка). Без человека, но с "историей" (стакан воды, чай, тарелка, будильник/часы/календарь). Срочность: реквизит (часы/таймер‑иконка) и/или опциональный urgency‑бейдж (1–3 слова) в углу кадра — только ясные формулировки («только сегодня», «последние штуки»), ЗАПРЕЩЕНО «24h». Высокий контраст, яркий акцент на продукте. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ВАЖНО: буллиты располагаются вертикально и всегда читабельны на экране телефона без зума — даже если уходят вбок, минимальный размер шрифта буллитов не уменьшается. Если буллиты не вмещаются сбоку с нужным шрифтом — переставь их вниз.`,
|
|
107472
|
-
headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO),
|
|
107473
|
-
bulletsFocus: `БУЛЛИТЫ:
|
|
106800
|
+
headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
|
|
106801
|
+
bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (срок + цифра + действие/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
|
|
107474
106802
|
},
|
|
107475
106803
|
{
|
|
107476
106804
|
name: 'Эмоция / Портрет',
|
|
107477
|
-
prompt: `ЭМОЦИЯ / ПОРТРЕТ:
|
|
107478
|
-
headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение».
|
|
107479
|
-
bulletsFocus: `БУЛЛИТЫ:
|
|
106805
|
+
prompt: `ЭМОЦИЯ / ПОРТРЕТ: один из двух подходов с человеком в серии. Используй максимально: ОЧЕНЬ крупный план лица (thumb‑stop), прямой взгляд в камеру, сильная эмоция облегчения/надежды (без широкой стоковой улыбки). Возраст человека подбери под категорию и ЦА продукта: по умолчанию 50–60, но для категорий с более молодой аудиторией (например похудение/фитнес) допускается 35–55. Продукт в руке на уровне лица или рядом, хорошо виден. Свет "тень → свет" на лице допустим. Минимум отвлекающих деталей, высокий контраст. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ИЕРАРХИЯ: лицо — главный и доминирующий визуальный элемент (верхние 2/3 кадра). Все текстовые блоки (HOOK исключение — сверху) уходят в нижнюю треть. Буллиты, цена, CTA НЕ перекрывают лицо и НЕ конкурируют с ним по визуальному весу — они заметно меньше и ниже.`,
|
|
106806
|
+
headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно. Тон максимально личный и прямой.`,
|
|
106807
|
+
bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (качество жизни + действие + срок/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
|
|
107480
106808
|
},
|
|
107481
106809
|
{
|
|
107482
106810
|
name: 'Визуализация проблемы',
|
|
107483
106811
|
prompt: `ВИЗУАЛИЗАЦИЯ ПРОБЛЕМЫ (без человека): схема/иконка проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав/позвоночник; для пищеварения — желудок; для простаты — силуэт мужчины) + мягкий красный акцент на этой зоне (не шок‑контент, без графики). Рядом продукт как решение. Можно добавить простую стрелку/переход “проблема → облегчение” как графику (без лишнего текста). Чистый фон, высокая читабельность. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека.`,
|
|
107484
|
-
headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории).
|
|
107485
|
-
bulletsFocus: `БУЛЛИТЫ:
|
|
106812
|
+
headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно, вопросительный знак допустим.`,
|
|
106813
|
+
bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
|
|
107486
106814
|
},
|
|
107487
106815
|
{
|
|
107488
106816
|
name: 'Power / Сила решения',
|
|
107489
106817
|
prompt: `POWER / СИЛА РЕШЕНИЯ (без человека): метафоры силы и победы над проблемой: щит, молния, энергия, взрыв‑бёрст, мощные стикеры/иконки. Продукт как “герой” в центре, высокая энергия, контрастные агрессивные цвета. Можно комикс/anti‑design подачу, но всё должно быть читабельно. Без лишнего текста. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека. Только продукт + power‑иконки.`,
|
|
107490
|
-
headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода.
|
|
107491
|
-
bulletsFocus: `БУЛЛИТЫ:
|
|
106818
|
+
headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
|
|
106819
|
+
bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + скорость + цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
|
|
107492
106820
|
},
|
|
107493
106821
|
{
|
|
107494
106822
|
name: 'Минимализм / Clean Big Text',
|
|
107495
106823
|
prompt: `МИНИМАЛИЗМ / CLEAN BIG TEXT (без человека): белый или мягкий градиентный фон, премиальное ощущение. ОГРОМНЫЙ HOOK как главный элемент (CAPS, жирный, на контрастной плашке). Продукт крупно, минимум реквизита, минимум шума. Тени/премиальные материалы допустимы, но без лишнего текста. БЕЗ человека. Urgency и trust‑печати в этом подходе НЕ рекомендуются — они нарушают чистоту и ощущение премиальности. Добавляй печати только если это критически необходимо, не более 1, но крупную — размер как у буллитов.`,
|
|
107496
|
-
headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно),
|
|
107497
|
-
bulletsFocus: `БУЛЛИТЫ:
|
|
106824
|
+
headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
|
|
106825
|
+
bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + формула/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
|
|
107498
106826
|
},
|
|
107499
106827
|
{
|
|
107500
106828
|
name: 'Problem Visual Punch',
|
|
@@ -107503,14 +106831,14 @@ const CREO_APPROACHES = [
|
|
|
107503
106831
|
- Фон: яркий сплошной цвет (красный/оранжевый) или агрессивный градиент
|
|
107504
106832
|
- Центр: КРУПНАЯ иконка/схема проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав; для простаты — силуэт мужчины; для пищеварения — желудок), с красным свечением
|
|
107505
106833
|
- Продукт: крупно рядом
|
|
107506
|
-
- HOOK:
|
|
106834
|
+
- HOOK: 1–4 строки, до 12 слов, огромный, CAPS, ключевое слово проблемы явно
|
|
107507
106835
|
- БЕЗ буллитов
|
|
107508
106836
|
- Цена + скидка: крупно, заметно
|
|
107509
106837
|
- CTA: контрастная яркая кнопка
|
|
107510
106838
|
|
|
107511
106839
|
Цель: считывание за 1 секунду. Минимум текста, максимум визуального удара.
|
|
107512
106840
|
БЕЗ человека. Никаких trust‑печатей/urgency бейджей — только HOOK + PRICE + -50% + CTA.`,
|
|
107513
|
-
headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления.
|
|
106841
|
+
headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
|
|
107514
106842
|
bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK и крупных надписях.`
|
|
107515
106843
|
},
|
|
107516
106844
|
{
|
|
@@ -107525,8 +106853,33 @@ const CREO_APPROACHES = [
|
|
|
107525
106853
|
- Крупные текстовые надписи рядом с продуктом: цена, скидка; допустимо одно короткое слово-восклицание («РАБОТАЕТ!», «ПРОВЕРЕНО!») на языке GEO
|
|
107526
106854
|
- Кнопка CTA: плоская, без градиентов, контрастный сплошной цвет
|
|
107527
106855
|
- НИКАКИХ буллитов, trust‑печатей, urgency‑бейджей, иконок — только продукт и текст`,
|
|
107528
|
-
headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений.
|
|
106856
|
+
headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
|
|
107529
106857
|
bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Всё — в HOOK и крупных надписях вокруг продукта (цена, скидка, одно слово-восклицание).`
|
|
106858
|
+
},
|
|
106859
|
+
{
|
|
106860
|
+
name: 'Врач / Эксперт',
|
|
106861
|
+
prompt: `ВРАЧ / ЭКСПЕРТ (с человеком): женщина-врач как авторитет, рекомендующий продукт. Один из двух подходов с человеком в серии.
|
|
106862
|
+
- Врач: женщина 40–55 лет, профессиональный вид (медицинский халат, планшет/стетоскоп/рецептурный блокнот). Уверенная, располагающая поза.
|
|
106863
|
+
- ЛОКАЛИЗАЦИЯ (КРИТИЧНО): врач НЕ generic — явно угадывается регион/GEO. Черты лица, тип кожи, причёска — характерные для целевого рынка. RO → румынский тип; PL → польский; HU → венгерский; ES → испанский/латино; IT → итальянский. Врач = местный специалист, не стоковое универсальное лицо.
|
|
106864
|
+
- Продукт: в руках врача или на столе/планшете рядом, хорошо виден.
|
|
106865
|
+
- Фон: профессиональный (кабинет, аптечные полки, медицинский контекст). Высокий контраст, читабельность.
|
|
106866
|
+
- Опционально: 1–3 trust‑печати по низу (цветные, яркие, в стиле FDA). Размер как минимум как у буллитов.
|
|
106867
|
+
- ИЕРАРХИЯ: врач + продукт — главные элементы. HOOK сверху, BULLETS сбоку или снизу, CTA и PRICE/DISCOUNT в нижнем блоке.`,
|
|
106868
|
+
headlineAngle: `HOOK: угол «врач/специалист рекомендует» или «эксперты знают». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
|
|
106869
|
+
bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
|
|
106870
|
+
},
|
|
106871
|
+
{
|
|
106872
|
+
name: 'Скрин отзывов',
|
|
106873
|
+
noBullets: true,
|
|
106874
|
+
prompt: `СКРИН ОТЗЫВОВ: имитация реалистичного скриншота с положительными благодарными отзывами и оценками 5 звёзд.
|
|
106875
|
+
- Визуал: реалистичный стиль — как скриншот приложения/сайта отзывов (App Store, Google Play, или страница отзывов). Высокое качество, читабельный текст.
|
|
106876
|
+
- Отзывы: 2–4 отзыва с аватарками, именами и возрастом (формат «Имя, 45 лет»), ОБЯЗАТЕЛЬНО 5 звёзд (★★★★★) в каждом, короткий текст благодарности. Текст отзывов СТРОГО на языке GEO — никакого английского. Релевантен категории продукта.
|
|
106877
|
+
- HOOK ОБЯЗАТЕЛЕН: крупно, выше блока отзывов или поверх, CAPS, на яркой подложке. Ключевое слово проблемы явно.
|
|
106878
|
+
- Продукт: виден в кадре (рядом со скрином или интегрирован в композицию).
|
|
106879
|
+
- Цена, скидка «-50%», CTA — в нижней части. БЕЗ буллитов — отзывы заменяют их.
|
|
106880
|
+
- Стиль: не мультяшный, не комикс — максимально реалистичный скрин.`,
|
|
106881
|
+
headlineAngle: `HOOK: угол «тысячи довольны» / «9 из 10 рекомендуют» / «реальный результат». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
|
|
106882
|
+
bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK, отзывах (с 5 звёздами), цене и CTA.`
|
|
107530
106883
|
}
|
|
107531
106884
|
];
|
|
107532
106885
|
|