docs-combiner 0.1.4 → 0.1.6
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 +374 -80
- package/dist/renderer.js.map +1 -1
- package/package.json +1 -1
package/dist/renderer.js
CHANGED
|
@@ -89973,6 +89973,13 @@ function App() {
|
|
|
89973
89973
|
return saved || _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.imageGeneration;
|
|
89974
89974
|
});
|
|
89975
89975
|
const [loadingImageModels, setLoadingImageModels] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
89976
|
+
const [validationModels, setValidationModels] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
89977
|
+
const [selectedValidationModel, setSelectedValidationModel] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(() => {
|
|
89978
|
+
// Load from localStorage or use default
|
|
89979
|
+
const saved = localStorage.getItem('selectedValidationModel');
|
|
89980
|
+
return saved || _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation;
|
|
89981
|
+
});
|
|
89982
|
+
const [loadingValidationModels, setLoadingValidationModels] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
89976
89983
|
// Helper function to parse price and currency from combined field
|
|
89977
89984
|
const parsePriceAndCurrency = (value) => {
|
|
89978
89985
|
const trimmed = value.trim();
|
|
@@ -90263,10 +90270,32 @@ function App() {
|
|
|
90263
90270
|
const cachedGenerateGeo = localStorage.getItem('cached_generateGeo');
|
|
90264
90271
|
const cachedGenerateAdditionalInfo = localStorage.getItem('cached_generateAdditionalInfo');
|
|
90265
90272
|
const cachedGeneratePriceWithCurrency = localStorage.getItem('cached_generatePriceWithCurrency');
|
|
90266
|
-
if (cachedTitles)
|
|
90273
|
+
if (cachedTitles) {
|
|
90267
90274
|
setTitles(cachedTitles);
|
|
90268
|
-
|
|
90269
|
-
|
|
90275
|
+
// Restore generatedTitlesData from cached titles string
|
|
90276
|
+
const titlesArray = cachedTitles.split('\n').map(t => t.trim()).filter(t => t);
|
|
90277
|
+
if (titlesArray.length > 0) {
|
|
90278
|
+
setGeneratedTitlesData(Array.from({ length: Math.max(3, titlesArray.length) }, (_, index) => ({
|
|
90279
|
+
index: index + 1,
|
|
90280
|
+
title: titlesArray[index] || '',
|
|
90281
|
+
generating: false,
|
|
90282
|
+
failed: !titlesArray[index]
|
|
90283
|
+
})));
|
|
90284
|
+
}
|
|
90285
|
+
}
|
|
90286
|
+
if (cachedTexts) {
|
|
90287
|
+
const textsArray = JSON.parse(cachedTexts);
|
|
90288
|
+
setTexts(textsArray);
|
|
90289
|
+
// Restore generatedTextsData from cached texts array
|
|
90290
|
+
if (Array.isArray(textsArray) && textsArray.length > 0) {
|
|
90291
|
+
setGeneratedTextsData(Array.from({ length: Math.max(3, textsArray.length) }, (_, index) => ({
|
|
90292
|
+
index: index + 1,
|
|
90293
|
+
text: textsArray[index] || '',
|
|
90294
|
+
generating: false,
|
|
90295
|
+
failed: !textsArray[index]
|
|
90296
|
+
})));
|
|
90297
|
+
}
|
|
90298
|
+
}
|
|
90270
90299
|
if (cachedDriveFolderUrl)
|
|
90271
90300
|
setDriveFolderUrl(cachedDriveFolderUrl);
|
|
90272
90301
|
if (cachedBrand)
|
|
@@ -90312,6 +90341,28 @@ function App() {
|
|
|
90312
90341
|
// Silent fail
|
|
90313
90342
|
}
|
|
90314
90343
|
}, [titles, texts, driveFolderUrl, brand, link, generateProduct, generateGeo, generateAdditionalInfo, generatePriceWithCurrency]);
|
|
90344
|
+
// Sync generatedTitlesData with titles when titles changes (except when user is editing in UI)
|
|
90345
|
+
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
90346
|
+
// Only sync if titles has content and generatedTitlesData is empty or out of sync
|
|
90347
|
+
if (titles) {
|
|
90348
|
+
const titlesArray = titles.split('\n').map(t => t.trim()).filter(t => t);
|
|
90349
|
+
const currentTitles = generatedTitlesData.map(t => t.title).filter(Boolean);
|
|
90350
|
+
const titlesString = currentTitles.join('\n');
|
|
90351
|
+
// Only update if titles actually changed and we have titles to restore
|
|
90352
|
+
if (titlesString !== titles && titlesArray.length > 0) {
|
|
90353
|
+
setGeneratedTitlesData(Array.from({ length: Math.max(3, titlesArray.length) }, (_, index) => ({
|
|
90354
|
+
index: index + 1,
|
|
90355
|
+
title: titlesArray[index] || '',
|
|
90356
|
+
generating: false,
|
|
90357
|
+
failed: !titlesArray[index]
|
|
90358
|
+
})));
|
|
90359
|
+
}
|
|
90360
|
+
}
|
|
90361
|
+
else if (titles === '' && generatedTitlesData.length > 0 && generatedTitlesData.some(t => t.title)) {
|
|
90362
|
+
// If titles is cleared, clear generatedTitlesData too
|
|
90363
|
+
setGeneratedTitlesData([]);
|
|
90364
|
+
}
|
|
90365
|
+
}, [titles]); // Only depend on titles, not generatedTitlesData to avoid loops
|
|
90315
90366
|
// Auto-scroll logs when new logs are added
|
|
90316
90367
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
90317
90368
|
if (productGenerationLogs.length > 0 && (generatingProduct || uploadingProduct)) {
|
|
@@ -90461,6 +90512,7 @@ function App() {
|
|
|
90461
90512
|
return;
|
|
90462
90513
|
}
|
|
90463
90514
|
fetchImageModels();
|
|
90515
|
+
fetchValidationModels();
|
|
90464
90516
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90465
90517
|
}, [openaiApiKey]);
|
|
90466
90518
|
// Check folder files when driveFolderUrl changes
|
|
@@ -90748,6 +90800,7 @@ function App() {
|
|
|
90748
90800
|
if (val) {
|
|
90749
90801
|
fetchOpenRouterBalance(val);
|
|
90750
90802
|
fetchImageModels(val);
|
|
90803
|
+
fetchValidationModels(val);
|
|
90751
90804
|
}
|
|
90752
90805
|
else {
|
|
90753
90806
|
setOpenRouterBalance(null);
|
|
@@ -90759,6 +90812,10 @@ function App() {
|
|
|
90759
90812
|
setSelectedImageModel(modelId);
|
|
90760
90813
|
localStorage.setItem('selectedImageModel', modelId);
|
|
90761
90814
|
};
|
|
90815
|
+
const handleValidationModelChange = (modelId) => {
|
|
90816
|
+
setSelectedValidationModel(modelId);
|
|
90817
|
+
localStorage.setItem('selectedValidationModel', modelId);
|
|
90818
|
+
};
|
|
90762
90819
|
// Fetch OpenRouter account balance and key limit
|
|
90763
90820
|
const fetchOpenRouterBalance = async (apiKey) => {
|
|
90764
90821
|
const keyToUse = apiKey ?? openaiApiKey;
|
|
@@ -90800,8 +90857,6 @@ function App() {
|
|
|
90800
90857
|
}
|
|
90801
90858
|
})
|
|
90802
90859
|
]);
|
|
90803
|
-
logToTerminal('log', `💰 Credits response status: ${creditsResponse.status} ${creditsResponse.statusText}`);
|
|
90804
|
-
logToTerminal('log', `💰 Key response status: ${keyResponse.status} ${keyResponse.statusText}`);
|
|
90805
90860
|
// Process account balance (remaining credits)
|
|
90806
90861
|
if (creditsResponse.ok) {
|
|
90807
90862
|
const contentType = creditsResponse.headers.get('content-type') || '';
|
|
@@ -90888,7 +90943,6 @@ function App() {
|
|
|
90888
90943
|
setOpenRouterBalance(limitValue);
|
|
90889
90944
|
}
|
|
90890
90945
|
else if (limit === null || limit === undefined) {
|
|
90891
|
-
logToTerminal('log', '✅ No limit set (unlimited)');
|
|
90892
90946
|
setOpenRouterBalance(-1);
|
|
90893
90947
|
}
|
|
90894
90948
|
else {
|
|
@@ -90910,14 +90964,12 @@ function App() {
|
|
|
90910
90964
|
const limit = keyData.data?.limit ??
|
|
90911
90965
|
keyData.limit ??
|
|
90912
90966
|
null;
|
|
90913
|
-
logToTerminal('log', `💰 Key limit remaining: ${limitRemaining}, limit: ${limit}`);
|
|
90914
90967
|
if (limitRemaining !== null && limitRemaining !== undefined) {
|
|
90915
90968
|
const limitValue = typeof limitRemaining === 'number' ? limitRemaining : parseFloat(limitRemaining);
|
|
90916
90969
|
logToTerminal('log', `✅ Key limit found: ${limitValue.toFixed(4)}`);
|
|
90917
90970
|
setOpenRouterBalance(limitValue);
|
|
90918
90971
|
}
|
|
90919
90972
|
else if (limit === null || limit === undefined) {
|
|
90920
|
-
logToTerminal('log', '✅ No limit set (unlimited)');
|
|
90921
90973
|
setOpenRouterBalance(-1); // Use -1 to indicate unlimited
|
|
90922
90974
|
}
|
|
90923
90975
|
else {
|
|
@@ -91017,6 +91069,72 @@ function App() {
|
|
|
91017
91069
|
setLoadingImageModels(false);
|
|
91018
91070
|
}
|
|
91019
91071
|
};
|
|
91072
|
+
// Fetch available validation models from OpenRouter (models that support image analysis)
|
|
91073
|
+
const fetchValidationModels = async (apiKey) => {
|
|
91074
|
+
const keyToUse = apiKey ?? openaiApiKey;
|
|
91075
|
+
if (!keyToUse) {
|
|
91076
|
+
setValidationModels([]);
|
|
91077
|
+
return;
|
|
91078
|
+
}
|
|
91079
|
+
setLoadingValidationModels(true);
|
|
91080
|
+
try {
|
|
91081
|
+
logToTerminal('log', '🔍 Fetching available validation models (image analysis) from OpenRouter...');
|
|
91082
|
+
const response = await fetch('https://openrouter.ai/api/v1/models', {
|
|
91083
|
+
method: 'GET',
|
|
91084
|
+
headers: {
|
|
91085
|
+
'Authorization': `Bearer ${keyToUse}`,
|
|
91086
|
+
'HTTP-Referer': window.location.origin || 'https://docs-combiner.app',
|
|
91087
|
+
'X-Title': 'Docs Combiner',
|
|
91088
|
+
'Accept': 'application/json'
|
|
91089
|
+
}
|
|
91090
|
+
});
|
|
91091
|
+
if (!response.ok) {
|
|
91092
|
+
logToTerminal('warn', `⚠️ Failed to fetch validation models. Status: ${response.status}`);
|
|
91093
|
+
setValidationModels([]);
|
|
91094
|
+
return;
|
|
91095
|
+
}
|
|
91096
|
+
const data = await response.json();
|
|
91097
|
+
logToTerminal('log', `✅ Fetched ${data.data?.length || 0} models from OpenRouter`);
|
|
91098
|
+
// Filter models that support image analysis (vision) - they need to accept image_url in content
|
|
91099
|
+
const validationModelsList = (data.data || [])
|
|
91100
|
+
.filter((model) => {
|
|
91101
|
+
// Check if model supports vision/image analysis
|
|
91102
|
+
const modalities = model.modalities || [];
|
|
91103
|
+
const supportsVision = modalities.includes('image') || modalities.includes('image_url') || modalities.includes('vision');
|
|
91104
|
+
// Also check if model supports chat completions with images (most GPT models)
|
|
91105
|
+
const supportsChatWithImages = model.id && (model.id.includes('gpt') ||
|
|
91106
|
+
model.id.includes('claude') ||
|
|
91107
|
+
model.id.includes('gemini'));
|
|
91108
|
+
return supportsVision || supportsChatWithImages;
|
|
91109
|
+
})
|
|
91110
|
+
.map((model) => ({
|
|
91111
|
+
id: model.id,
|
|
91112
|
+
name: model.name || model.id
|
|
91113
|
+
}))
|
|
91114
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
91115
|
+
logToTerminal('log', `✅ Found ${validationModelsList.length} validation models`);
|
|
91116
|
+
setValidationModels(validationModelsList);
|
|
91117
|
+
// If we have models and the current selection is not in the list, reset to default
|
|
91118
|
+
if (validationModelsList.length > 0) {
|
|
91119
|
+
const currentModelExists = validationModelsList.some((m) => m.id === selectedValidationModel);
|
|
91120
|
+
if (!currentModelExists) {
|
|
91121
|
+
// Try to use default model, or first available
|
|
91122
|
+
const defaultModel = validationModelsList.find((m) => m.id === _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation) || validationModelsList[0];
|
|
91123
|
+
if (defaultModel) {
|
|
91124
|
+
setSelectedValidationModel(defaultModel.id);
|
|
91125
|
+
localStorage.setItem('selectedValidationModel', defaultModel.id);
|
|
91126
|
+
}
|
|
91127
|
+
}
|
|
91128
|
+
}
|
|
91129
|
+
}
|
|
91130
|
+
catch (error) {
|
|
91131
|
+
logToTerminal('error', '❌ Failed to fetch validation models:', error instanceof Error ? error.message : String(error));
|
|
91132
|
+
setValidationModels([]);
|
|
91133
|
+
}
|
|
91134
|
+
finally {
|
|
91135
|
+
setLoadingValidationModels(false);
|
|
91136
|
+
}
|
|
91137
|
+
};
|
|
91020
91138
|
const generateWithGPT = async (product, geo, additionalInfo, type, addLog) => {
|
|
91021
91139
|
const logMsg = (level, ...args) => {
|
|
91022
91140
|
logToTerminal(level, ...args);
|
|
@@ -91525,17 +91643,30 @@ function App() {
|
|
|
91525
91643
|
referenceImageApiUrls.forEach(url => {
|
|
91526
91644
|
content.push({ type: 'image_url', image_url: { url } });
|
|
91527
91645
|
});
|
|
91646
|
+
// Log request details for debugging
|
|
91647
|
+
logToTerminal('log', `📤 generateImageWithDALLE: prompt length=${prompt.length}, reference images=${referenceImageApiUrls.length}`);
|
|
91648
|
+
if (referenceImageApiUrls.length > 0) {
|
|
91649
|
+
logToTerminal('log', `📤 Reference images: ${referenceImageApiUrls.map(url => url.substring(0, 50) + '...').join(', ')}`);
|
|
91650
|
+
}
|
|
91651
|
+
logToTerminal('log', `📤 Content array length: ${content.length}, has images: ${content.some(c => c.type === 'image_url')}`);
|
|
91528
91652
|
// According to OpenRouter docs: use /api/v1/chat/completions with modalities parameter
|
|
91653
|
+
// Always use content array format when we have images, even if only one element
|
|
91654
|
+
// For image generation models, content must be an array with text and image_url objects
|
|
91655
|
+
const finalContent = referenceImageApiUrls.length > 0 ? content : prompt;
|
|
91529
91656
|
const requestBody = {
|
|
91530
|
-
model:
|
|
91657
|
+
model: selectedImageModel,
|
|
91531
91658
|
messages: [
|
|
91532
91659
|
{
|
|
91533
91660
|
role: 'user',
|
|
91534
|
-
content:
|
|
91661
|
+
content: finalContent
|
|
91535
91662
|
}
|
|
91536
91663
|
],
|
|
91537
91664
|
modalities: ['image', 'text'],
|
|
91538
91665
|
};
|
|
91666
|
+
logToTerminal('log', `📤 Request body: model=${requestBody.model}, content type=${Array.isArray(requestBody.messages[0].content) ? 'array' : 'string'}, content length=${Array.isArray(requestBody.messages[0].content) ? requestBody.messages[0].content.length : 'N/A'}, modalities=${requestBody.modalities.join(',')}`);
|
|
91667
|
+
if (Array.isArray(requestBody.messages[0].content)) {
|
|
91668
|
+
logToTerminal('log', `📤 Content array items: ${requestBody.messages[0].content.map((c) => c.type || 'unknown').join(', ')}`);
|
|
91669
|
+
}
|
|
91539
91670
|
const startTime = Date.now();
|
|
91540
91671
|
// Create AbortController for timeout (5 minutes for image generation)
|
|
91541
91672
|
// Keep timeout active for the entire process, including response reading
|
|
@@ -91546,6 +91677,7 @@ function App() {
|
|
|
91546
91677
|
}, timeoutMs);
|
|
91547
91678
|
let response;
|
|
91548
91679
|
try {
|
|
91680
|
+
logToTerminal('log', `📤 Sending request to OpenRouter API for image generation...`);
|
|
91549
91681
|
response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
91550
91682
|
method: 'POST',
|
|
91551
91683
|
headers: {
|
|
@@ -91557,9 +91689,11 @@ function App() {
|
|
|
91557
91689
|
body: JSON.stringify(requestBody),
|
|
91558
91690
|
signal: controller.signal
|
|
91559
91691
|
});
|
|
91692
|
+
logToTerminal('log', `📥 Response received: ${response.status} ${response.statusText}`);
|
|
91560
91693
|
}
|
|
91561
91694
|
catch (fetchError) {
|
|
91562
91695
|
clearTimeout(timeoutId);
|
|
91696
|
+
logToTerminal('error', `❌ Fetch error: ${fetchError.message || String(fetchError)}`);
|
|
91563
91697
|
if (fetchError.name === 'AbortError') {
|
|
91564
91698
|
throw new Error('Request timeout: Image generation took too long (exceeded 5 minutes)');
|
|
91565
91699
|
}
|
|
@@ -91594,17 +91728,44 @@ function App() {
|
|
|
91594
91728
|
throw readError;
|
|
91595
91729
|
}
|
|
91596
91730
|
if (!response.ok) {
|
|
91731
|
+
logToTerminal('error', `❌ HTTP error: ${response.status} ${response.statusText}`);
|
|
91732
|
+
logToTerminal('error', `❌ Response text length: ${responseText.length}`);
|
|
91733
|
+
logToTerminal('error', `❌ Response text preview: ${responseText.substring(0, 500)}`);
|
|
91597
91734
|
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
91598
91735
|
let errorDetails = `Status: ${response.status}\nStatusText: ${response.statusText}`;
|
|
91599
91736
|
try {
|
|
91600
91737
|
const error = JSON.parse(responseText);
|
|
91601
|
-
|
|
91738
|
+
logToTerminal('error', `❌ Parsed error JSON: ${JSON.stringify(error, null, 2)}`);
|
|
91739
|
+
errorMessage = error.error?.message || error.message || error.error?.code || errorMessage;
|
|
91602
91740
|
errorDetails += `\n\nError JSON: ${JSON.stringify(error, null, 2)}`;
|
|
91741
|
+
// If there's additional error info, include it
|
|
91742
|
+
if (error.error) {
|
|
91743
|
+
if (error.error.type)
|
|
91744
|
+
errorDetails += `\nError type: ${error.error.type}`;
|
|
91745
|
+
if (error.error.param)
|
|
91746
|
+
errorDetails += `\nError param: ${error.error.param}`;
|
|
91747
|
+
}
|
|
91603
91748
|
}
|
|
91604
91749
|
catch (e) {
|
|
91605
|
-
|
|
91750
|
+
logToTerminal('error', `❌ Failed to parse error JSON: ${e}`);
|
|
91751
|
+
errorDetails += `\n\nResponse text (first 1000 chars): ${responseText.substring(0, 1000)}`;
|
|
91606
91752
|
errorMessage = `${errorMessage}. Response: ${responseText.substring(0, 200)}`;
|
|
91607
91753
|
}
|
|
91754
|
+
// Log request body for debugging (without sensitive data)
|
|
91755
|
+
const requestBodyForLog = {
|
|
91756
|
+
...requestBody,
|
|
91757
|
+
messages: requestBody.messages.map((msg) => ({
|
|
91758
|
+
role: msg.role,
|
|
91759
|
+
content: Array.isArray(msg.content)
|
|
91760
|
+
? msg.content.map((c) => ({
|
|
91761
|
+
type: c.type,
|
|
91762
|
+
text: c.text ? c.text.substring(0, 100) + '...' : undefined,
|
|
91763
|
+
image_url: c.image_url ? '...' : undefined
|
|
91764
|
+
}))
|
|
91765
|
+
: typeof msg.content === 'string' ? msg.content.substring(0, 100) + '...' : msg.content
|
|
91766
|
+
}))
|
|
91767
|
+
};
|
|
91768
|
+
logToTerminal('error', `❌ Request body (sanitized): ${JSON.stringify(requestBodyForLog, null, 2)}`);
|
|
91608
91769
|
// Create error with full details
|
|
91609
91770
|
const fullError = new Error(errorMessage);
|
|
91610
91771
|
fullError.status = response.status;
|
|
@@ -91724,6 +91885,11 @@ function App() {
|
|
|
91724
91885
|
addLog(formatLogMessage(level, ...args));
|
|
91725
91886
|
logToTerminal(level, ...args);
|
|
91726
91887
|
};
|
|
91888
|
+
logMsg('log', `🔍 === Начало валидации креатива ===`);
|
|
91889
|
+
logMsg('log', `📦 Продукт: ${product}`);
|
|
91890
|
+
logMsg('log', `🌍 GEO: ${geo}`);
|
|
91891
|
+
logMsg('log', `🖼️ Image URL тип: ${imageUrl.startsWith('data:') ? 'data URL (base64)' : imageUrl.startsWith('http') ? 'HTTP URL' : 'другой'}`);
|
|
91892
|
+
logMsg('log', `🖼️ Image URL длина: ${imageUrl.length} символов`);
|
|
91727
91893
|
// Получаем ключевое слово для GEO (примеры из промпта)
|
|
91728
91894
|
const geoKeywords = {
|
|
91729
91895
|
'HU': ['prostata', 'paraziták', 'ízület', 'potencia', 'kilók', 'látás'],
|
|
@@ -91734,9 +91900,10 @@ function App() {
|
|
|
91734
91900
|
'SK': ['prostata', 'parazity', 'kĺby', 'potencia', 'kilo', 'zrak']
|
|
91735
91901
|
};
|
|
91736
91902
|
const keywords = geoKeywords[geo.toUpperCase()] || ['продукт', 'проблема'];
|
|
91903
|
+
logMsg('log', `🔑 Ключевые слова: ${keywords.join(', ')}`);
|
|
91737
91904
|
const validationPrompt = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getValidationPrompt)(product, geo, keywords);
|
|
91738
91905
|
const requestBody = {
|
|
91739
|
-
model:
|
|
91906
|
+
model: selectedValidationModel,
|
|
91740
91907
|
messages: [
|
|
91741
91908
|
{
|
|
91742
91909
|
role: 'user',
|
|
@@ -91749,15 +91916,21 @@ function App() {
|
|
|
91749
91916
|
max_tokens: 8000
|
|
91750
91917
|
};
|
|
91751
91918
|
logMsg('log', '🔍 Отправка запроса на проверку креатива...');
|
|
91919
|
+
logMsg('log', `📊 Модель валидации: ${selectedValidationModel}`);
|
|
91920
|
+
logMsg('log', `📏 Размер промпта: ${validationPrompt.length} символов`);
|
|
91752
91921
|
// Add timeout protection for validation request
|
|
91753
91922
|
const validationStartTime = Date.now();
|
|
91754
91923
|
const validationTimeoutMs = 2 * 60 * 1000; // 2 minutes timeout for validation
|
|
91755
91924
|
const validationController = new AbortController();
|
|
91756
91925
|
const validationTimeoutId = setTimeout(() => {
|
|
91926
|
+
const elapsed = Date.now() - validationStartTime;
|
|
91927
|
+
logMsg('error', `❌ Validation request timeout after ${Math.round(elapsed / 1000)}s`);
|
|
91757
91928
|
validationController.abort();
|
|
91758
91929
|
}, validationTimeoutMs);
|
|
91759
91930
|
let response;
|
|
91760
91931
|
try {
|
|
91932
|
+
const fetchStartTime = Date.now();
|
|
91933
|
+
logMsg('log', '📤 Отправка fetch запроса...');
|
|
91761
91934
|
response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
91762
91935
|
method: 'POST',
|
|
91763
91936
|
headers: {
|
|
@@ -91769,26 +91942,53 @@ function App() {
|
|
|
91769
91942
|
body: JSON.stringify(requestBody),
|
|
91770
91943
|
signal: validationController.signal
|
|
91771
91944
|
});
|
|
91945
|
+
const fetchElapsed = Date.now() - fetchStartTime;
|
|
91946
|
+
logMsg('log', `✅ Fetch завершен за ${Math.round(fetchElapsed / 1000)}s, статус: ${response.status} ${response.statusText}`);
|
|
91947
|
+
logMsg('log', `📋 Headers: Content-Type=${response.headers.get('content-type')}, Content-Length=${response.headers.get('content-length') || 'не указан'}`);
|
|
91772
91948
|
}
|
|
91773
91949
|
catch (fetchError) {
|
|
91774
91950
|
clearTimeout(validationTimeoutId);
|
|
91951
|
+
const elapsed = Date.now() - validationStartTime;
|
|
91775
91952
|
if (fetchError.name === 'AbortError') {
|
|
91776
|
-
logMsg('error',
|
|
91777
|
-
throw new Error(
|
|
91953
|
+
logMsg('error', `❌ Validation request timeout after ${Math.round(elapsed / 1000)}s (fetch stage)`);
|
|
91954
|
+
throw new Error(`Validation timeout: Request took too long (exceeded 2 minutes) - fetch stage`);
|
|
91778
91955
|
}
|
|
91956
|
+
logMsg('error', `❌ Fetch error: ${fetchError.message || String(fetchError)}`);
|
|
91779
91957
|
throw fetchError;
|
|
91780
91958
|
}
|
|
91959
|
+
// Check response status before reading
|
|
91960
|
+
if (!response.ok) {
|
|
91961
|
+
clearTimeout(validationTimeoutId);
|
|
91962
|
+
logMsg('error', `❌ HTTP error: ${response.status} ${response.statusText}`);
|
|
91963
|
+
// Try to read error response
|
|
91964
|
+
try {
|
|
91965
|
+
const errorText = await response.text();
|
|
91966
|
+
logMsg('error', `❌ Error response body: ${errorText.substring(0, 500)}`);
|
|
91967
|
+
}
|
|
91968
|
+
catch (e) {
|
|
91969
|
+
logMsg('error', '❌ Не удалось прочитать тело ошибки');
|
|
91970
|
+
}
|
|
91971
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
91972
|
+
}
|
|
91781
91973
|
// Read response with timeout protection
|
|
91782
91974
|
let responseText;
|
|
91783
91975
|
try {
|
|
91976
|
+
const readStartTime = Date.now();
|
|
91784
91977
|
const readTimeoutMs = 60000; // 1 minute max for reading response
|
|
91978
|
+
logMsg('log', `📥 Начало чтения ответа (таймаут: ${readTimeoutMs / 1000}s)...`);
|
|
91785
91979
|
responseText = await Promise.race([
|
|
91786
|
-
response.text()
|
|
91980
|
+
response.text().then(text => {
|
|
91981
|
+
const readElapsed = Date.now() - readStartTime;
|
|
91982
|
+
logMsg('log', `✅ Ответ прочитан за ${Math.round(readElapsed / 1000)}s, размер: ${text.length} символов`);
|
|
91983
|
+
return text;
|
|
91984
|
+
}),
|
|
91787
91985
|
new Promise((_, reject) => {
|
|
91788
91986
|
setTimeout(() => {
|
|
91987
|
+
const readElapsed = Date.now() - readStartTime;
|
|
91789
91988
|
clearTimeout(validationTimeoutId);
|
|
91790
91989
|
validationController.abort();
|
|
91791
|
-
|
|
91990
|
+
logMsg('error', `❌ Таймаут чтения ответа после ${Math.round(readElapsed / 1000)}s`);
|
|
91991
|
+
reject(new Error(`Validation response reading timeout after ${Math.round(readElapsed / 1000)}s`));
|
|
91792
91992
|
}, readTimeoutMs);
|
|
91793
91993
|
})
|
|
91794
91994
|
]);
|
|
@@ -91796,10 +91996,13 @@ function App() {
|
|
|
91796
91996
|
}
|
|
91797
91997
|
catch (readError) {
|
|
91798
91998
|
clearTimeout(validationTimeoutId);
|
|
91999
|
+
const totalElapsed = Date.now() - validationStartTime;
|
|
91799
92000
|
if (readError.name === 'AbortError' || readError.message?.includes('timeout')) {
|
|
91800
|
-
logMsg('error',
|
|
91801
|
-
|
|
92001
|
+
logMsg('error', `❌ Validation response reading timeout (общее время: ${Math.round(totalElapsed / 1000)}s)`);
|
|
92002
|
+
logMsg('error', `❌ Детали ошибки: ${readError.message || String(readError)}`);
|
|
92003
|
+
throw new Error(`Validation timeout: Failed to read response body after ${Math.round(totalElapsed / 1000)}s`);
|
|
91802
92004
|
}
|
|
92005
|
+
logMsg('error', `❌ Ошибка чтения ответа: ${readError.message || String(readError)}`);
|
|
91803
92006
|
throw readError;
|
|
91804
92007
|
}
|
|
91805
92008
|
if (!response.ok) {
|
|
@@ -91817,14 +92020,24 @@ function App() {
|
|
|
91817
92020
|
}
|
|
91818
92021
|
let data;
|
|
91819
92022
|
try {
|
|
92023
|
+
logMsg('log', '📝 Парсинг JSON ответа...');
|
|
91820
92024
|
data = JSON.parse(responseText);
|
|
92025
|
+
logMsg('log', `✅ JSON распарсен успешно. Структура: choices=${data.choices?.length || 0}, message=${data.choices?.[0]?.message ? 'есть' : 'нет'}`);
|
|
91821
92026
|
}
|
|
91822
92027
|
catch (e) {
|
|
91823
|
-
logMsg('error',
|
|
92028
|
+
logMsg('error', `❌ Не удалось распарсить ответ как JSON. Ошибка: ${e}`);
|
|
92029
|
+
logMsg('error', `❌ Первые 500 символов ответа: ${responseText.substring(0, 500)}`);
|
|
91824
92030
|
throw new Error(`Invalid JSON response: ${responseText.substring(0, 200)}`);
|
|
91825
92031
|
}
|
|
91826
92032
|
const content = data.choices?.[0]?.message?.content || '';
|
|
91827
|
-
logMsg('log',
|
|
92033
|
+
logMsg('log', `✅ Получен ответ от модели проверки. Длина контента: ${content.length} символов`);
|
|
92034
|
+
if (content.length > 0) {
|
|
92035
|
+
logMsg('log', `📄 Первые 300 символов ответа: ${content.substring(0, 300)}`);
|
|
92036
|
+
}
|
|
92037
|
+
else {
|
|
92038
|
+
logMsg('error', '⚠️ Контент ответа пустой!');
|
|
92039
|
+
logMsg('error', `📋 Полный объект data: ${JSON.stringify(data, null, 2).substring(0, 1000)}`);
|
|
92040
|
+
}
|
|
91828
92041
|
// Update balance after successful API call
|
|
91829
92042
|
fetchOpenRouterBalance();
|
|
91830
92043
|
// Парсим результат
|
|
@@ -91855,10 +92068,21 @@ function App() {
|
|
|
91855
92068
|
errors.push(...lines.map(l => l.trim()).filter(l => l.length > 0));
|
|
91856
92069
|
}
|
|
91857
92070
|
}
|
|
92071
|
+
const totalElapsed = Date.now() - validationStartTime;
|
|
92072
|
+
const finalErrors = errors.length > 0 ? errors : (status === 'needs_rebuild' ? ['Обнаружены нарушения в креативе'] : []);
|
|
92073
|
+
logMsg('log', `✅ === Валидация завершена ===`);
|
|
92074
|
+
logMsg('log', `⏱️ Общее время: ${Math.round(totalElapsed / 1000)}s`);
|
|
92075
|
+
logMsg('log', `📊 Статус: ${status === 'ok' ? '✅ OK' : '❌ НУЖНА ПЕРЕСБОРКА'}`);
|
|
92076
|
+
logMsg('log', `🔍 Найдено ошибок: ${finalErrors.length}`);
|
|
92077
|
+
if (finalErrors.length > 0) {
|
|
92078
|
+
finalErrors.forEach((error, idx) => {
|
|
92079
|
+
logMsg('error', ` ${idx + 1}. ${error}`);
|
|
92080
|
+
});
|
|
92081
|
+
}
|
|
91858
92082
|
return {
|
|
91859
92083
|
status,
|
|
91860
92084
|
result: content,
|
|
91861
|
-
errors:
|
|
92085
|
+
errors: finalErrors
|
|
91862
92086
|
};
|
|
91863
92087
|
};
|
|
91864
92088
|
const generateProductFromBanka = async (bankaImageUrls, additionalPrompt = '', addLog, currentProductImageUrl) => {
|
|
@@ -92801,7 +93025,7 @@ function App() {
|
|
|
92801
93025
|
setGeneratedImagesData(initialPlaceholders);
|
|
92802
93026
|
const imagePrompts = approaches.map(approach => `${basePromptStructure}🎯 ПОДХОД: ${approach.name}\n\n${approach.prompt}\n\n${approach.headlineAngle}\n\n${approach.bulletsFocus}\n\nТекст (заголовок/буллеты) — строго следуй указанным углам и акцентам для этого подхода.`);
|
|
92803
93027
|
addLog(formatLogMessage('log', '📝 Generated prompts for 5 different approaches'));
|
|
92804
|
-
const maxParallel =
|
|
93028
|
+
const maxParallel = 5;
|
|
92805
93029
|
addLog(formatLogMessage('log', `🚀 Generating 5 images in parallel (up to ${maxParallel} at a time)...`));
|
|
92806
93030
|
const generationStartTime = Date.now();
|
|
92807
93031
|
const resultsMap = new Map();
|
|
@@ -93035,72 +93259,111 @@ function App() {
|
|
|
93035
93259
|
// Get current customRegeneratePrompt from state
|
|
93036
93260
|
const currentImageData = generatedImagesData.find(img => img.index === imageData.index);
|
|
93037
93261
|
const customPrompt = currentImageData?.customRegeneratePrompt?.trim() || '';
|
|
93038
|
-
//
|
|
93262
|
+
// Use current imageUrl from state, not from props (to get the latest version)
|
|
93263
|
+
const currentImageUrl = currentImageData?.imageUrl || imageData.imageUrl;
|
|
93264
|
+
// Update status to regenerating - clear old image URL immediately to force re-render
|
|
93039
93265
|
setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
|
|
93040
|
-
? {
|
|
93266
|
+
? {
|
|
93267
|
+
...img,
|
|
93268
|
+
regenerating: true,
|
|
93269
|
+
regenerateStartTime: Date.now(),
|
|
93270
|
+
checkStatus: 'pending',
|
|
93271
|
+
imageUrl: undefined // Clear old image URL to force re-render with new one
|
|
93272
|
+
}
|
|
93041
93273
|
: img));
|
|
93042
93274
|
addLog(formatLogMessage('log', `🔄 Переделка изображения ${imageData.index} (${imageData.approach})...`));
|
|
93043
93275
|
// Build improved prompt with custom instructions
|
|
93044
93276
|
let improvedPrompt = imageData.originalPrompt;
|
|
93045
93277
|
if (customPrompt) {
|
|
93046
|
-
if (
|
|
93047
|
-
// For existing images, use reference
|
|
93048
|
-
improvedPrompt =
|
|
93278
|
+
if (currentImageUrl) {
|
|
93279
|
+
// For existing images, use reference - only the last generated image is attached
|
|
93280
|
+
improvedPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
|
|
93049
93281
|
|
|
93050
|
-
|
|
93051
|
-
|
|
93052
|
-
|
|
93282
|
+
${imageData.originalPrompt}
|
|
93283
|
+
|
|
93284
|
+
⚠️ КРИТИЧНО: Предыдущий вариант требует изменений. Прикреплен текущий креатив (требует изменений согласно требованиям ниже).
|
|
93285
|
+
В креативе уже есть упаковка продукта - используй её как основу. Если в требованиях ниже не указано иное - сохрани форму и цвет упаковки.
|
|
93053
93286
|
|
|
93054
93287
|
ТРЕБОВАНИЯ К ИЗМЕНЕНИЯМ:
|
|
93055
93288
|
${customPrompt.split('\n').map(line => `- ${line}`).join('\n')}
|
|
93056
93289
|
|
|
93057
93290
|
ТРЕБОВАНИЯ К ИСПРАВЛЕНИЮ:
|
|
93058
|
-
- Учти ВСЕ указанные требования в новом варианте
|
|
93291
|
+
- Учти ВСЕ указанные требования в новом варианте (включая изменения упаковки, если они указаны)
|
|
93059
93292
|
- Сохрани тот же подход и стиль (${imageData.approach})
|
|
93060
|
-
-
|
|
93061
|
-
- Убедись, что новый вариант соответствует всем правилам из основного
|
|
93293
|
+
- Если в требованиях не указано изменение упаковки - сохрани упаковку продукта из прикрепленного креатива
|
|
93294
|
+
- Убедись, что новый вариант соответствует всем правилам из основного промпта
|
|
93295
|
+
|
|
93296
|
+
ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
|
|
93062
93297
|
}
|
|
93063
93298
|
else {
|
|
93064
|
-
// For failed generations, add custom requirements if provided
|
|
93065
|
-
improvedPrompt =
|
|
93299
|
+
// For failed generations, add custom requirements if provided - only product image is attached
|
|
93300
|
+
improvedPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
|
|
93066
93301
|
|
|
93067
|
-
|
|
93302
|
+
${imageData.originalPrompt}
|
|
93068
93303
|
|
|
93069
|
-
|
|
93304
|
+
⚠️ Повторная генерация после неудачной попытки. Прикреплено изображение продукта - используй его упаковку как основу. Если в требованиях ниже не указано иное - сохрани форму и цвет упаковки.
|
|
93305
|
+
|
|
93306
|
+
${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPrompt.split('\n').map(line => `- ${line}`).join('\n')}\n\n` : ''}Убедись, что новый вариант соответствует всем правилам из основного промпта.
|
|
93307
|
+
|
|
93308
|
+
ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
|
|
93070
93309
|
}
|
|
93071
93310
|
}
|
|
93072
93311
|
else {
|
|
93073
93312
|
// If no custom prompt
|
|
93074
|
-
if (
|
|
93075
|
-
// For existing images, just add general improvement instruction
|
|
93076
|
-
improvedPrompt =
|
|
93313
|
+
if (currentImageUrl) {
|
|
93314
|
+
// For existing images, just add general improvement instruction - only the last generated image is attached
|
|
93315
|
+
improvedPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
|
|
93316
|
+
|
|
93317
|
+
${imageData.originalPrompt}
|
|
93077
93318
|
|
|
93078
93319
|
⚠️ Переделка: улучши текущий вариант, сохранив подход и стиль (${imageData.approach}).
|
|
93079
|
-
|
|
93080
|
-
|
|
93081
|
-
|
|
93320
|
+
Прикреплен текущий креатив - улучши его, сохранив упаковку продукта из креатива (не меняй форму/цвет упаковки).
|
|
93321
|
+
|
|
93322
|
+
ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
|
|
93082
93323
|
}
|
|
93083
93324
|
else {
|
|
93084
|
-
// For failed generations, use original prompt as-is
|
|
93085
|
-
improvedPrompt =
|
|
93325
|
+
// For failed generations, use original prompt as-is - only product image is attached
|
|
93326
|
+
improvedPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
|
|
93327
|
+
|
|
93328
|
+
${imageData.originalPrompt}
|
|
93329
|
+
|
|
93330
|
+
ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
|
|
93086
93331
|
}
|
|
93087
93332
|
}
|
|
93088
|
-
// Generate new image: use
|
|
93089
|
-
//
|
|
93333
|
+
// Generate new image: use only the last generated image if it exists, otherwise use product image
|
|
93334
|
+
// If there's a last generated image, attach only that (model will use product from it)
|
|
93335
|
+
// If no last image exists, attach only the product image
|
|
93090
93336
|
const referenceImages = [];
|
|
93091
|
-
if (
|
|
93092
|
-
// If regenerating existing image, use
|
|
93093
|
-
referenceImages.push(
|
|
93337
|
+
if (currentImageUrl) {
|
|
93338
|
+
// If regenerating existing image, use ONLY the last generated image as reference
|
|
93339
|
+
referenceImages.push(currentImageUrl);
|
|
93340
|
+
addLog(formatLogMessage('log', `🖼️ Используется последнее сгенерированное изображение как референс`));
|
|
93341
|
+
}
|
|
93342
|
+
else {
|
|
93343
|
+
// If no last image exists, use only product image
|
|
93344
|
+
referenceImages.push(imageData.productImageUrl);
|
|
93345
|
+
addLog(formatLogMessage('log', `🖼️ Используется изображение продукта как референс`));
|
|
93346
|
+
}
|
|
93347
|
+
// Log regeneration details
|
|
93348
|
+
addLog(formatLogMessage('log', `📝 Промпт для перегенерации (длина: ${improvedPrompt.length} символов)`));
|
|
93349
|
+
addLog(formatLogMessage('log', `🖼️ Референсных изображений: ${referenceImages.length}`));
|
|
93350
|
+
if (referenceImages.length > 0) {
|
|
93351
|
+
referenceImages.forEach((url, idx) => {
|
|
93352
|
+
const urlPreview = url.length > 100 ? url.substring(0, 100) + '...' : url;
|
|
93353
|
+
addLog(formatLogMessage('log', ` ${idx + 1}. ${urlPreview}`));
|
|
93354
|
+
});
|
|
93094
93355
|
}
|
|
93095
|
-
|
|
93096
|
-
referenceImages.push(imageData.productImageUrl);
|
|
93356
|
+
addLog(formatLogMessage('log', `📤 Первые 200 символов промпта: ${improvedPrompt.substring(0, 200)}...`));
|
|
93097
93357
|
const newImageUrl = await generateImageWithDALLE(improvedPrompt, referenceImages);
|
|
93098
93358
|
addLog(formatLogMessage('log', `✅ Изображение ${imageData.index} переделано успешно`));
|
|
93099
93359
|
// Update image data - force React to re-render by creating a completely new object
|
|
93100
|
-
//
|
|
93360
|
+
// Always add timestamp for HTTP URLs to prevent browser caching (remove old timestamp if exists)
|
|
93101
93361
|
let updatedImageUrl = newImageUrl;
|
|
93102
|
-
if (!newImageUrl.startsWith('data:')
|
|
93103
|
-
|
|
93362
|
+
if (!newImageUrl.startsWith('data:')) {
|
|
93363
|
+
// Remove existing timestamp parameter if present
|
|
93364
|
+
const urlWithoutTimestamp = newImageUrl.replace(/[?&]t=\d+/g, '');
|
|
93365
|
+
const separator = urlWithoutTimestamp.includes('?') ? '&' : '?';
|
|
93366
|
+
updatedImageUrl = `${urlWithoutTimestamp}${separator}t=${Date.now()}`;
|
|
93104
93367
|
}
|
|
93105
93368
|
setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
|
|
93106
93369
|
? {
|
|
@@ -93171,7 +93434,7 @@ ${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPr
|
|
|
93171
93434
|
logToTerminal('log', msg.replace(/\[.*?\]\s*/, ''));
|
|
93172
93435
|
};
|
|
93173
93436
|
try {
|
|
93174
|
-
// Mark as regenerating (show overlay timer)
|
|
93437
|
+
// Mark as regenerating (show overlay timer) - clear old image URL immediately to force re-render
|
|
93175
93438
|
setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
|
|
93176
93439
|
? {
|
|
93177
93440
|
...img,
|
|
@@ -93184,17 +93447,26 @@ ${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPr
|
|
|
93184
93447
|
checkResult: undefined,
|
|
93185
93448
|
checkErrors: undefined,
|
|
93186
93449
|
// Explicitly ignore any previous comments
|
|
93187
|
-
customRegeneratePrompt: ''
|
|
93450
|
+
customRegeneratePrompt: '',
|
|
93451
|
+
imageUrl: undefined // Clear old image URL to force re-render with new one
|
|
93188
93452
|
}
|
|
93189
93453
|
: img));
|
|
93190
93454
|
addLog(formatLogMessage('log', `🔁 Переделка заново (с нуля) изображения ${imageData.index} (${imageData.approach})...`));
|
|
93191
|
-
// Generate using only product image as reference, and original prompt
|
|
93192
|
-
const
|
|
93455
|
+
// Generate using only product image as reference, and original prompt with image generation instructions
|
|
93456
|
+
const freshPrompt = `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
|
|
93457
|
+
|
|
93458
|
+
${imageData.originalPrompt}
|
|
93459
|
+
|
|
93460
|
+
ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
|
|
93461
|
+
const newImageUrl = await generateImageWithDALLE(freshPrompt, imageData.productImageUrl);
|
|
93193
93462
|
addLog(formatLogMessage('log', `✅ Изображение ${imageData.index} (с нуля) сгенерировано`));
|
|
93194
|
-
// Update image data
|
|
93463
|
+
// Update image data - always add timestamp for HTTP URLs to prevent browser caching
|
|
93195
93464
|
let updatedImageUrl = newImageUrl;
|
|
93196
|
-
if (!newImageUrl.startsWith('data:')
|
|
93197
|
-
|
|
93465
|
+
if (!newImageUrl.startsWith('data:')) {
|
|
93466
|
+
// Remove existing timestamp parameter if present
|
|
93467
|
+
const urlWithoutTimestamp = newImageUrl.replace(/[?&]t=\d+/g, '');
|
|
93468
|
+
const separator = urlWithoutTimestamp.includes('?') ? '&' : '?';
|
|
93469
|
+
updatedImageUrl = `${urlWithoutTimestamp}${separator}t=${Date.now()}`;
|
|
93198
93470
|
}
|
|
93199
93471
|
setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index
|
|
93200
93472
|
? {
|
|
@@ -93787,6 +94059,16 @@ ${customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPr
|
|
|
93787
94059
|
!loadingImageModels && imageModels.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_23__["default"], null, selectedImageModel === _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.imageGeneration
|
|
93788
94060
|
? 'Используется модель по умолчанию'
|
|
93789
94061
|
: 'Выбрана модель: ' + (imageModels.find(m => m.id === selectedImageModel)?.name || selectedImageModel)))))),
|
|
94062
|
+
openaiApiKey && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { mb: 2 } },
|
|
94063
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], { fullWidth: true, variant: "outlined" },
|
|
94064
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_20__["default"], null, "\u041C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043A\u0440\u0435\u0430\u0442\u0438\u0432\u043E\u0432"),
|
|
94065
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_21__["default"], { value: selectedValidationModel, onChange: (e) => handleValidationModelChange(e.target.value), label: "\u041C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043A\u0440\u0435\u0430\u0442\u0438\u0432\u043E\u0432", disabled: loadingValidationModels || validationModels.length === 0 }, loadingValidationModels ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { disabled: true },
|
|
94066
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
|
|
94067
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }),
|
|
94068
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043C\u043E\u0434\u0435\u043B\u0435\u0439...")))) : validationModels.length === 0 ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { disabled: true }, "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043C\u043E\u0434\u0435\u043B\u0435\u0439")) : (validationModels.map((model) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { key: model.id, value: model.id }, model.name))))),
|
|
94069
|
+
!loadingValidationModels && validationModels.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_23__["default"], null, selectedValidationModel === _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation
|
|
94070
|
+
? 'Используется модель по умолчанию'
|
|
94071
|
+
: 'Выбрана модель: ' + (validationModels.find(m => m.id === selectedValidationModel)?.name || selectedValidationModel)))))),
|
|
93790
94072
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
|
|
93791
94073
|
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 || !openaiApiKey || !generateProduct.trim() || !generateGeo.trim(), sx: { flexGrow: 1 }, size: "large" }, generating ? 'Generating...' : 'Generate Titles & Descriptions'),
|
|
93792
94074
|
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 ||
|
|
@@ -94499,7 +94781,7 @@ function getTitlesSystemPrompt(geo) {
|
|
|
94499
94781
|
- Используй знания лучших практик рекламного копирайтинга
|
|
94500
94782
|
- Избегай только жёстких медицинских гарантий («лечит», «вылечивает», «гарантируем»). Мягкие обещания результата разрешены («Nopți fără treziri», «Stomac liniștit», «Efect rapid»). Разрешены: сроки («în 14 zile»), цифры соц. доказательства («9 din 10 bărbați»), мягкие результаты. Запрещены: «100% гарантия», «навсегда», «излечение».
|
|
94501
94783
|
- Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
|
|
94502
|
-
-
|
|
94784
|
+
- Если в категории продукта есть специфические медицинские термины или диагнозы — используй их осторожно, не в каждом заголовке. Максимум 1 заголовок из 3 может содержать прямое упоминание таких терминов. Предпочитай мягкие формулировки, описывающие симптомы и дискомфорт, а не диагнозы. Если все 3 заголовка содержат прямые медицинские термины — перепиши 2 из них с мягкими альтернативами, фокусируясь на ощущениях и симптомах
|
|
94503
94785
|
- После создания проверь их правильность и разнообразие
|
|
94504
94786
|
- Верни только заголовки, по одному на строку, без нумерации или буллетов
|
|
94505
94787
|
- Заголовки должны быть на языке ${geo}
|
|
@@ -94560,8 +94842,8 @@ function getTextsSystemPrompt(geo) {
|
|
|
94560
94842
|
- Разрешены формулировки: «reduce frecvența», «elimină disconfortul», «rezultat vizibil în X zile», «9 din 10 observă diferența». Запрещены только: «лечит/vindecă», «гарантируем 100%», «навсегда/definitiv».
|
|
94561
94843
|
- Ты можешь усиливать эффект и результаты, но без прямых медицинских обещаний
|
|
94562
94844
|
- Избегай только: прямых медицинских диагнозов, слов «лечит/вылечивает/гарантируем», обещаний «100%/навсегда». Всё остальное — разрешено.
|
|
94563
|
-
-
|
|
94564
|
-
- Избегай
|
|
94845
|
+
- Если в категории продукта есть специфические медицинские термины или диагнозы — используй их осторожно, не в каждом тексте. Максимум 1 текст из 3 может содержать прямое упоминание таких терминов. Остальные тексты фокусируй на симптомах, дискомфорте и ощущениях пользователя, релевантных категории продукта. Избегай эвфемизмов и искусственных замен — используй естественные формулировки
|
|
94846
|
+
- Избегай прямых упоминаний интимных или деликатных деталей, связанных с проблемой. Используй мягкие формулировки, описывающие дискомфорт и симптомы, а не физиологические процессы
|
|
94565
94847
|
- Избегай повторения полного названия продукта более 2 раз в одном тексте. Используй местоимения или более короткие ссылки при необходимости
|
|
94566
94848
|
|
|
94567
94849
|
Требования к формату:
|
|
@@ -94644,7 +94926,7 @@ OTHER_TEXT: ["..."] (любой другой рекламный текст на
|
|
|
94644
94926
|
- ЗАПРЕЩЕНЫ только: «лечит», «вылечивает», «гарантируем», «100%», «навсегда», «полностью избавляет», диагностика заболеваний
|
|
94645
94927
|
- «Выводит/устраняет токсины» для детокс-продуктов — это мягкий клейм, НЕ медицинская гарантия
|
|
94646
94928
|
- Если найден запрещённый клейм — ошибка. Мягкие клеймы — не ошибка.
|
|
94647
|
-
-
|
|
94929
|
+
- Специфические медицинские термины или диагнозы — разрешены, но не должны повторяться чрезмерно. Если в HEADLINE + BULLETS один и тот же специфический термин встречается более 1 раза — РЕКОМЕНДАЦИЯ: разнообразить формулировки, использовать синонимы или фокус на симптомах вместо повторения термина
|
|
94648
94930
|
|
|
94649
94931
|
ШАГ 3 — TEXT LIMIT (критично):
|
|
94650
94932
|
- Общее число слов в HEADLINE + BULLETS <= 16. Если больше — ошибка.
|
|
@@ -94675,8 +94957,18 @@ OTHER_TEXT: ["..."] (любой другой рекламный текст на
|
|
|
94675
94957
|
* Базовый промпт для генерации изображений креативов
|
|
94676
94958
|
*/
|
|
94677
94959
|
function getImageGenerationBasePrompt(generateGeo, generatePrice, generateCurrency) {
|
|
94678
|
-
return
|
|
94960
|
+
return `ЭТО ЗАДАЧА НА ГЕНЕРАЦИЮ ИЗОБРАЖЕНИЯ. Создай готовый визуальный креатив (картинку), а не текстовое описание или план. Не рассуждай, не объясняй, не выводи текстовые блоки — сразу генерируй изображение.
|
|
94961
|
+
|
|
94962
|
+
Создай рекламный креатив для Facebook Ads 1:1 (квадрат).
|
|
94679
94963
|
Язык текста: ${generateGeo}.
|
|
94964
|
+
|
|
94965
|
+
КРИТИЧНО — РЕЛЕВАНТНОСТЬ ПРОДУКТУ:
|
|
94966
|
+
- Заголовок и буллиты ДОЛЖНЫ соответствовать категории и проблеме продукта из его названия и описания
|
|
94967
|
+
- НЕ копируй примеры из промпта буквально — примеры показывают только ФОРМАТ, не содержание
|
|
94968
|
+
- Определи категорию продукта (диабет, суставы, пищеварение, простата, зрение, похудение и т.д.) и используй ТОЛЬКО релевантные этой категории проблемы и выгоды
|
|
94969
|
+
- Если примеры в промпте про другую категорию — игнорируй их содержание, используй только структуру
|
|
94970
|
+
- Прочитай название и описание продукта внимательно и построй заголовок/буллиты вокруг ЕГО проблематики
|
|
94971
|
+
|
|
94680
94972
|
Используй предоставленный рендер упаковки продукта как есть (форма/цвет без изменений). На упаковке запрещены текст/цена/буллеты/стикеры.
|
|
94681
94973
|
|
|
94682
94974
|
CRITICAL RULES (это ВАЛИДАЦИЯ, не рекомендации; если нарушено — УДАЛИ ЛИШНЕЕ и ПЕРЕСОБЕРИ МАКЕТ):
|
|
@@ -94699,7 +94991,7 @@ HEADLINE (строгое правило):
|
|
|
94699
94991
|
- если не влезает: сначала измени композицию (расширь блок/переставь элементы), затем перепиши короче. НЕЛЬЗЯ уменьшать шрифт.
|
|
94700
94992
|
- Каждый из 5 креативов должен иметь УНИКАЛЬНЫЙ заголовок. Не повторяй один и тот же заголовок на разных визуалах. Варьируй: угол боли, формулировку, акцент (проблема vs облегчение)
|
|
94701
94993
|
- Вторая часть заголовка = конкретное улучшение, не абстрактное слово. ПРАВИЛЬНО: «Stomac liniștit», «Zile fără disconfort», «Confort după masă». НЕ ТАК: «Mai ușor», «Mai bine», «Soluția»
|
|
94702
|
-
-
|
|
94994
|
+
- Специфические медицинские термины или диагнозы разрешены в заголовке, но не обязательны. Варьируй между прямыми формулировками (если релевантно категории) и мягкими формулировками, фокусирующимися на симптомах и дискомфорте, для разных креативов. Не используй один и тот же термин во всех креативах
|
|
94703
94995
|
ПРИМЕР (HU, только как формат; для других GEO адаптируй язык, не копируй слова):
|
|
94704
94996
|
- НЕ ТАК: «Makacs méreganyag ellen» (лозунг)
|
|
94705
94997
|
- ТАК: «Puffadás gond? Könnyebb napok»
|
|
@@ -94743,38 +95035,40 @@ AUTO-CHECK (перед финалом):
|
|
|
94743
95035
|
- Нет лишнего текста кроме: заголовка, буллетов, цены, скидки, кнопки
|
|
94744
95036
|
- Есть скидка «-50%» (вне упаковки) и она слабее CTA
|
|
94745
95037
|
- CTA визуально сильнее цены/скидки
|
|
94746
|
-
Если хоть один пункт нарушен — пересоздай
|
|
95038
|
+
Если хоть один пункт нарушен — пересоздай креатив.
|
|
95039
|
+
|
|
95040
|
+
ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай. Сразу генерируй визуальный креатив как картинку.`;
|
|
94747
95041
|
}
|
|
94748
95042
|
const CREO_APPROACHES = [
|
|
94749
95043
|
{
|
|
94750
95044
|
name: 'Врач / Эксперт (мужчина)',
|
|
94751
95045
|
prompt: `ВРАЧ-ЭКСПЕРТ (мужчина): спокойный профессионал, кабинет/клиника; врач слева/по центру, продукт справа, чистый нейтральный фон, фронтальный ракурс. Врач виден минимум по пояс, в белом халате со стетоскопом.`,
|
|
94752
|
-
headlineAngle:
|
|
94753
|
-
bulletsFocus:
|
|
95046
|
+
headlineAngle: `ЗАГОЛОВОК: угол «ежедневная проблема + спокойствие». Возьми ключевую проблему из описания продукта + мягкое обещание облегчения. Примеры в промпте — только для формата, НЕ копируй их содержание.`,
|
|
95047
|
+
bulletsFocus: `БУЛЛИТЫ: акцент на натуральности и доверии — натуральная формула, растительные экстракты, рекомендовано специалистами, без химии. Адаптируй под язык GEO и категорию продукта. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте — только ориентир для смысла, переводи их на нужный язык.`
|
|
94754
95048
|
},
|
|
94755
95049
|
{
|
|
94756
95050
|
name: 'Врач / Эксперт (женщина)',
|
|
94757
95051
|
prompt: `ВРАЧ-ЭКСПЕРТ (женщина): тёплый свет, уютный кабинет; врач справа/в углу, продукт слева/по центру. Врач в фокусе, чётко виден, лицо читаемое, профессиональный вид.`,
|
|
94758
|
-
headlineAngle:
|
|
94759
|
-
bulletsFocus:
|
|
95052
|
+
headlineAngle: `ЗАГОЛОВОК: угол «состояние/момент + облегчение». Возьми типичный момент проявления проблемы из описания продукта + конкретный результат. НЕ копируй примеры из других категорий.`,
|
|
95053
|
+
bulletsFocus: `БУЛЛИТЫ: акцент на результате — эффект, срок действия, улучшение состояния. Адаптируй под продукт и язык GEO. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте — только ориентир для смысла, переводи их на нужный язык.`
|
|
94760
95054
|
},
|
|
94761
95055
|
{
|
|
94762
95056
|
name: 'Lifestyle / Clean',
|
|
94763
95057
|
prompt: `LIFESTYLE/CLEAN: продукт в контексте момента приёма — кухня, обеденный стол, рядом с едой/чаем. Без человека, но с "историей" использования. Тёплые цвета, естественный свет.`,
|
|
94764
|
-
headlineAngle:
|
|
94765
|
-
bulletsFocus:
|
|
95058
|
+
headlineAngle: `ЗАГОЛОВОК: угол «социальное доказательство». Цифры клиентов/пользователей + основная выгода продукта. Формат: «X из Y выбирают...», «Более X клиентов...». НЕ копируй примеры из других категорий.`,
|
|
95059
|
+
bulletsFocus: `БУЛЛИТЫ: акцент на простоте использования — лёгкость приёма, удобство, доступность без рецепта. Адаптируй под продукт и язык GEO. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте — только ориентир для смысла, переводи их на нужный язык.`
|
|
94766
95060
|
},
|
|
94767
95061
|
{
|
|
94768
95062
|
name: 'WOW-эффект',
|
|
94769
95063
|
prompt: `WOW-ЭФФЕКТ: визуальная метафора действия — свежесть, очищение, лёгкость. Капли воды, зелёные листья в движении, эффект «чистоты изнутри». Премиально, минималистично. Фокус на эффекте, не на человеке.`,
|
|
94770
|
-
headlineAngle:
|
|
94771
|
-
bulletsFocus:
|
|
95064
|
+
headlineAngle: `ЗАГОЛОВОК: угол «ключевое действие продукта». Что продукт делает + мягкий результат. Визуальная метафора в заголовке. НЕ копируй примеры из других категорий.`,
|
|
95065
|
+
bulletsFocus: `БУЛЛИТЫ: акцент на механизме действия — что продукт делает, как работает, какой эффект даёт. Адаптируй под продукт и язык GEO. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте — только ориентир для смысла, переводи их на нужный язык.`
|
|
94772
95066
|
},
|
|
94773
95067
|
{
|
|
94774
95068
|
name: 'Эмоция результата',
|
|
94775
95069
|
prompt: `ЭМОЦИЯ РЕЗУЛЬТАТА: реальный человек с естественным облегчением (тихий выдох, расслабленная поза), без стоковой улыбки. Фокус на человеке и его состоянии после приёма.`,
|
|
94776
|
-
headlineAngle:
|
|
94777
|
-
bulletsFocus:
|
|
95070
|
+
headlineAngle: `ЗАГОЛОВОК: угол «срочность + результат». Ограниченное предложение/время + основная выгода продукта. Формат: «Только сегодня — ...», «Последние дни — ...». НЕ копируй примеры из других категорий.`,
|
|
95071
|
+
bulletsFocus: `БУЛЛИТЫ: акцент на ощущениях пользователя — как человек себя чувствует после приёма, улучшение качества жизни. Адаптируй под продукт и язык GEO. Все тексты генерируй строго на языке, указанном в параметре GEO. Примеры слов в промпте — только ориентир для смысла, переводи их на нужный язык.`
|
|
94778
95072
|
}
|
|
94779
95073
|
];
|
|
94780
95074
|
|