docs-combiner 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/renderer.js CHANGED
@@ -100888,14 +100888,14 @@ __webpack_require__.r(__webpack_exports__);
100888
100888
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/CircularProgress/CircularProgress.js");
100889
100889
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/FormHelperText/FormHelperText.js");
100890
100890
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Alert/Alert.js");
100891
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/InputAdornment/InputAdornment.js");
100892
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Tooltip/Tooltip.js");
100893
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/FormControl/FormControl.js");
100894
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/InputLabel/InputLabel.js");
100895
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Select/Select.js");
100896
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/MenuItem/MenuItem.js");
100897
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/FormControlLabel/FormControlLabel.js");
100898
- /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Checkbox/Checkbox.js");
100891
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/FormControlLabel/FormControlLabel.js");
100892
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Checkbox/Checkbox.js");
100893
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/InputAdornment/InputAdornment.js");
100894
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Tooltip/Tooltip.js");
100895
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/FormControl/FormControl.js");
100896
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/InputLabel/InputLabel.js");
100897
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Select/Select.js");
100898
+ /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/MenuItem/MenuItem.js");
100899
100899
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButtonGroup/ToggleButtonGroup.js");
100900
100900
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/ToggleButton/ToggleButton.js");
100901
100901
  /* harmony import */ var _mui_material__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(/*! @mui/material */ "./node_modules/@mui/material/esm/Paper/Paper.js");
@@ -100983,15 +100983,219 @@ const INSTRUCTION_ROW = [
100983
100983
  "# Обязательно | The URL for the main image of your item. Images must be in a supported format (JPG/GIF/PNG) and at least 500 x 500 pixels.",
100984
100984
  "# Обязательно | Фирменное название товара. Не более 100 символов."
100985
100985
  ];
100986
- /** Хвост ссылки для каталога: sub/utm и макросы Meta (не URL-encode — подстановка на стороне FB). */
100987
- const CATALOG_LINK_TRACKING_SUFFIX = 'sub1={{ad.id}}&sub2={{adset.id}}&sub3={{campaign.id}}&sub4={{ad.name}}&sub5={{adset.name}}&sub6={{campaign.name}}&sub7={{placement}}&sub8={{site_source_name}}&utm_source=facebook&utm_medium=paid';
100988
- /** Добавляет к URL лендинга creative_id и трекинговые параметры для строки каталога. */
100989
- function appendCreativeIdToCatalogLink(baseUrl, creativeId) {
100986
+ /** Доп. query-параметры для ссылки каталога (RedTrack / utm / макросы Meta; не URL-encode — подстановка на стороне FB). */
100987
+ const DEFAULT_CATALOG_LINK_EXTRA_MACROS = 'sub1={{ad.id}}&sub2={{adset.id}}&sub3={{campaign.id}}&sub4={{ad.name}}&sub5={{adset.name}}&sub6={{campaign.name}}&sub7={{placement}}&sub8={{site_source_name}}&utm_source=facebook&utm_medium=paid';
100988
+ const DEFAULT_CATALOG_LINK_KEITARO_MACROS = 'utm_campaign={{campaign.name}}&utm_source={{site_source_name}}&utm_placement={{placement}}&campaign_id={{campaign.id}}&adset_id={{adset.id}}&ad_id={{ad.id}}&adset_name={{adset.name}}&ad_name={{ad.name}}';
100989
+ /** 8 случайных hex-символов для уникального суффикса имени файла на Drive. */
100990
+ function randomHex8() {
100991
+ const a = new Uint8Array(4);
100992
+ crypto.getRandomValues(a);
100993
+ return Array.from(a, b => b.toString(16).padStart(2, '0')).join('');
100994
+ }
100995
+ /** Имя файла при загрузке крео: N_xxxxxxxx.png — N = номер строки подхода в UI (1–10). */
100996
+ function creativeImageUploadFilename(creoApproachUiNumber) {
100997
+ return `${creoApproachUiNumber}_${randomHex8()}.png`;
100998
+ }
100999
+ /**
101000
+ * Из имени файла картинки на Drive — номер строки подхода к крео (1–10) для каталога/трекера.
101001
+ * Формат: N_xxxxxxxx; снимаются расширение, суффиксы « (N)» от дубликатов Google.
101002
+ * Старый формат (текст_hex8) — возвращается «лейбл» после снятия hex (не только цифры).
101003
+ */
101004
+ function parseCreoApproachLabelFromImageFileName(fileName) {
101005
+ let s = fileName.replace(/\.[^/.]+$/i, '').trim();
101006
+ if (!s)
101007
+ return undefined;
101008
+ while (/\s+\(\d+\)$/.test(s)) {
101009
+ s = s.replace(/\s+\(\d+\)$/, '').trim();
101010
+ }
101011
+ const m = /^(\d+)_([0-9a-fA-F]{8})$/i.exec(s);
101012
+ if (m)
101013
+ return m[1];
101014
+ const legacy = s.replace(/_[0-9a-fA-F]{8}$/i, '').trim();
101015
+ return legacy || undefined;
101016
+ }
101017
+ const DEFAULT_CATALOG_TEXT_APPROACH_PARAM = 'sub19';
101018
+ const DEFAULT_CATALOG_CREO_APPROACH_PARAM = 'sub20';
101019
+ /** Имя query-параметра для трекера: латиница, цифры, _, - */
101020
+ function sanitizeCatalogUrlParamKey(raw, fallback) {
101021
+ const t = raw.trim();
101022
+ if (!t)
101023
+ return fallback;
101024
+ const cleaned = t.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
101025
+ return cleaned || fallback;
101026
+ }
101027
+ /** Добавляет к URL лендинга creative_id, опционально кастомные параметры подходов (имена редактируются в UI) и строку доп. макросов. */
101028
+ function appendCreativeIdToCatalogLink(baseUrl, creativeId, catalogAnalytics, extraMacrosQueryString) {
100990
101029
  const u = baseUrl.trim();
100991
101030
  if (!u)
100992
101031
  return u;
100993
101032
  const sep = u.includes('?') ? '&' : '?';
100994
- return `${u}${sep}creative_id=${encodeURIComponent(creativeId)}&${CATALOG_LINK_TRACKING_SUFFIX}`;
101033
+ const q = [`creative_id=${encodeURIComponent(creativeId)}`];
101034
+ const textKey = sanitizeCatalogUrlParamKey(catalogAnalytics?.textParamKey ?? '', DEFAULT_CATALOG_TEXT_APPROACH_PARAM);
101035
+ const creoKey = sanitizeCatalogUrlParamKey(catalogAnalytics?.creoParamKey ?? '', DEFAULT_CATALOG_CREO_APPROACH_PARAM);
101036
+ if (catalogAnalytics?.textApproach) {
101037
+ q.push(`${textKey}=${encodeURIComponent(catalogAnalytics.textApproach)}`);
101038
+ }
101039
+ if (catalogAnalytics?.creoApproach) {
101040
+ q.push(`${creoKey}=${encodeURIComponent(catalogAnalytics.creoApproach)}`);
101041
+ }
101042
+ const core = `${u}${sep}${q.join('&')}`;
101043
+ const tail = (extraMacrosQueryString ?? '').trim();
101044
+ return tail ? `${core}&${tail}` : core;
101045
+ }
101046
+ /** Текст ответа из OpenRouter/OpenAI chat completion (string или массив частей). */
101047
+ function extractChatCompletionText(choice) {
101048
+ const msg = choice?.message;
101049
+ if (!msg)
101050
+ return '';
101051
+ const c = msg.content;
101052
+ if (typeof c === 'string')
101053
+ return c;
101054
+ if (Array.isArray(c)) {
101055
+ return c
101056
+ .map((part) => (typeof part === 'string' ? part : part?.text ?? part?.content ?? ''))
101057
+ .filter(Boolean)
101058
+ .join('');
101059
+ }
101060
+ if (c != null && typeof c === 'object' && typeof c.text === 'string') {
101061
+ return c.text;
101062
+ }
101063
+ return '';
101064
+ }
101065
+ function stripMarkdownJsonFence(raw) {
101066
+ let s = raw.trim();
101067
+ if (!s.startsWith('```'))
101068
+ return s;
101069
+ s = s.replace(/^```(?:json)?\s*/i, '');
101070
+ const end = s.lastIndexOf('```');
101071
+ if (end >= 0)
101072
+ s = s.slice(0, end).trim();
101073
+ return s;
101074
+ }
101075
+ /** Парсит JSON-массив переводов из ответа модели (с ```json или мусором вокруг). */
101076
+ function parsePairTranslationsJson(raw) {
101077
+ const cleaned = stripMarkdownJsonFence(raw);
101078
+ try {
101079
+ const parsed = JSON.parse(cleaned);
101080
+ if (Array.isArray(parsed))
101081
+ return parsed;
101082
+ }
101083
+ catch {
101084
+ /* далее — вырезка по скобкам */
101085
+ }
101086
+ const jsonMatch = cleaned.match(/\[[\s\S]*\]/);
101087
+ if (!jsonMatch)
101088
+ return null;
101089
+ try {
101090
+ const parsed = JSON.parse(jsonMatch[0]);
101091
+ return Array.isArray(parsed) ? parsed : null;
101092
+ }
101093
+ catch {
101094
+ return null;
101095
+ }
101096
+ }
101097
+ /** Убирает повторяющиеся префиксы «ОШИБКА:» и лишние пробелы в строке из ответа валидатора. */
101098
+ function normalizeValidationErrorText(s) {
101099
+ let t = s.trim().replace(/\s+/g, ' ');
101100
+ while (/^ОШИБКА[:\s]+/i.test(t)) {
101101
+ t = t.replace(/^ОШИБКА[:\s]+/i, '').trim();
101102
+ }
101103
+ return t;
101104
+ }
101105
+ /** Одна и та же формулировка из ответа модели не дублируется в UI и в промпте переделки. */
101106
+ function dedupeValidationErrors(errors) {
101107
+ const seen = new Set();
101108
+ const out = [];
101109
+ for (const raw of errors) {
101110
+ const n = normalizeValidationErrorText(raw);
101111
+ if (!n)
101112
+ continue;
101113
+ const key = n.toLowerCase();
101114
+ if (seen.has(key))
101115
+ continue;
101116
+ seen.add(key);
101117
+ out.push(n);
101118
+ }
101119
+ return out;
101120
+ }
101121
+ /** Строка «ОШИБКА: нет» при статусе пересборки — противоречие; не считать содержательной ошибкой. */
101122
+ function isValidationNegationLine(s) {
101123
+ const t = normalizeValidationErrorText(s).toLowerCase();
101124
+ if (!t)
101125
+ return true;
101126
+ if (t === 'нет')
101127
+ return true;
101128
+ if (t.includes('нет ошибок') || t.includes('ошибок нет'))
101129
+ return true;
101130
+ if (t === 'none' || /^no errors?\.?$/i.test(t))
101131
+ return true;
101132
+ return false;
101133
+ }
101134
+ /** Все подписанные строки ОШИБКА:/ERROR: (модель иногда пишет по-английски). */
101135
+ function extractLabeledValidationErrors(content) {
101136
+ const out = [];
101137
+ const patterns = [
101138
+ /(?:^|\n)\s*ОШИБКА\s*[:\s]\s*([^\n]+)/gi,
101139
+ /(?:^|\n)\s*ERROR\s*[:\s]\s*([^\n]+)/gi,
101140
+ ];
101141
+ for (const re of patterns) {
101142
+ let m;
101143
+ const r = new RegExp(re.source, re.flags);
101144
+ while ((m = r.exec(content)) !== null) {
101145
+ const t = normalizeValidationErrorText(m[1]);
101146
+ if (t)
101147
+ out.push(t);
101148
+ }
101149
+ }
101150
+ return out;
101151
+ }
101152
+ /**
101153
+ * Если модель поставила НУЖНА ПЕРЕСБОРКА, но не оформила список как ОШИБКА: — забираем осмысленные строки
101154
+ * между статусом и блоком РЕКОМЕНДАЦИЯ (часто там остаётся описание причин).
101155
+ */
101156
+ function extractFallbackValidationErrorsBetweenStatusAndRecommendations(content) {
101157
+ const statusNeed = /СТАТУС\s*:\s*НУЖНА\s+ПЕРЕСБОРКА/i;
101158
+ const idx = content.search(statusNeed);
101159
+ if (idx < 0)
101160
+ return [];
101161
+ const tail = content.slice(idx);
101162
+ const firstNl = tail.indexOf('\n');
101163
+ const blockStart = firstNl >= 0 ? idx + firstNl + 1 : content.length;
101164
+ const rest = content.slice(blockStart);
101165
+ const recIdx = rest.search(/\nРЕКОМЕНДАЦИЯ\s*:/i);
101166
+ const block = recIdx >= 0 ? rest.slice(0, recIdx) : rest.slice(0, 3500);
101167
+ const lines = block.split('\n').map(l => l.trim()).filter(l => {
101168
+ if (l.length < 12)
101169
+ return false;
101170
+ if (/^(затем|список|каждая)\s/i.test(l))
101171
+ return false;
101172
+ if (/^ОШИБКА\s*[:\s]*нет\s*\.?$/i.test(l))
101173
+ return false;
101174
+ if (/^РЕКОМЕНДАЦИЯ\s*:/i.test(l))
101175
+ return false;
101176
+ if (/^ШАГ\s+[\dP]/i.test(l))
101177
+ return false;
101178
+ if (/^(?:HOOK|HEADLINE|PRICE|DISCOUNT|CTA|OTHER_TEXT)\s*:/i.test(l))
101179
+ return false;
101180
+ if (/^СТАТУС\s*:/i.test(l))
101181
+ return false;
101182
+ return true;
101183
+ });
101184
+ return lines
101185
+ .map(l => normalizeValidationErrorText(l.replace(/^ОШИБКА\s*[:\s]+/i, '')))
101186
+ .filter(Boolean);
101187
+ }
101188
+ /** Короткий фрагмент блока ФИНАЛ для подсказки, если структура ответа нестандартная. */
101189
+ function extractValidationFinalSnippet(content, maxLen) {
101190
+ const fi = content.search(/\bФИНАЛ\s*:/i);
101191
+ if (fi < 0)
101192
+ return null;
101193
+ let sn = content.slice(fi, fi + maxLen).replace(/\s+/g, ' ').trim();
101194
+ if (sn.length < 50)
101195
+ return null;
101196
+ if (sn.length > maxLen)
101197
+ sn = `${sn.slice(0, maxLen - 1)}…`;
101198
+ return sn;
100995
101199
  }
100996
101200
  /**
100997
101201
  * Разбор строк «ЗАГОЛОВОК n:» / «ТЕКСТ n:» из ответа модели.
@@ -101006,6 +101210,18 @@ function extractLeadingPriceNumber(value) {
101006
101210
  return m[1].replace(',', '.');
101007
101211
  return '99';
101008
101212
  }
101213
+ /** Фон карточки крео: после 1-й переделки — светло-жёлтый, далее темнеет с каждой следующей. */
101214
+ function getRemakeHighlightBackground(remakeCount, dark) {
101215
+ if (remakeCount <= 0)
101216
+ return undefined;
101217
+ const step = Math.min(remakeCount - 1, 7);
101218
+ if (dark) {
101219
+ const opacities = [0.11, 0.16, 0.22, 0.28, 0.34, 0.42, 0.5, 0.58];
101220
+ return `rgba(255, 193, 7, ${opacities[step]})`;
101221
+ }
101222
+ const light = ['#fffde7', '#fff9c4', '#fff59d', '#ffee58', '#ffeb3b', '#fdd835', '#fbc02d', '#f9a825'];
101223
+ return light[step];
101224
+ }
101009
101225
  function App() {
101010
101226
  const [clientId, setClientId] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
101011
101227
  const [clientSecret, setClientSecret] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
@@ -101022,6 +101238,7 @@ function App() {
101022
101238
  const [translatingPairs, setTranslatingPairs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
101023
101239
  const [driveFolderUrl, setDriveFolderUrl] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
101024
101240
  const [link, setLink] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
101241
+ const linkInputRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(null);
101025
101242
  const [openaiApiKey, setOpenaiApiKey] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
101026
101243
  const [openRouterBalance, setOpenRouterBalance] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
101027
101244
  const [openRouterAccountBalance, setOpenRouterAccountBalance] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
@@ -101242,6 +101459,16 @@ function App() {
101242
101459
  // const [availability, setAvailability] = useState('in stock');
101243
101460
  // const [condition, setCondition] = useState('new');
101244
101461
  const [linkError, setLinkError] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');
101462
+ /** В ссылку каталога: имя подхода к паре текстов (под трекер) */
101463
+ const [catalogUrlIncludeTextApproach, setCatalogUrlIncludeTextApproach] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
101464
+ /** В ссылку каталога: подход крео — если крео загружены из приложения (есть file id на Drive) */
101465
+ const [catalogUrlIncludeCreoApproach, setCatalogUrlIncludeCreoApproach] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
101466
+ /** Имя query-параметра для подхода к тексту (по умолчанию sub19) */
101467
+ const [catalogUrlTextApproachParam, setCatalogUrlTextApproachParam] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(DEFAULT_CATALOG_TEXT_APPROACH_PARAM);
101468
+ /** Имя query-параметра для подхода к крео (по умолчанию sub20) */
101469
+ const [catalogUrlCreoApproachParam, setCatalogUrlCreoApproachParam] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(DEFAULT_CATALOG_CREO_APPROACH_PARAM);
101470
+ /** Query-хвост после creative_id / sub19 / sub20 (редактируемый; пресет RedTrack). */
101471
+ const [catalogLinkExtraMacros, setCatalogLinkExtraMacros] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(DEFAULT_CATALOG_LINK_EXTRA_MACROS);
101245
101472
  const [loading, setLoading] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
101246
101473
  const [authLoading, setAuthLoading] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
101247
101474
  const [generatedData, setGeneratedData] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
@@ -101433,7 +101660,21 @@ function App() {
101433
101660
  }
101434
101661
  }, 1000);
101435
101662
  return () => clearTimeout(timeoutId);
101436
- }, [generateProduct, generateGeo, generateAdditionalInfo, generatePriceWithCurrency, brand, link, driveFolderUrl, loadingContentFromDrive]);
101663
+ }, [
101664
+ generateProduct,
101665
+ generateGeo,
101666
+ generateAdditionalInfo,
101667
+ generatePriceWithCurrency,
101668
+ brand,
101669
+ link,
101670
+ catalogUrlIncludeTextApproach,
101671
+ catalogUrlIncludeCreoApproach,
101672
+ catalogUrlTextApproachParam,
101673
+ catalogUrlCreoApproachParam,
101674
+ catalogLinkExtraMacros,
101675
+ driveFolderUrl,
101676
+ loadingContentFromDrive
101677
+ ]);
101437
101678
  // Load generated content from Google Drive when driveFolderUrl changes
101438
101679
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
101439
101680
  if (!driveFolderUrl) {
@@ -101448,6 +101689,8 @@ function App() {
101448
101689
  setTexts(['']);
101449
101690
  setLink('');
101450
101691
  setUploadedLink('');
101692
+ setCatalogLinkExtraMacros(DEFAULT_CATALOG_LINK_EXTRA_MACROS);
101693
+ setPairTranslations({});
101451
101694
  return;
101452
101695
  }
101453
101696
  const folderId = extractFolderId(driveFolderUrl);
@@ -101463,6 +101706,8 @@ function App() {
101463
101706
  setTexts(['']);
101464
101707
  setLink('');
101465
101708
  setUploadedLink('');
101709
+ setCatalogLinkExtraMacros(DEFAULT_CATALOG_LINK_EXTRA_MACROS);
101710
+ setPairTranslations({});
101466
101711
  return;
101467
101712
  }
101468
101713
  logToTerminal('log', '[Load] driveFolderUrl changed, clearing old data and loading from folderId:', folderId);
@@ -101474,6 +101719,8 @@ function App() {
101474
101719
  setTexts(['']);
101475
101720
  setLink('');
101476
101721
  setUploadedLink('');
101722
+ setCatalogLinkExtraMacros(DEFAULT_CATALOG_LINK_EXTRA_MACROS);
101723
+ setPairTranslations({});
101477
101724
  setLoadingContentFromDrive(true);
101478
101725
  setDriveFilesFound({ content: false });
101479
101726
  // Load content from Google Drive
@@ -102802,22 +103049,33 @@ function App() {
102802
103049
  },
102803
103050
  body: JSON.stringify(requestBody)
102804
103051
  });
102805
- if (!response.ok)
103052
+ if (!response.ok) {
103053
+ logToTerminal('warn', '[Translate RU] HTTP', response.status, '— перевод пар пропущен');
102806
103054
  return;
103055
+ }
102807
103056
  const data = await response.json();
102808
- const raw = data.choices?.[0]?.message?.content || '';
102809
- const jsonMatch = raw.match(/\[[\s\S]*\]/);
102810
- if (!jsonMatch)
103057
+ const choice = data.choices?.[0];
103058
+ let raw = extractChatCompletionText(choice) ||
103059
+ (typeof choice?.text === 'string' ? choice.text : '') ||
103060
+ '';
103061
+ if (!raw.trim()) {
103062
+ logToTerminal('warn', '[Translate RU] Пустой ответ модели (content), перевод не выполнен');
103063
+ return;
103064
+ }
103065
+ const parsed = parsePairTranslationsJson(raw);
103066
+ if (!parsed || parsed.length === 0) {
103067
+ logToTerminal('warn', '[Translate RU] Не удалось разобрать JSON-массив переводов, превью:', raw.substring(0, 280));
102811
103068
  return;
102812
- const parsed = JSON.parse(jsonMatch[0]);
103069
+ }
102813
103070
  const translations = {};
102814
103071
  parsed.forEach((item, idx) => {
102815
103072
  translations[idx] = { titleRu: item.titleRu || '', textRu: item.textRu || '' };
102816
103073
  });
102817
103074
  setPairTranslations(translations);
103075
+ logToTerminal('log', '[Translate RU] OK, пар переведено:', parsed.length);
102818
103076
  }
102819
103077
  catch {
102820
- // тихо игнорируем ошибки переводаэто вспомогательная функция
103078
+ logToTerminal('warn', '[Translate RU] Ошибка запроса или разбора см. консоль / лог');
102821
103079
  }
102822
103080
  finally {
102823
103081
  setTranslatingPairs(false);
@@ -103339,7 +103597,21 @@ function App() {
103339
103597
  };
103340
103598
  const keywords = geoKeywords[geo.toUpperCase()] || ['продукт', 'проблема'];
103341
103599
  logMsg('log', `🔑 Ключевые слова: ${keywords.join(', ')}`);
103342
- const validationPrompt = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getValidationPrompt)(product, geo, keywords, approachName);
103600
+ const { price: briefPrice, currency: briefCurrency, currencySymbol } = parsePriceAndCurrency(generatePriceWithCurrency);
103601
+ let priceBriefForValidation = '';
103602
+ if (briefPrice && briefCurrency) {
103603
+ const nv = parseFloat(String(briefPrice).replace(',', '.'));
103604
+ const oldNum = Number.isFinite(nv) && nv > 0 ? nv * 2 : NaN;
103605
+ const oldStr = Number.isFinite(oldNum)
103606
+ ? (Number.isInteger(oldNum) ? String(oldNum) : (Math.round(oldNum * 100) / 100).toString().replace(/\.?0+$/, ''))
103607
+ : '';
103608
+ priceBriefForValidation = `Новая цена по брифу (после скидки -50%): ${briefPrice} ${briefCurrency}${currencySymbol ? `, символ валюты: ${currencySymbol}` : ''}. ${Number.isFinite(oldNum) ? `Ожидаемая старая цена до скидки: ${oldStr} ${briefCurrency} (2× новой).` : 'Старая цена на макете должна быть в 2 раза больше новой.'} Любые другие суммы — ошибка «не совпадает с брифом».`;
103609
+ logMsg('log', `💶 Эталон цен для валидации: новая ${briefPrice} ${briefCurrency}${oldStr ? `, старая ${oldStr}` : ''}`);
103610
+ }
103611
+ else {
103612
+ logMsg('log', '💶 Поле цены в приложении пустое — валидатор сверяет только визуал двух цен');
103613
+ }
103614
+ const validationPrompt = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getValidationPrompt)(product, geo, keywords, approachName, undefined, priceBriefForValidation);
103343
103615
  const requestBody = {
103344
103616
  model: selectedValidationModel,
103345
103617
  messages: [
@@ -103478,25 +103750,28 @@ function App() {
103478
103750
  }
103479
103751
  // Update balance after successful API call
103480
103752
  fetchOpenRouterBalance();
103481
- // Парсим результат
103482
- const statusMatch = content.match(/СТАТУС:\s*(OK|НУЖНА ПЕРЕСБОРКА)/i);
103483
- const status = statusMatch ? (statusMatch[1].toUpperCase() === 'OK' ? 'ok' : 'needs_rebuild') : 'needs_rebuild';
103484
- // Извлекаем ошибки
103485
- const errors = [];
103486
- const errorMatches = content.match(/ОШИБКА[:\s]+([^\n]+)/gi);
103487
- if (errorMatches) {
103488
- errors.push(...errorMatches
103489
- .map(m => m.replace(/ОШИБКА[:\s]+/i, '').trim())
103490
- .filter(err => {
103491
- const lowerErr = err.toLowerCase();
103492
- // Игнорируем "нет", "нет ошибок", "ошибок нет" и подобные
103493
- return lowerErr !== 'нет' &&
103494
- !lowerErr.includes('нет ошибок') &&
103495
- !lowerErr.includes('ошибок нет') &&
103496
- err.length > 0;
103497
- }));
103753
+ // Парсим результат (английский STATUS — на случай ответа модели не по шаблону)
103754
+ const statusMatchRu = content.match(/СТАТУС\s*:\s*(OK|НУЖНА\s+ПЕРЕСБОРКА)/i);
103755
+ const statusMatchEn = content.match(/\bSTATUS\s*:\s*(OK|NEEDS?\s+REBUILD|FAIL|FAILED)\b/i);
103756
+ const statusToken = statusMatchRu?.[1] ?? statusMatchEn?.[1] ?? '';
103757
+ const status = (() => {
103758
+ if (!statusToken)
103759
+ return 'needs_rebuild';
103760
+ const u = statusToken.toUpperCase().replace(/\s+/g, ' ');
103761
+ if (u === 'OK')
103762
+ return 'ok';
103763
+ if (u.includes('NEED') || u === 'FAIL' || u === 'FAILED')
103764
+ return 'needs_rebuild';
103765
+ if (u.includes('НУЖНА') || u.includes('ПЕРЕСБОРКА'))
103766
+ return 'needs_rebuild';
103767
+ return 'needs_rebuild';
103768
+ })();
103769
+ let errors = extractLabeledValidationErrors(content).filter(e => !isValidationNegationLine(e));
103770
+ errors = dedupeValidationErrors(errors);
103771
+ // Противоречие: пересборка, а единственная строка была «ОШИБКА: нет» — тянем текст из хвоста ответа
103772
+ if (status === 'needs_rebuild' && errors.length === 0) {
103773
+ errors = dedupeValidationErrors(extractFallbackValidationErrorsBetweenStatusAndRecommendations(content));
103498
103774
  }
103499
- // Если статус "НУЖНА ПЕРЕСБОРКА", но ошибок нет в формате ОШИБКА, ищем в тексте
103500
103775
  if (status === 'needs_rebuild' && errors.length === 0) {
103501
103776
  const lines = content.split('\n').filter(line => line.includes('нарушено') ||
103502
103777
  line.includes('проблема') ||
@@ -103504,10 +103779,20 @@ function App() {
103504
103779
  line.includes('не хватает'));
103505
103780
  if (lines.length > 0) {
103506
103781
  errors.push(...lines.map(l => l.trim()).filter(l => l.length > 0));
103782
+ errors = dedupeValidationErrors(errors);
103507
103783
  }
103508
103784
  }
103509
103785
  const totalElapsed = Date.now() - validationStartTime;
103510
- const finalErrors = errors.length > 0 ? errors : (status === 'needs_rebuild' ? ['Обнаружены нарушения в креативе'] : []);
103786
+ const snippet = status === 'needs_rebuild' && errors.length === 0
103787
+ ? extractValidationFinalSnippet(content, 900)
103788
+ : null;
103789
+ const finalErrors = errors.length > 0
103790
+ ? errors
103791
+ : status === 'needs_rebuild'
103792
+ ? (snippet
103793
+ ? [`Валидатор не выписал строки «ОШИБКА:». Фрагмент ответа: ${snippet}`]
103794
+ : ['Валидатор указал пересборку, но не перечислил причины (нет строк «ОШИБКА:»). Откройте полный текст ответа модели в логе или повторите проверку.'])
103795
+ : [];
103511
103796
  logMsg('log', `✅ === Валидация завершена ===`);
103512
103797
  logMsg('log', `⏱️ Общее время: ${Math.round(totalElapsed / 1000)}s`);
103513
103798
  logMsg('log', `📊 Статус: ${status === 'ok' ? '✅ OK' : '❌ НУЖНА ПЕРЕСБОРКА'}`);
@@ -104146,8 +104431,8 @@ function App() {
104146
104431
  }
104147
104432
  addLog(formatLogMessage('log', '✅ Product image found'));
104148
104433
  // Generate images with different approaches (количество по каждому подходу)
104149
- const approaches = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getCreoApproaches)();
104150
- if (approaches.length === 0) {
104434
+ const expandedTasks = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getCreoApproachExpandedTasks)();
104435
+ if (expandedTasks.length === 0) {
104151
104436
  addLog(formatLogMessage('error', '❌ Укажите количество изображений (хотя бы 1) в настройках подходов'));
104152
104437
  alert('Укажите количество изображений (1–4) хотя бы для одного подхода в настройках');
104153
104438
  setGeneratingImages(false);
@@ -104156,11 +104441,15 @@ function App() {
104156
104441
  // При "both" — на каждый подход генерируем и 1:1, и 2:3
104157
104442
  const useBoth = imageAspectRatio === 'both';
104158
104443
  const tasks = useBoth
104159
- ? approaches.flatMap(a => [
104160
- { approach: a, ratio: '1:1' },
104161
- { approach: a, ratio: '2:3' }
104444
+ ? expandedTasks.flatMap(t => [
104445
+ { approach: t.approach, poolIndex: t.poolIndex, ratio: '1:1' },
104446
+ { approach: t.approach, poolIndex: t.poolIndex, ratio: '2:3' }
104162
104447
  ])
104163
- : approaches.map(a => ({ approach: a, ratio: imageAspectRatio }));
104448
+ : expandedTasks.map(t => ({
104449
+ approach: t.approach,
104450
+ poolIndex: t.poolIndex,
104451
+ ratio: imageAspectRatio
104452
+ }));
104164
104453
  const additionalInfoLine = generateAdditionalInfo.trim()
104165
104454
  ? `\n📋 ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ О ПРОДУКТЕ: ${generateAdditionalInfo.trim()}`
104166
104455
  : '';
@@ -104172,13 +104461,14 @@ function App() {
104172
104461
  const [b1, b2, b3] = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.pickRandomBullets)(t.approach.name);
104173
104462
  return `ОБЯЗАТЕЛЬНЫЕ БУЛЛЕТЫ (используй именно эти 3, в указанном порядке; переведи на язык ${generateGeo}): 1) ${b1} 2) ${b2} 3) ${b3}`;
104174
104463
  })();
104175
- 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 обязателен; буллиты добавляй только если они разрешены для подхода).`;
104464
+ 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/CTA/CAPS; буллиты добавляй только если они разрешены для подхода).`;
104176
104465
  });
104177
104466
  // Initialize placeholders for all images
104178
104467
  const initialPlaceholders = tasks.map((t, index) => ({
104179
104468
  index: index + 1,
104180
104469
  imageUrl: undefined,
104181
104470
  approach: t.approach.name + (useBoth ? ` (${t.ratio})` : ''),
104471
+ creoApproachUiNumber: t.poolIndex + 1,
104182
104472
  aspectRatio: t.ratio,
104183
104473
  uploaded: false,
104184
104474
  checking: false,
@@ -104190,7 +104480,8 @@ function App() {
104190
104480
  regenerating: false,
104191
104481
  customRegeneratePrompt: '',
104192
104482
  failed: false,
104193
- generating: false // In sequential mode, images start queued (not generating)
104483
+ generating: false, // In sequential mode, images start queued (not generating)
104484
+ remakeCount: 0
104194
104485
  }));
104195
104486
  setGeneratedImagesData(initialPlaceholders);
104196
104487
  addLog(formatLogMessage('log', `📝 Generated prompts for ${tasks.length} images${useBoth ? ' (1:1 + 2:3 на каждый подход)' : ''}`));
@@ -104347,7 +104638,7 @@ function App() {
104347
104638
  imageUrl: null,
104348
104639
  success: false,
104349
104640
  error: new Error('Generation did not complete'),
104350
- approach: approaches[i]?.name || 'Unknown',
104641
+ approach: tasks[i]?.approach.name || 'Unknown',
104351
104642
  originalPrompt: imagePrompts[i],
104352
104643
  productImageUrl: productImage.url
104353
104644
  });
@@ -104599,7 +104890,8 @@ ${imageData.originalPrompt}
104599
104890
  checkErrors: undefined,
104600
104891
  uploaded: false, // Reset uploaded status since it's a new image
104601
104892
  customRegeneratePrompt: '', // Reset custom prompt after regeneration
104602
- failed: false // Mark as successful
104893
+ failed: false, // Mark as successful
104894
+ remakeCount: (img.remakeCount ?? 0) + 1
104603
104895
  }
104604
104896
  : img));
104605
104897
  // Run validation on the new image (skip if disabled)
@@ -104714,7 +105006,8 @@ ${imageData.originalPrompt}
104714
105006
  checkErrors: undefined,
104715
105007
  uploaded: false,
104716
105008
  customRegeneratePrompt: '',
104717
- failed: false
105009
+ failed: false,
105010
+ remakeCount: (img.remakeCount ?? 0) + 1
104718
105011
  }
104719
105012
  : img));
104720
105013
  // Validate new image (skip if disabled)
@@ -104836,11 +105129,10 @@ ${imageData.originalPrompt}
104836
105129
  logToTerminal('log', msg.replace(/\[.*?\]\s*/, ''));
104837
105130
  };
104838
105131
  try {
104839
- const filename = `${generateProduct.replace(/\s+/g, '_')}_${imageData.index}_${Date.now()}.png`;
105132
+ const filename = creativeImageUploadFilename(imageData.creoApproachUiNumber);
104840
105133
  addLog(formatLogMessage('log', `📤 Uploading image ${imageData.index} to Drive: ${filename}`));
104841
105134
  const driveUrl = await uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog);
104842
105135
  addLog(formatLogMessage('log', `✅ Image ${imageData.index} uploaded: ${driveUrl}`));
104843
- // Update uploaded status
104844
105136
  setGeneratedImagesData(prev => prev.map(img => img.index === imageData.index ? { ...img, uploaded: true } : img));
104845
105137
  // (Removed legacy Generated Images links list)
104846
105138
  }
@@ -104880,9 +105172,8 @@ ${imageData.originalPrompt}
104880
105172
  : img));
104881
105173
  try {
104882
105174
  addLog(formatLogMessage('log', `📤 Uploading ${notUploaded.length} image(s) to Drive (parallel)...`));
104883
- const baseTs = Date.now();
104884
- const results = await Promise.allSettled(notUploaded.map((imageData, i) => {
104885
- const filename = `${generateProduct.replace(/\s+/g, '_')}_${imageData.index}_${baseTs}_${i}.png`;
105175
+ const results = await Promise.allSettled(notUploaded.map((imageData) => {
105176
+ const filename = creativeImageUploadFilename(imageData.creoApproachUiNumber);
104886
105177
  addLog(formatLogMessage('log', `📤 Starting upload image ${imageData.index}: ${filename}`));
104887
105178
  return uploadImageToDrive(imageData.imageUrl, filename, folderId, addLog).then(driveUrl => ({
104888
105179
  index: imageData.index,
@@ -104948,8 +105239,12 @@ ${imageData.originalPrompt}
104948
105239
  try {
104949
105240
  const parsed = new URL(toParse);
104950
105241
  let path = parsed.pathname || '/';
104951
- if (!path.endsWith('/'))
104952
- path += '/';
105242
+ // Корень сайта — оставляем как `/`. Если есть «тело» пути (не один домен), завершающий `/` убираем, не добавляем.
105243
+ if (path !== '/' && path.length > 1) {
105244
+ while (path.length > 1 && path.endsWith('/')) {
105245
+ path = path.slice(0, -1);
105246
+ }
105247
+ }
104953
105248
  const result = `https://${parsed.host}${path}${parsed.search}${parsed.hash}`;
104954
105249
  setLink(result);
104955
105250
  setLinkError('');
@@ -104958,6 +105253,22 @@ ${imageData.originalPrompt}
104958
105253
  // invalid — leave as is
104959
105254
  }
104960
105255
  };
105256
+ const handleLinkPaste = (e) => {
105257
+ e.preventDefault();
105258
+ const pasted = e.clipboardData.getData('text/plain');
105259
+ const cleanedPaste = pasted.split('?')[0] ?? pasted;
105260
+ const input = e.currentTarget;
105261
+ const start = input.selectionStart ?? 0;
105262
+ const end = input.selectionEnd ?? 0;
105263
+ const newVal = link.slice(0, start) + cleanedPaste + link.slice(end);
105264
+ handleLinkChange(newVal);
105265
+ const pos = start + cleanedPaste.length;
105266
+ setTimeout(() => {
105267
+ const el = linkInputRef.current;
105268
+ if (el)
105269
+ el.setSelectionRange(pos, pos);
105270
+ }, 0);
105271
+ };
104961
105272
  const extractFolderId = (url) => {
104962
105273
  // Try to match format: /folders/([a-zA-Z0-9_-]+)
104963
105274
  const foldersMatch = url.match(/folders\/([a-zA-Z0-9_-]+)/);
@@ -105004,7 +105315,13 @@ ${imageData.originalPrompt}
105004
105315
  const name = f.name?.toLowerCase() || '';
105005
105316
  return name !== 'product.png' && name !== 'product.jpg' && name !== 'product.webp';
105006
105317
  });
105007
- return filteredFiles.map((f) => f.id ? `https://drive.google.com/file/d/${f.id}/view?usp=sharing` : '');
105318
+ return filteredFiles
105319
+ .filter((f) => f.id && f.name)
105320
+ .map((f) => ({
105321
+ fileId: f.id,
105322
+ name: f.name,
105323
+ viewUrl: `https://drive.google.com/file/d/${f.id}/view?usp=sharing`
105324
+ }));
105008
105325
  };
105009
105326
  const fetchProductImage = async (folderId) => {
105010
105327
  const validToken = await getValidAccessToken();
@@ -105137,6 +105454,11 @@ ${imageData.originalPrompt}
105137
105454
  },
105138
105455
  brand: brandValue !== undefined ? brandValue : brand || '',
105139
105456
  link: linkValue !== undefined ? linkValue : link || '',
105457
+ catalogUrlIncludeTextApproach,
105458
+ catalogUrlIncludeCreoApproach,
105459
+ catalogUrlTextApproachParam,
105460
+ catalogUrlCreoApproachParam,
105461
+ catalogLinkExtraMacros,
105140
105462
  selectedPairApproaches: (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_52__.getSelectedPairApproaches)(),
105141
105463
  imageApproachCounts: (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_52__.getImageApproachCounts)(),
105142
105464
  savedAt: new Date().toISOString()
@@ -105287,6 +105609,24 @@ ${imageData.originalPrompt}
105287
105609
  setLink(loadedData.link || '');
105288
105610
  logToTerminal('log', '[Load Content] Loaded link:', loadedData.link || '(empty)');
105289
105611
  }
105612
+ if (typeof loadedData.catalogUrlIncludeTextApproach === 'boolean') {
105613
+ setCatalogUrlIncludeTextApproach(loadedData.catalogUrlIncludeTextApproach);
105614
+ }
105615
+ if (typeof loadedData.catalogUrlIncludeCreoApproach === 'boolean') {
105616
+ setCatalogUrlIncludeCreoApproach(loadedData.catalogUrlIncludeCreoApproach);
105617
+ }
105618
+ if (typeof loadedData.catalogUrlTextApproachParam === 'string' && loadedData.catalogUrlTextApproachParam.trim()) {
105619
+ setCatalogUrlTextApproachParam(loadedData.catalogUrlTextApproachParam.trim());
105620
+ }
105621
+ if (typeof loadedData.catalogUrlCreoApproachParam === 'string' && loadedData.catalogUrlCreoApproachParam.trim()) {
105622
+ setCatalogUrlCreoApproachParam(loadedData.catalogUrlCreoApproachParam.trim());
105623
+ }
105624
+ if (typeof loadedData.catalogLinkExtraMacros === 'string') {
105625
+ setCatalogLinkExtraMacros(loadedData.catalogLinkExtraMacros);
105626
+ }
105627
+ else {
105628
+ setCatalogLinkExtraMacros(DEFAULT_CATALOG_LINK_EXTRA_MACROS);
105629
+ }
105290
105630
  const mergedApproaches = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_52__.mergePromptApproachesFromDriveFile)(loadedData);
105291
105631
  if (mergedApproaches) {
105292
105632
  logToTerminal('log', '[Load Content] Applied text + image approach settings from Drive JSON');
@@ -105375,9 +105715,17 @@ ${imageData.originalPrompt}
105375
105715
  for (let i = 0; i < pairCount; i++) {
105376
105716
  const title = titleList[i];
105377
105717
  const text = textList[i];
105718
+ const pairApproachIdx = lastUsedApproachIndices[i] ?? i;
105719
+ const textApproachUiNumber = String(pairApproachIdx + 1);
105378
105720
  for (const image of images) {
105379
105721
  const id = `${brand}${idCounter++}`;
105380
- const rowLink = appendCreativeIdToCatalogLink(link, id);
105722
+ const creoFromFileName = parseCreoApproachLabelFromImageFileName(image.name);
105723
+ const rowLink = appendCreativeIdToCatalogLink(link, id, {
105724
+ textApproach: catalogUrlIncludeTextApproach ? textApproachUiNumber : undefined,
105725
+ creoApproach: catalogUrlIncludeCreoApproach && creoFromFileName ? creoFromFileName : undefined,
105726
+ textParamKey: catalogUrlTextApproachParam,
105727
+ creoParamKey: catalogUrlCreoApproachParam
105728
+ }, catalogLinkExtraMacros);
105381
105729
  rows.push([
105382
105730
  id,
105383
105731
  title,
@@ -105386,7 +105734,7 @@ ${imageData.originalPrompt}
105386
105734
  'new',
105387
105735
  '10,00 USD',
105388
105736
  rowLink,
105389
- image,
105737
+ image.viewUrl,
105390
105738
  brand
105391
105739
  ]);
105392
105740
  }
@@ -105722,7 +106070,23 @@ ${imageData.originalPrompt}
105722
106070
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: { xs: 'column', sm: 'row' }, spacing: 2 },
105723
106071
  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" }),
105724
106072
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { flex: 1, minWidth: 0 } },
105725
- 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/" }))),
106073
+ 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/", inputRef: linkInputRef, InputProps: { onPaste: handleLinkPaste } }))),
106074
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', gap: 1, alignItems: 'flex-start', mt: 1.5 } },
106075
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "\u0414\u043E\u043F. \u043C\u0430\u043A\u0440\u043E\u0441\u044B (\u043A\u0430\u0442\u0430\u043B\u043E\u0433)", variant: "outlined", size: "small", fullWidth: true, multiline: true, minRows: 2, value: catalogLinkExtraMacros, onChange: (e) => setCatalogLinkExtraMacros(e.target.value), placeholder: DEFAULT_CATALOG_LINK_EXTRA_MACROS, helperText: "\u0414\u043E\u0431\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 creative_id \u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432 \u043F\u043E\u0434\u0445\u043E\u0434\u043E\u0432. \u0421\u043E\u0445\u0440\u0430\u043D\u044F\u0435\u0442\u0441\u044F \u0432 JSON \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043A \u043F\u0430\u043F\u043A\u0438.", sx: { '& .MuiInputBase-input': { fontFamily: 'monospace', fontSize: 12 } } }),
106076
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 0.5, sx: { flexShrink: 0, mt: 0.5 } },
106077
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", onClick: () => setCatalogLinkExtraMacros(DEFAULT_CATALOG_LINK_EXTRA_MACROS), sx: { textTransform: 'none', whiteSpace: 'nowrap' } }, "redtrack"),
106078
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_17__["default"], { size: "small", variant: "outlined", onClick: () => setCatalogLinkExtraMacros(DEFAULT_CATALOG_LINK_KEITARO_MACROS), sx: { textTransform: 'none', whiteSpace: 'nowrap' } }, "keitaro"))),
106079
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { spacing: 1.5, sx: { mt: 1 } },
106080
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: { xs: 'column', sm: 'row' }, spacing: 2, alignItems: { xs: 'stretch', sm: 'flex-start' } },
106081
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_21__["default"], { control: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { checked: catalogUrlIncludeTextApproach, onChange: e => setCatalogUrlIncludeTextApproach(e.target.checked), size: "small" }), label: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
106082
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2" }, "\u041F\u043E\u0434\u0445\u043E\u0434 \u043A \u0442\u0435\u043A\u0441\u0442\u0443 \u0432 URL \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430"),
106083
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", color: "text.secondary" }, "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u2014 \u043D\u043E\u043C\u0435\u0440 \u0441\u0442\u0440\u043E\u043A\u0438 \u043F\u043E\u0434\u0445\u043E\u0434\u0430 \u0434\u043B\u044F \u043F\u0430\u0440 \u0432 \u0442\u0430\u0431\u043B\u0438\u0446\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043A (1\u201310)")), sx: { alignItems: 'flex-start', mr: 0, flex: { sm: '1 1 200px' }, minWidth: 0 } }),
106084
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0434\u043B\u044F \u0442\u0435\u043A\u0441\u0442\u0430", size: "small", value: catalogUrlTextApproachParam, onChange: e => setCatalogUrlTextApproachParam(e.target.value), placeholder: DEFAULT_CATALOG_TEXT_APPROACH_PARAM, sx: { width: { xs: '100%', sm: 200 }, flexShrink: 0 }, helperText: `По умолчанию: ${DEFAULT_CATALOG_TEXT_APPROACH_PARAM}` })),
106085
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: { xs: 'column', sm: 'row' }, spacing: 2, alignItems: { xs: 'stretch', sm: 'flex-start' } },
106086
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_21__["default"], { control: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { checked: catalogUrlIncludeCreoApproach, onChange: e => setCatalogUrlIncludeCreoApproach(e.target.checked), size: "small" }), label: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
106087
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "body2" }, "\u041F\u043E\u0434\u0445\u043E\u0434 \u043A \u043A\u0440\u0435\u043E \u0432 URL \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430"),
106088
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", color: "text.secondary" }, "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u2014 \u043D\u043E\u043C\u0435\u0440 N \u0438\u0437 \u0438\u043C\u0435\u043D\u0438 \u0444\u0430\u0439\u043B\u0430 N_xxxxxxxx.png \u043D\u0430 Drive (N = \u0441\u0442\u0440\u043E\u043A\u0430 \u043F\u043E\u0434\u0445\u043E\u0434\u0430 \u043A \u043A\u0440\u0435\u043E, 1\u201310); \u00AB (1)\u00BB \u043E\u0442 \u0434\u0443\u0431\u043B\u0438\u043A\u0430\u0442\u0430 Google \u0441\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F")), sx: { alignItems: 'flex-start', mr: 0, flex: { sm: '1 1 200px' }, minWidth: 0 } }),
106089
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0434\u043B\u044F \u043A\u0440\u0435\u043E", size: "small", value: catalogUrlCreoApproachParam, onChange: e => setCatalogUrlCreoApproachParam(e.target.value), placeholder: DEFAULT_CATALOG_CREO_APPROACH_PARAM, sx: { width: { xs: '100%', sm: 200 }, flexShrink: 0 }, helperText: `По умолчанию: ${DEFAULT_CATALOG_CREO_APPROACH_PARAM}` }))),
105726
106090
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_16__["default"], { sx: { my: 2 } }),
105727
106091
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "h6", gutterBottom: true }, "AI Generation Settings"),
105728
106092
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
@@ -105730,20 +106094,20 @@ ${imageData.originalPrompt}
105730
106094
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
105731
106095
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "\u0413\u0435\u043E", variant: "outlined", fullWidth: true, value: generateGeo, onChange: (e) => setGenerateGeo(e.target.value), placeholder: "\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: \u0420\u0443\u043C\u044B\u043D\u0438\u044F" }),
105732
106096
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_15__["default"], { label: "\u0426\u0435\u043D\u0430 \u0438 \u0432\u0430\u043B\u044E\u0442\u0430", variant: "outlined", fullWidth: true, value: generatePriceWithCurrency, onChange: (e) => setGeneratePriceWithCurrency(e.target.value), placeholder: "\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: 29 euro, 11400 HUF, 149 RON \u0438\u043B\u0438 \u043B\u044E\u0431\u043E\u0439 \u0434\u0440\u0443\u0433\u043E\u0439 \u0444\u043E\u0440\u043C\u0430\u0442", helperText: "\u041B\u044E\u0431\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439. \u041A\u043D\u043E\u043F\u043A\u0438 \u0441\u043F\u0440\u0430\u0432\u0430 \u043F\u043E\u0434\u0441\u0442\u0430\u0432\u043B\u044F\u044E\u0442 \u0441\u0438\u043C\u0432\u043E\u043B \u0432\u0430\u043B\u044E\u0442\u044B, \u0446\u0438\u0444\u0440\u0443 \u0431\u0435\u0440\u0443\u0442 \u0438\u0437 \u043F\u043E\u043B\u044F (\u0438\u043B\u0438 99).", InputProps: {
105733
- endAdornment: (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_21__["default"], { position: "end" },
106097
+ endAdornment: (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_23__["default"], { position: "end" },
105734
106098
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 0.25, mr: -0.5 } },
105735
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { title: "\u041F\u043E\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C $ (\u0446\u0438\u0444\u0440\u0430 \u0438\u0437 \u043F\u043E\u043B\u044F \u0438\u043B\u0438 99)" },
106099
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["default"], { title: "\u041F\u043E\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C $ (\u0446\u0438\u0444\u0440\u0430 \u0438\u0437 \u043F\u043E\u043B\u044F \u0438\u043B\u0438 99)" },
105736
106100
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { size: "small", "aria-label": "\u0411\u044B\u0441\u0442\u0440\u043E \u0434\u043E\u043B\u043B\u0430\u0440", onClick: () => {
105737
106101
  const n = extractLeadingPriceNumber(generatePriceWithCurrency);
105738
106102
  setGeneratePriceWithCurrency(`$${n}`);
105739
106103
  }, edge: "end" },
105740
106104
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_35__["default"], { sx: { fontSize: 20 } }))),
105741
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { title: "\u041F\u043E\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u20AC (\u0446\u0438\u0444\u0440\u0430 \u0438\u0437 \u043F\u043E\u043B\u044F \u0438\u043B\u0438 99)" },
106105
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["default"], { title: "\u041F\u043E\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u20AC (\u0446\u0438\u0444\u0440\u0430 \u0438\u0437 \u043F\u043E\u043B\u044F \u0438\u043B\u0438 99)" },
105742
106106
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { size: "small", "aria-label": "\u0411\u044B\u0441\u0442\u0440\u043E \u0435\u0432\u0440\u043E", onClick: () => {
105743
106107
  const n = extractLeadingPriceNumber(generatePriceWithCurrency);
105744
106108
  setGeneratePriceWithCurrency(`€${n}`);
105745
106109
  }, edge: "end", sx: { minWidth: 34, fontSize: '1rem', fontWeight: 700 } }, "\u20AC")),
105746
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { title: "\u041F\u043E\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C z\u0142 (\u0446\u0438\u0444\u0440\u0430 \u0438\u0437 \u043F\u043E\u043B\u044F \u0438\u043B\u0438 99)" },
106110
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["default"], { title: "\u041F\u043E\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C z\u0142 (\u0446\u0438\u0444\u0440\u0430 \u0438\u0437 \u043F\u043E\u043B\u044F \u0438\u043B\u0438 99)" },
105747
106111
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { size: "small", "aria-label": "\u0411\u044B\u0441\u0442\u0440\u043E \u0437\u043B\u043E\u0442\u044B\u0435", onClick: () => {
105748
106112
  const n = extractLeadingPriceNumber(generatePriceWithCurrency);
105749
106113
  setGeneratePriceWithCurrency(`${n} zł`);
@@ -105752,29 +106116,29 @@ ${imageData.originalPrompt}
105752
106116
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
105753
106117
  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\u0430\u044F \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F (\u043D\u0435 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E)", variant: "outlined", fullWidth: true, multiline: true, minRows: 3, value: generateAdditionalInfo, onChange: (e) => setGenerateAdditionalInfo(e.target.value), placeholder: "\u0418\u043D\u0433\u0440\u0435\u0434\u0438\u0435\u043D\u0442\u044B, \u0443\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u044F \u043A \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u0438 \u0442.\u0434.", sx: { mb: 2 } })),
105754
106118
  openaiApiKey && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
105755
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_23__["default"], { fullWidth: true, variant: "outlined" },
105756
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["default"], null, "\u041C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439"),
105757
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_25__["default"], { value: selectedImageModel, onChange: (e) => handleImageModelChange(e.target.value), label: "\u041C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439", disabled: loadingImageModels || imageModels.length === 0 }, loadingImageModels ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_26__["default"], { disabled: true },
106119
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_25__["default"], { fullWidth: true, variant: "outlined" },
106120
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_26__["default"], null, "\u041C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439"),
106121
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["default"], { value: selectedImageModel, onChange: (e) => handleImageModelChange(e.target.value), label: "\u041C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439", disabled: loadingImageModels || imageModels.length === 0 }, loadingImageModels ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_28__["default"], { disabled: true },
105758
106122
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
105759
106123
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }),
105760
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043C\u043E\u0434\u0435\u043B\u0435\u0439...")))) : imageModels.length === 0 ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_26__["default"], { disabled: true }, "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043C\u043E\u0434\u0435\u043B\u0435\u0439")) : (imageModels.map((model) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_26__["default"], { key: model.id, value: model.id }, model.name))))),
106124
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043C\u043E\u0434\u0435\u043B\u0435\u0439...")))) : imageModels.length === 0 ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_28__["default"], { disabled: true }, "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043C\u043E\u0434\u0435\u043B\u0435\u0439")) : (imageModels.map((model) => (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_28__["default"], { key: model.id, value: model.id }, model.name))))),
105761
106125
  !loadingImageModels && imageModels.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], null, selectedImageModel === _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.imageGeneration
105762
106126
  ? 'Используется модель по умолчанию'
105763
106127
  : 'Выбрана модель: ' + (imageModels.find(m => m.id === selectedImageModel)?.name || selectedImageModel)))),
105764
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_23__["default"], { fullWidth: true, variant: "outlined" },
105765
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["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"),
105766
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_25__["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_26__["default"], { disabled: true },
106128
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_25__["default"], { fullWidth: true, variant: "outlined" },
106129
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_26__["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"),
106130
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["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_28__["default"], { disabled: true },
105767
106131
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
105768
106132
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 16 }),
105769
- 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_26__["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_26__["default"], { key: model.id, value: model.id }, model.name))))),
106133
+ 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_28__["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_28__["default"], { key: model.id, value: model.id }, model.name))))),
105770
106134
  !loadingValidationModels && validationModels.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_19__["default"], null, selectedValidationModel === _models__WEBPACK_IMPORTED_MODULE_2__.MODELS.creativeValidation
105771
106135
  ? 'Используется модель по умолчанию'
105772
106136
  : 'Выбрана модель: ' + (validationModels.find(m => m.id === selectedValidationModel)?.name || selectedValidationModel))),
105773
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_27__["default"], { control: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_28__["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 } })))),
106137
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_21__["default"], { control: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["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 } })))),
105774
106138
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 2, sx: { mb: 2 } },
105775
106139
  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_36__["default"], null), onClick: handleGenerateContent, disabled: generating || loadingContentFromDrive || !openaiApiKey || !generateProduct.trim() || !generateGeo.trim(), sx: { flexGrow: 1 }, size: "large" }, generating ? 'Generating...' : 'Generate Titles & Descriptions'),
105776
106140
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_11__["default"], { direction: "row", spacing: 1, alignItems: "center", sx: { flexGrow: 1 } },
105777
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { title: imageAspectRatio === '1:1'
106141
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["default"], { title: imageAspectRatio === '1:1'
105778
106142
  ? '1:1 — квадрат (1024×1024 px)'
105779
106143
  : imageAspectRatio === '2:3'
105780
106144
  ? '2:3 — портрет (1024×1536 px)'
@@ -105866,8 +106230,20 @@ ${imageData.originalPrompt}
105866
106230
  } }, generatedImagesData.map((imageData) => {
105867
106231
  const imgRatio = imageData.aspectRatio ?? (imageAspectRatio === 'both' ? '1:1' : imageAspectRatio);
105868
106232
  const aspectRatioCss = imgRatio === '2:3' ? '2 / 3' : '1 / 1';
105869
- return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { key: imageData.index, sx: { position: 'relative' } },
105870
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { title: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043A\u0440\u0435\u0430\u0442\u0438\u0432" },
106233
+ const remakeN = imageData.remakeCount ?? 0;
106234
+ const remakeBg = getRemakeHighlightBackground(remakeN, darkMode);
106235
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { key: imageData.index, sx: {
106236
+ position: 'relative',
106237
+ ...(remakeBg
106238
+ ? {
106239
+ p: 1,
106240
+ borderRadius: 1,
106241
+ bgcolor: remakeBg,
106242
+ boxSizing: 'border-box'
106243
+ }
106244
+ : {})
106245
+ } },
106246
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["default"], { title: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043A\u0440\u0435\u0430\u0442\u0438\u0432" },
105871
106247
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
105872
106248
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { size: "small", "aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043A\u0440\u0435\u0430\u0442\u0438\u0432", sx: {
105873
106249
  position: 'absolute',
@@ -106083,7 +106459,7 @@ ${imageData.originalPrompt}
106083
106459
  "/",
106084
106460
  Math.max(generatedTitlesData.length, generatedTextsData.length),
106085
106461
  ")"),
106086
- (generatedTitlesData.length > 0 || generatedTextsData.length > 0) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { title: pairsJsonCopied ? 'Скопировано' : 'Скопировать все пары как JSON-массив' },
106462
+ (generatedTitlesData.length > 0 || generatedTextsData.length > 0) && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["default"], { title: pairsJsonCopied ? 'Скопировано' : 'Скопировать все пары как JSON-массив' },
106087
106463
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { size: "small", "aria-label": "\u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0430\u0440\u044B \u043A\u0430\u043A JSON", onClick: () => void copyGeneratedPairsAsJson(), disabled: generating, sx: {
106088
106464
  opacity: 0.28,
106089
106465
  p: 0.35,
@@ -106146,11 +106522,11 @@ ${imageData.originalPrompt}
106146
106522
  i + 1),
106147
106523
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", color: "text.secondary" }, pairLabel),
106148
106524
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { sx: { flexGrow: 1, minWidth: 8 } }),
106149
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { title: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0430\u0440\u0443" },
106525
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["default"], { title: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0430\u0440\u0443" },
106150
106526
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null,
106151
106527
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_8__["default"], { size: "small", color: "error", "aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0430\u0440\u0443", onClick: () => handleDeleteGeneratedPair(i), disabled: pairGenerating || generating },
106152
106528
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_icons_material__WEBPACK_IMPORTED_MODULE_42__["default"], { fontSize: "small" })))),
106153
- translatingPairs && !pairTranslations[i] ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 13, sx: { color: 'text.disabled', ml: 0.5 } })) : pairTranslations[i] ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_22__["default"], { title: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
106529
+ translatingPairs && !pairTranslations[i] ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_18__["default"], { size: 13, sx: { color: 'text.disabled', ml: 0.5 } })) : pairTranslations[i] ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_24__["default"], { title: react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], null,
106154
106530
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { fontWeight: 700, display: 'block', mb: 0.5 } }, "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A:"),
106155
106531
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { display: 'block', mb: 1 } }, pairTranslations[i].titleRu),
106156
106532
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_6__["default"], { variant: "caption", sx: { fontWeight: 700, display: 'block', mb: 0.5 } }, "\u0422\u0435\u043A\u0441\u0442:"),
@@ -106880,7 +107256,7 @@ function PromptManagerDialog({ open, onClose }) {
106880
107256
  case 'getImageCheckPrompt':
106881
107257
  return (0,_prompts__WEBPACK_IMPORTED_MODULE_36__.getImageCheckPrompt)('${product}', '${geo}', true);
106882
107258
  case 'getValidationPrompt':
106883
- return (0,_prompts__WEBPACK_IMPORTED_MODULE_36__.getValidationPrompt)('${product}', '${geo}', ['keyword1', 'keyword2'], '${approachName}', true);
107259
+ return (0,_prompts__WEBPACK_IMPORTED_MODULE_36__.getValidationPrompt)('${product}', '${geo}', ['keyword1', 'keyword2'], '${approachName}', true, 'Новая по брифу: 29 EUR; старая (2×): 58 EUR — в рантайме подставляется из поля цены; в кастомном промпте плейсхолдер ${priceBrief}');
106884
107260
  case 'getImageGenerationBasePrompt':
106885
107261
  return (0,_prompts__WEBPACK_IMPORTED_MODULE_36__.getImageGenerationBasePrompt)('${generateGeo}', '${generatePrice}', '${generateCurrency}', true);
106886
107262
  case 'getLandingPageSystemPrompt':
@@ -107060,7 +107436,14 @@ function PromptManagerDialog({ open, onClose }) {
107060
107436
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_mui_material__WEBPACK_IMPORTED_MODULE_5__["default"], { type: "number", size: "small", value: count, onChange: (e) => {
107061
107437
  const v = parseInt(e.target.value, 10);
107062
107438
  handleImageApproachCountChange(i, isNaN(v) ? 0 : v);
107063
- }, onBlur: handleImageCountBlur, inputProps: { min: 0, max: 4, step: 1 }, sx: { width: 56, '& .MuiInputBase-input': { textAlign: 'center', py: 0.5 } } })),
107439
+ }, onBlur: handleImageCountBlur, inputProps: {
107440
+ min: 0,
107441
+ max: 4,
107442
+ step: 1,
107443
+ onWheel: (e) => {
107444
+ e.preventDefault();
107445
+ },
107446
+ }, sx: { width: 56, '& .MuiInputBase-input': { textAlign: 'center', py: 0.5 } } })),
107064
107447
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", { style: { padding: '4px 8px', fontWeight: 500, whiteSpace: 'nowrap' } }, approach.name),
107065
107448
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", { style: { padding: '4px 8px', color: '#777' } }, approach.prompt.split('\n')[0])));
107066
107449
  }))))),
@@ -107674,6 +108057,7 @@ __webpack_require__.r(__webpack_exports__);
107674
108057
  /* harmony export */ PAIR_APPROACH_POOL: () => (/* binding */ PAIR_APPROACH_POOL),
107675
108058
  /* harmony export */ TEXTS_APPROACH_POOL: () => (/* binding */ TEXTS_APPROACH_POOL),
107676
108059
  /* harmony export */ TITLES_APPROACH_POOL: () => (/* binding */ TITLES_APPROACH_POOL),
108060
+ /* harmony export */ getCreoApproachExpandedTasks: () => (/* binding */ getCreoApproachExpandedTasks),
107677
108061
  /* harmony export */ getCreoApproaches: () => (/* binding */ getCreoApproaches),
107678
108062
  /* harmony export */ getImageCheckPrompt: () => (/* binding */ getImageCheckPrompt),
107679
108063
  /* harmony export */ getImageGenerationBasePrompt: () => (/* binding */ getImageGenerationBasePrompt),
@@ -108117,7 +108501,9 @@ function getImageCheckPrompt(product, geo, noOverride) {
108117
108501
  /**
108118
108502
  * Промпт для валидации рекламных креативов
108119
108503
  */
108120
- function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
108504
+ function getValidationPrompt(product, geo, keywords, approachName, noOverride,
108505
+ /** Краткий эталон цен из приложения (новая + ожидаемая старая при -50%); пусто — без сверки чисел */
108506
+ priceBrief) {
108121
108507
  if (!noOverride) {
108122
108508
  const override = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_0__.getPromptOverride)('getValidationPrompt');
108123
108509
  if (override?.enabled && override.customPrompt) {
@@ -108126,63 +108512,49 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
108126
108512
  .replace(/\$\{product\}/g, product)
108127
108513
  .replace(/\$\{geo\}/g, geo)
108128
108514
  .replace(/\$\{keywords\}/g, keywords.join(', '))
108129
- .replace(/\$\{approachLine\}/g, approachLine);
108515
+ .replace(/\$\{approachLine\}/g, approachLine)
108516
+ .replace(/\$\{priceBrief\}/g, priceBrief?.trim() || 'В приложении бриф цены не задан — сверяй только визуально две цены и -50%.');
108130
108517
  }
108131
108518
  }
108132
108519
  const approachLine = approachName?.trim() ? `\nПОДХОД: ${approachName.trim()}\n` : '\n';
108133
- // Resolve no-bullets conditions in code — don't leave "if approach = X" for the LLM
108134
- const noBulletsApproachNames = CREO_APPROACHES.filter(a => a.noBullets).map(a => a.name);
108135
- const isNoBullets = noBulletsApproachNames.includes(approachName?.trim() ?? '');
108136
108520
  const isScreenshotReviews = (approachName?.trim() ?? '') === 'Скрин отзывов';
108137
- // ШАГ 0 hint for BULLETS depends on approach
108138
- const step0BulletsHint = isNoBullets
108139
- ? `BULLETS: [] (для данного подхода буллиты запрещены ожидается 0)`
108140
- : `BULLETS: ["...","...","..."] (если буллетов не 3перечисли сколько есть)`;
108141
- // ШАГ 2 completely different rules for no-bullets approaches vs normal
108142
- const step2Bullets = isNoBullets
108143
- ? `ШАГ 2 — BULLETS (критично):
108144
- - Данный подход НЕ допускает буллитов. Буллетов должно быть РОВНО 0.
108145
- - Если на креативе есть ЛЮБЫЕ буллиты — это ОШИБКА.`
108146
- : `ШАГ 2 — BULLETS (критично):
108147
- - РОВНО 3 буллета. Если буллетов не 3 — ОШИБКА.
108148
- - Каждый буллет должен быть читабельным (хорошо читается на телефоне, не микрошрифт)
108149
- - Буллеты: крупные, с иконками/галочками (), на контрастных подложках. Буллеты НЕ на упаковке/банке.
108150
- - Формат бенефита РАЗРЕШЁН: «Чувствуешь себя легче», «Больше энергии», «Без дискомфорта», «Лёгкость движений»это бенефиты, НЕ предложения
108151
- - ПРЕДЛОЖЕНИЕ (ошибка) = полная грамматическая конструкция с подлежащим + сказуемым + дополнением, например «Продукт быстро снижает дискомфорт». Короткие бенефиты — не считать предложениями
108152
- - Буллиты с глаголом во 2-м лице («Чувствуешь...», «Получаешь...») или безличные («Больше энергии», «Без дискомфорта») — НЕ ошибка
108153
- - Буллеты = свойства, характеристики ИЛИ бенефиты. Разрешено: «Reduce disconfortul», «Mai puține treziri», «Efect în 14 zile», цифры соц. доказательства.
108154
- - Буллиты должны быть расположены вертикально (столбиком). Если буллиты идут горизонтально в одну строкуОШИБКА: плохая читаемость на мобиле
108155
- - Цена визуально ОТЛИЧНА от буллитов (цена компактный блок без иконок; буллетыс иконками/галочками). Если цена выглядит как буллет ОШИБКА.`;
108156
- // ШАГ 3 TEXT LIMIT: PVP has no badges allowed, normal allows urgency/trust (без проверок по длине/количеству слов)
108157
- const step3TextLimit = isScreenshotReviews
108158
- ? `ШАГ 3OTHER_TEXT (критично):
108159
- - Для «Скрин отзывов» блок отзывов (аватарки, имена, возраст, 5 звёзд, текст отзывов) — это НЕ ошибка, это основной контент.
108160
- - Допускаются также trust‑печати (0–3 шт). URGENCY не рекомендуется, но не ошибка.
108161
- - На креативе допустимы: HOOK, блок отзывов, цена, скидка, CTA, опционально trust‑печати.`
108162
- : isNoBullets
108163
- ? `ШАГ 3 — OTHER_TEXT (критично):
108164
- - OTHER_TEXT должен быть ПУСТЫМ (0 элементов). Никаких дополнительных бейджей, подписей, ярлыков, trust‑печатей/urgency — НИЧЕГО.
108165
- - Любой дополнительный текст (бейджи/подписи/пояснения/urgency/trust‑печати) ОШИБКА: слишком много текста для punch‑креатива.
108166
- - На креативе допустимы ТОЛЬКО: HOOK, цена, скидка, кнопка CTA.`
108167
- : `ШАГ 3OTHER_TEXT (критично):
108168
- - По умолчанию любой другой рекламный текст (бейджи/подписи/пояснения) запрещён.
108169
- - НО допускаются (не обязательно) дополнительные бейджи ВНЕ упаковки — это НЕ ошибка, если ВСЕ элементы OTHER_TEXT подпадают под разрешённые типы:
108170
- A) URGENCY (0–1 шт): бейдж срочности/дефицита — только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» и подобные — непонятно что означает.
108171
- B) TRUST (0–3 шт, для «Минимализм / Clean Big Text» — макс 1): печати цветные, яркие, очень похожие на печать FDA. Подчёркивают: натуральность состава, премиальность, безопасность. Допустимо: качество, натуральные ингредиенты, экологичность, контроль качества. Печати — размер как минимум как у буллитов, крупные. Даже одна печать — крупная.
108172
- Правила для каждого элемента OTHER_TEXT:
108173
- * строго на языке ${geo} (без английских слов)
108174
- * без цены/скидки/процентов (кроме обязательного поля DISCOUNT), без доменов/ссылок
108175
- * TRUST‑печати — про натуральность/премиальность/безопасность (не про доставку/поддержку)
108176
- Примеры стиля (адаптируй к языку GEO, не требуются):
108177
- - URGENCY: «Ostatnie sztuki», «Tylko dziś», «Koniec dziś» (НЕ «24h» — непонятно)
108178
- - TRUST: «Naturalna formuła», «Wysoka jakość» (для PL); «Ingrediente naturale», «Calitate» (для RO). ЗАПРЕЩЕНО: «NATURAL», «QUALITY», «100% NATURAL» — английские слова.
108179
- - Если OTHER_TEXT содержит что-то вне этих типов, или URGENCY > 1, или TRUST > 3 (для Минимализма — TRUST > 1) — ОШИБКА.`;
108521
+ // Буллеты валидатором не проверяются (ни количество, ни наличие)
108522
+ const step0BulletsHint = 'BULLETS: кратко опиши, что видишь на макете (для справки). Количество и наличие буллетов НЕ валидируются.';
108523
+ const step2Bullets = `ШАГ 2БУЛЛЕТЫ:
108524
+ - Не проверяй и не оценивай количество, отсутствие или наличие буллетов. 0, 1, 2, 3 и большевсё допустимо.
108525
+ - НЕ добавляй в финальный список ошибок ни одного пункта, связанного с буллетами (вертикальность, «ровно 3», иконки, читаемость буллетов и т.п.).`;
108526
+ const priceBriefTrim = priceBrief?.trim() || '';
108527
+ const stepPriceBrief = priceBriefTrim
108528
+ ? `ШАГ P ЦЕНЫ (сначала макет, потом бриф):
108529
+ Эталон из приложения:
108530
+ ${priceBriefTrim}
108531
+
108532
+ 1) ВНУТРЕННЯЯ ЛОГИКА НА МАКЕТЕ (главное):
108533
+ - Должны быть видны две суммы: новая (акционная) и старая (зачёркнутая). Старая новой при скидке -50% (допуск до ~2% из‑за округления; «1 180» и «1180» — одно и то же).
108534
+ - Если отношение старая/новая 2 пределах допуска) это уже доказательство корректной пары -50% на макете. В этом случае ЗАПРЕЩЕНО выводить ОШИБКУ формулировками вроде «выдуманные цены», «неверные суммы», «не соответствуют брифу» даже если цифры в брифе другие или бриф кажется не тем.
108535
+ - Если пара математически согласована с -50% и процент «-50%» где‑либо читаем в зоне оффера (см. ШАГ 4) — не придумывай расхождение с брифом из‑за символа валюты (€ / EUR / MXN и т.д.) или пробелов.
108536
+
108537
+ 2) СВЕРКА С БРИФОМ (только когда сопоставимо):
108538
+ - Сравнивай число НОВОЙ цены с брифом ТОЛЬКО если валюта на макете явно та же, что в брифе (EUR↔EUR, MXN↔MXN и т.д.; символ € и код EUR одна валюта). Допускается разное оформление числа (пробелы тысяч, запятая/точка).
108539
+ - Если валюта на креативе другая, чем в брифе, или не уверен в коде валюты не требуй совпадения цифр с брифом: достаточно пункта (1).
108540
+ - Ошибку по ценам ставь только если: (а) на макете нет двух цен / нет согласованности -50%, ИЛИ (б) валюта однозначно совпадает с брифом, а число новой цены явно другое (не в пределах округления).
108541
+
108542
+ 3) Цена не должна быть оформлена как строка буллета с галочкой отдельный блок.`
108543
+ : `ШАГ P ЦЕНЫ (бриф числом не задан):
108544
+ - На макете должны быть ДВЕ цены, старая зачёркнута толсто, новая выразительна; пара должна визуально соответствовать -50% (старая ≈ 2× новой). Конкретные суммы с приложением не сверяй.`;
108545
+ const step3OtherText = isScreenshotReviews
108546
+ ? `ШАГ 3 — Подход «Скрин отзывов»:
108547
+ - Блок отзывов (аватары, имена, звёзды, текст) это нормальный контент креатива, не считай его лишним OTHER_TEXT.`
108548
+ : `ШАГ 3 Доп. текст, бейджи, печати:
108549
+ - Не валидируй объём текста и не отклоняй креатив за «слишком много элементов», punch vs не punch, количество плашек или пустоту OTHER_TEXT.
108550
+ - Не требуй отсутствия trust/urgency/подписей для любого подхода это не ошибка.
108551
+ - Язык рекламного текста (включая бейджи) по-прежнему ШАГ 5. Неясные срочности вроде «24h» — по-прежнему ШАГ 6.`;
108180
108552
  return `Ты — СТРОГИЙ валидатор рекламных креативов (1:1). Не улучшай и не предлагай идеи — только проверка и вердикт.
108181
108553
  Продукт: ${product}. GEO/язык: ${geo}.${approachLine}
108182
108554
 
108183
108555
  ВАЖНО:
108184
- - Проверяй язык ТОЛЬКО для рекламного текста на макете (заголовок/буллеты/CTA/цена/скидка/разрешённые бейджи из OTHER_TEXT). Текст на самой упаковке/этикетке игнорируй (включая название/бренд вроде "${product}").
108185
- - Если сомневаешься, где расположен текст (на упаковке или вне её) — считай, что он НА упаковке и НЕ записывай его в OTHER_TEXT.
108556
+ - Проверяй язык ТОЛЬКО для рекламного текста на макете (заголовок/CTA/цена/скидка/разрешённые бейджи из OTHER_TEXT; если есть буллеты — и их текст). Текст на самой упаковке/этикетке игнорируй (включая название/бренд вроде "${product}").
108557
+ - Если сомневаешься, мелкий текст на банке — этикетка или нет: для языка считай этикеткой и не валидируй. Исключение: **нижний оффер-блок** макета (две цены, зачёркивание, «-50%», кнопка CTA в одной полосе/ряду под картинкой продукта) — это всегда рекламный слой **вне упаковки**, даже рядом с фото банки; цены и скидку оттуда учитывай в PRICE/DISCOUNT и не считай «на упаковке».
108186
108558
  - Если текст плохо читается/микрошрифт — это ошибка.
108187
108559
 
108188
108560
  ШАГ 0 — Сначала выпиши распознанный текст (как ты его видишь на макете):
@@ -108192,9 +108564,7 @@ PRICE: "..." (если нет — "нет")
108192
108564
  DISCOUNT: "..." (если нет — "нет")
108193
108565
  CTA: "..." (если нет — "нет")
108194
108566
  OTHER_TEXT: ["..."] (любой другой рекламный текст на макете ВНЕ упаковки и ВНЕ элементов выше; название/бренд на упаковке НЕ включай)
108195
- (если можешь, для ясности — не обязательно):
108196
- URGENCY_BADGE: "..." (бейдж срочности, если есть; «24h» — ОШИБКА, непонятно что означает)
108197
- TRUST_BADGES: ["..."] (печати цветные, яркие, в стиле FDA; про натуральность/премиальность/безопасность; размер как у буллитов — крупные, если есть)
108567
+ (опционально, для ясности): URGENCY_BADGE, TRUST_BADGES перечисли, если есть
108198
108568
 
108199
108569
  ШАГ 1 — HOOK/HEADLINE (критично):
108200
108570
  - Должен быть ВЫШЕ всех элементов и читабелен (хорошо читается на телефоне)
@@ -108207,35 +108577,38 @@ TRUST_BADGES: ["..."] (печати цветные, яркие, в стиле FD
108207
108577
 
108208
108578
  ${step2Bullets}
108209
108579
 
108580
+ ${stepPriceBrief}
108581
+
108210
108582
  ШАГ 2.5 — CLAIMS CHECK:
108211
108583
  - Клеймы по результату (жёсткие/абсолютные или мягкие) — НЕ проверяются и НЕ запрещаются. Не блокируй за формулировки обещаний результата.
108212
108584
 
108213
- ${step3TextLimit}
108585
+ ${step3OtherText}
108214
108586
 
108215
- ШАГ 4 — CTA > PRICE (критично):
108587
+ ШАГ 4 — CTA и визуал цены/скидки (критично):
108216
108588
  - CTA: на языке ${geo}, хорошо заметная кнопка (позиция НЕ важна: можно вправо/влево/по центру). Главное — CTA читабельна и выглядит как кнопка.
108217
- - Цена: ОБЯЗАТЕЛЬНО ДВЕ старая (до скидки) + новая. Если только одна цена ОШИБКА. Старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Тонкое/незаметное зачёркивание — ОШИБКА.
108218
- - Скидка ОБЯЗАТЕЛЬНА: «-50%» отдельным видимым бейджем (процент должен быть явно читаем, не только в цене). Ярко, заметно. Строго НЕ на упаковке/банке.
108589
+ - Блок цен: две суммы, старая зачёркнута ТОЛСТОЙ линией, новая выразительно (детали и сверка чисел с брифом в ШАГ P). Если только одна цена — ОШИБКА.
108590
+ - Скидка ОБЯЗАТЕЛЬНА: читаемое «-50%». **Отдельный видимый бейдж** = любая заметная плашка/круг/прямоугольник с текстом «-50в **той же зоне оффера**, что и цены (сбоку от сумм, над/под ценовой строкой, между ценой и CTA) это ОК, не требуй отдельного «далёкого» элемента. «НЕ на упаковке» означает только: не напечатано на этикетке/банке товара; плашка в нижней рекламной полосе макета **не** считается упаковкой.
108591
+ - Если в ШАГ 0 в DISCOUNT ты указал «-50%» (или эквивалент) — ЗАПРЕЩЕНО ставить ошибку «скидка не отображается» / «нет отдельного бейджа».
108219
108592
  - Не считать ошибкой, если скидка/цена конкурируют с CTA по акценту — это допустимо, пока CTA остаётся хорошо заметной и читабельной.
108220
108593
  - Если скидка указана и она не равна -50% — ошибка.
108221
108594
 
108222
108595
  ШАГ 5 — ЯЗЫК (критично):
108223
- - В рекламном тексте (HOOK/HEADLINE/BULLETS/CTA/PRICE/DISCOUNT/OTHER_TEXT) НЕТ английских слов. Если есть — ошибка.
108596
+ - В рекламном тексте (HOOK/HEADLINE, текст на буллетах если есть, CTA/PRICE/DISCOUNT/OTHER_TEXT) НЕТ английских слов. Если есть — ошибка.
108597
+ - ЗАПРЕЩЁННЫЕ служебные подписи на макете (частая ошибка модели): слова HOOK, CTA, CAPS, BULLET, HEADLINE, PRICE, DISCOUNT как видимый текст на плашках или кнопке — ОШИБКА (это метки из брифа, не для читателя).
108224
108598
  - Все слова соответствуют GEO/языку ${geo}. Если смешение языков — ошибка.
108225
108599
 
108226
108600
  ШАГ 6 — КОМПОЗИЦИЯ:
108227
108601
  - Если креатив без человека (lifestyle/clean), проверь наличие контекста использования (кухня, стол, тумбочка, и т.д.). Продукт на полностью пустом/белом фоне без контекста — РЕКОМЕНДАЦИЯ к улучшению (не критичная ошибка, но слабый визуал)
108228
- - КОНТРАСТ ПЛАШЕК: если хотя бы одна текстовая плашка (HOOK, буллеты, CTA или цена) визуально сливается с фоном и текст плохо читается — это ОШИБКА.
108229
- - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание ОШИБКА. Цена не должна выглядеть как буллет.
108230
- - TRUST‑печати: если печати мелкие или текст нечитабелен на телефоне — ОШИБКА (размер как минимум как у буллитов).
108231
- - СКИДКА «-50%»: ОБЯЗАТЕЛЬНО отдельным видимым бейджем (процент явно читаем). Если скидка не отображается или только в цене без отдельного «-50%» ОШИБКА. Ярко, заметно, НЕ на упаковке.
108232
- - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА.
108602
+ - КОНТРАСТ ПЛАШЕК: если HOOK, CTA или блок цен визуально сливается с фоном и текст плохо читается — это ОШИБКА. (Контраст буллетов не проверяй.)
108603
+ - Две цены и зачёркивание старой см. ШАГ 4 и ШАГ P. Цена не должна быть оформлена как элемент списка с галочкой в стиле бенефитов.
108604
+ - TRUST‑печати: если печати мелкие или текст нечитабелен на телефоне — ОШИБКА (должны быть крупными).
108605
+ - СКИДКА «-50%»: как в ШАГ 4 плашка рядом с ценами в оффер-блоке засчитывается; если DISCOUNT в ШАГ 0 заполненне дублируй ошибку про бейдж.
108233
108606
  - Бейджи срочности: «24h» и подобные неясные формулировки — ОШИБКА (непонятно что означает). Ясные («только сегодня», «последние штуки») — ок.
108234
108607
 
108235
108608
  ФИНАЛ:
108236
108609
  Выведи строго:
108237
108610
  СТАТУС: OK / НУЖНА ПЕРЕСБОРКА
108238
- Затем список ошибок, каждая строка начинается с "ОШИБКА:" (кратко, по делу). Если ошибок нет — напиши "ОШИБКА: нет".
108611
+ Затем список ошибок, каждая строка начинается с "ОШИБКА:" (кратко, по делу). Одну и ту же проблему не повторяй — не больше одной строки на один тип нарушения. Если СТАТУС: НУЖНА ПЕРЕСБОРКА — ОБЯЗАТЕЛЬНО минимум одна конкретная строка "ОШИБКА: ..." (что именно не так); нельзя писать только "ОШИБКА: нет". Если ошибок нет — СТАТУС: OK и "ОШИБКА: нет".
108239
108612
  Затем список рекомендаций (если есть), каждая строка начинается с "РЕКОМЕНДАЦИЯ:" (кратко, по делу). Если рекомендаций нет — напиши "РЕКОМЕНДАЦИЯ: нет".
108240
108613
  Не блокируй за клеймы по результату или формулировки обещаний.`;
108241
108614
  }
@@ -108271,6 +108644,9 @@ function getImageGenerationBasePrompt(generateGeo, generatePrice, generateCurren
108271
108644
  ВАЖНО: изображение должно быть ${ratioShape} — строго соблюдай пропорции холста.
108272
108645
  Язык текста: ${generateGeo}.
108273
108646
 
108647
+ 🚫 СЛУЖЕБНЫЕ СЛОВА НЕ РИСОВАТЬ НА КАРТИНКЕ (КРИТИЧНО):
108648
+ Слова HOOK, CTA, CAPS, BULLET, HEADLINE, PRICE, DISCOUNT, BULLETS, LAYER и любые похожие английские метки из этой инструкции (в т.ч. thumb‑stop) — только для тебя; на макете их НЕТ ни на кнопке, ни на плашках, ни мелким текстом. Кнопка: только реальный призыв на языке ${generateGeo} (1–2 слова), без префикса «CTA». Заголовок: только живой текст оффера, без слова HOOK. Заголовок делай прописными буквами на языке ${generateGeo}, но не пиши на изображении слово CAPS и не подписывай блоки ярлыками.
108649
+
108274
108650
  🚨 КРИТИЧНО — РЕЛЕВАНТНОСТЬ ПРОДУКТУ (читай ЭТО ПЕРВЫМ):
108275
108651
  - Название продукта передано в начале промпта (строка «🏷️ ПРОДУКТ: ...»). Прочитай его ПРЯМО СЕЙЧАС и определи категорию.
108276
108652
  - Категории и их проблемы: суставы/колени → боль в суставах, скованность, тугоподвижность; пищеварение → дискомфорт после еды, тяжесть; сон → бессонница, усталость; похудение → лишний вес; простата → частые позывы, дискомфорт.
@@ -108301,7 +108677,7 @@ HOOK / HEADLINE (строгое правило):
108301
108677
  - 1–4 строки; до 12 слов. Без переноса внутри слова
108302
108678
  - ОБЯЗАТЕЛЬНО содержит ключевое слово проблемы ЯВНО. Примеры по категориям: простата → простатит, простата; похудение → лишний вес, похудение; суставы → боль в суставах, колени, скованность; пищеварение → дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Без ключевого слова — ОШИБКА
108303
108679
  - HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). Самый крупный текстовый элемент на креативе
108304
- - HOOK должен быть ВИЗУАЛЬНО “тяжёлым”: ОЧЕНЬ крупный, ТОЛСТЫЙ/жирный шрифт, ВСЕ БУКВЫ ПРОПИСНЫЕ (CAPS), на яркой контрастной плашке/подложке. Это верхний главный “thumb‑stop” элемент
108680
+ - HOOK должен быть ВИЗУАЛЬНО “тяжёлым”: ОЧЕНЬ крупный, ТОЛСТЫЙ/жирный шрифт, весь текст заголовка ПРОПИСНЫМИ буквами на языке ${generateGeo}, на яркой контрастной плашке/подложке. Это верхний главный элемент, цепляющий внимание в ленте
108305
108681
  - Можно (не обязательно) включить СРОК прямо в HOOK (например «7 dni», «14 dni») как триггер
108306
108682
  - Заголовок должен быть РЕЗКИМ и призывным: короткие рубленые фразы, вопросы и предупреждения допустимы. Можно использовать обращение на «ты» (в языке GEO: «Masz…», «Twój…») и вопросительный знак
108307
108683
  - заголовок НЕ должен быть абстрактным слоганом/лозунгом или метафорой без конкретной боли. Допускается «thumb‑stop» стиль (вызов/вопрос/предупреждение), если это конкретно и релевантно категории
@@ -108352,7 +108728,7 @@ CTA > PRICE:
108352
108728
  ❗ ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → цена и «-50%» могут быть более крупными и заметными, но CTA всё равно должен оставаться очень заметным и выглядеть как кнопка (не теряется на фоне)
108353
108729
 
108354
108730
  ПОКАЗЫВАЙ ТОЛЬКО ЭТИ ТЕКСТОВЫЕ ЭЛЕМЕНТЫ:
108355
- - HOOK (1–4 строки; выше всех элементов; самый крупный; CAPS; жирный; на яркой контрастной подложке; ключевое слово проблемы явно)
108731
+ - HOOK (1–4 строки; выше всех элементов; самый крупный; прописные буквы; жирный; на яркой контрастной подложке; ключевое слово проблемы явно)
108356
108732
  - 3 буллета (крупные, с иконками/галочками ✓, на контрастных подложках, НЕ на банке/упаковке)
108357
108733
  - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (2×${generatePrice} ${generateCurrency}) зачёркнута ТОЛСТОЙ контрастной линией + новая ${generatePrice} ${generateCurrency} выразительно. Одна цена = ОШИБКА. Тонкая линия зачёркивания = ОШИБКА. Без слов. Не на упаковке.
108358
108734
  - Скидка: «-50%» ОБЯЗАТЕЛЬНО отдельным видимым бейджем (не только в цене!). Процент скидки должен быть явно читаем. Ярко, заметно, строго НЕ на банке/упаковке, визуально слабее CTA
@@ -108388,11 +108764,11 @@ ANTI-TEMPLATE DIVERSITY (КРИТИЧНО):
108388
108764
  * 🎯 ПОДХОД: Визуализация проблемы → Инфографика/схема: слева проблема (иконка/схема зоны тела, релевантной продукту), справа решение + продукт. HOOK сверху по центру, BULLETS справа или снизу (вертикально), CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне). Красный акцент только на проблеме.
108389
108765
  * 🎯 ПОДХОД: Power / Сила решения → БЕЗ человека. ДИНАМИЧНЫЙ комикс‑кадр, диагональная композиция. Продукт как “герой” + power‑иконки (щит/молния/бёрст). HOOK сверху слева на яркой плашке, BULLETS слева ниже, CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
108390
108766
  * 🎯 ПОДХОД: Минимализм / Clean Big Text → Белый/градиентный фон, минимум элементов. ОГРОМНЫЙ HOOK занимает верх/центр, продукт крупно (центр/право), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Trust‑печати (если есть) — размер как минимум как у буллитов, крупные и читабельные.
108391
- * 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK 1–4 строки, до 12 слов, огромный CAPS, выше всех. БЕЗ буллитов. PRICE + «-50%» крупно и заметно. CTA контрастной кнопкой
108392
- * 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — 1–4 строки, до 12 слов, крупный, грубый bold/рукописный, CAPS, выше всех. Крупные надписи цены/скидки вокруг продукта. CTAплоская кнопка без градиентов. БЕЗ буллитов, бейджей, теней, иконок.
108767
+ * 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK 1–4 строки, до 12 слов, огромный прописной текст, выше всех. БЕЗ буллитов. Две цены + «-50%» крупно и заметно. Кнопка призыва контрастная (только текст на языке ${generateGeo}, без слова CTA)
108768
+ * 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — 1–4 строки, до 12 слов, крупный, грубый bold/рукописный, прописные буквы, выше всех. Крупные надписи цены/скидки вокруг продукта. Кнопка призыва плоская, без градиентов (только короткая фраза на языке ${generateGeo}). БЕЗ буллитов, бейджей, теней, иконок.
108393
108769
 
108394
108770
  AUTO-CHECK (перед финалом):
108395
- - HOOK: 1–4 строки, до 12 слов, CAPS, жирный на контрастной плашке, выше всех элементов, ключевое слово проблемы явно, язык ${generateGeo} без английских слов
108771
+ - HOOK: 1–4 строки, до 12 слов, прописные буквы, жирный на контрастной плашке, выше всех элементов, ключевое слово проблемы явно, язык ${generateGeo} без английских слов (включая метки HOOK/CTA/CAPS)
108396
108772
  - Буллеты: по умолчанию ровно 3, каждый <= 4 слов, без запятых, не предложения. ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллетов 0 (запрещены)
108397
108773
  - Нет лишнего текста кроме: HOOK, буллетов, цены, скидки, кнопки (+ опционально 1 бейдж срочности + опционально 1–3 trust‑печати)
108398
108774
  - Есть скидка «-50%» (вне упаковки) и она слабее CTA
@@ -108404,10 +108780,10 @@ AUTO-CHECK (перед финалом):
108404
108780
  ВАЖНО: Твой ответ — это ИЗОБРАЖЕНИЕ, не текст. Не пиши план, не перечисляй элементы текстом, не рассуждай, не вызывай tools. Сразу генерируй визуальный креатив как картинку.`;
108405
108781
  }
108406
108782
  /**
108407
- * Получить подходы с учетом количества и оверрайдов.
108408
- * Каждый подход повторяется N раз (0–4), где N = imageApproachCounts[i].
108783
+ * Развёрнутый список задач крео: каждый подход повторяется N раз (0–4), N = imageApproachCounts[i].
108784
+ * poolIndex индекс строки в CREO_APPROACHES (0-based); в UI таблице это номер строки poolIndex + 1 (1–10).
108409
108785
  */
108410
- function getCreoApproaches() {
108786
+ function getCreoApproachExpandedTasks() {
108411
108787
  const counts = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_0__.getImageApproachCounts)();
108412
108788
  const result = [];
108413
108789
  for (let i = 0; i < CREO_APPROACHES.length && i < counts.length; i++) {
@@ -108427,46 +108803,50 @@ function getCreoApproaches() {
108427
108803
  _debugLog(`✅ Using CUSTOM approach: ${approach.name}`, `hasPrompt=${!!override.customPrompt}`, `hasHeadline=${!!override.customHeadlineAngle}`, `hasBullets=${!!override.customBulletsFocus}`);
108428
108804
  }
108429
108805
  for (let k = 0; k < counts[i]; k++) {
108430
- result.push(resolved);
108806
+ result.push({ approach: resolved, poolIndex: i });
108431
108807
  }
108432
108808
  }
108433
108809
  return result;
108434
108810
  }
108811
+ /** Плоский список подходов (без индекса строки) — для обратной совместимости. */
108812
+ function getCreoApproaches() {
108813
+ return getCreoApproachExpandedTasks().map(t => t.approach);
108814
+ }
108435
108815
  const CREO_APPROACHES = [
108436
108816
  {
108437
108817
  name: 'Эксперт / Авторитет',
108438
108818
  prompt: `ЭКСПЕРТ / АВТОРИТЕТ (без человека): стиль “рекомендация эксперта”, но БЕЗ человека. Профессиональный контекст — аптечные полки на фоне / медицинский планшет / стетоскоп / рецептурный блокнот как реквизит рядом с продуктом. Продукт в центре как “рекомендованное решение”. Чистый профессиональный фон, высокий контраст. Trust‑печати (1–3 шт): цветные, яркие, очень похожие на печать FDA; подчёркивают натуральность состава, премиальность, безопасность. Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная.`,
108439
- headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108819
+ headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108440
108820
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор для этого подхода — не повторяй комбинации из других креативов.`
108441
108821
  },
108442
108822
  {
108443
108823
  name: 'Lifestyle / Момент приёма',
108444
108824
  prompt: `LIFESTYLE / МОМЕНТ ПРИЁМА: продукт в контексте использования (кухня/стол/спальня/тумбочка). Без человека, но с "историей" (стакан воды, чай, тарелка, будильник/часы/календарь). Срочность: реквизит (часы/таймер‑иконка) и/или опциональный urgency‑бейдж (1–3 слова) в углу кадра — только ясные формулировки («только сегодня», «последние штуки»), ЗАПРЕЩЕНО «24h». Высокий контраст, яркий акцент на продукте. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ВАЖНО: буллиты располагаются вертикально и всегда читабельны на экране телефона без зума — даже если уходят вбок, минимальный размер шрифта буллитов не уменьшается. Если буллиты не вмещаются сбоку с нужным шрифтом — переставь их вниз.`,
108445
- headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108825
+ headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108446
108826
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (срок + цифра + действие/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108447
108827
  },
108448
108828
  {
108449
108829
  name: 'Эмоция / Портрет',
108450
108830
  prompt: `ЭМОЦИЯ / ПОРТРЕТ: один из двух подходов с человеком в серии. Используй максимально: ОЧЕНЬ крупный план лица (thumb‑stop), прямой взгляд в камеру, сильная эмоция облегчения/надежды (без широкой стоковой улыбки). Возраст человека подбери под категорию и ЦА продукта: по умолчанию 50–60, но для категорий с более молодой аудиторией (например похудение/фитнес) допускается 35–55. Продукт в руке на уровне лица или рядом, хорошо виден. Свет "тень → свет" на лице допустим. Минимум отвлекающих деталей, высокий контраст. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ИЕРАРХИЯ: лицо — главный и доминирующий визуальный элемент (верхние 2/3 кадра). Все текстовые блоки (HOOK исключение — сверху) уходят в нижнюю треть. Буллиты, цена, CTA НЕ перекрывают лицо и НЕ конкурируют с ним по визуальному весу — они заметно меньше и ниже.`,
108451
- headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно. Тон максимально личный и прямой.`,
108831
+ headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение». 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно. Тон максимально личный и прямой.`,
108452
108832
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (качество жизни + действие + срок/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108453
108833
  },
108454
108834
  {
108455
108835
  name: 'Визуализация проблемы',
108456
108836
  prompt: `ВИЗУАЛИЗАЦИЯ ПРОБЛЕМЫ (без человека): схема/иконка проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав/позвоночник; для пищеварения — желудок; для простаты — силуэт мужчины) + мягкий красный акцент на этой зоне (не шок‑контент, без графики). Рядом продукт как решение. Можно добавить простую стрелку/переход “проблема → облегчение” как графику (без лишнего текста). Чистый фон, высокая читабельность. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека.`,
108457
- headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно, вопросительный знак допустим.`,
108837
+ headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно, вопросительный знак допустим.`,
108458
108838
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108459
108839
  },
108460
108840
  {
108461
108841
  name: 'Power / Сила решения',
108462
108842
  prompt: `POWER / СИЛА РЕШЕНИЯ (без человека): метафоры силы и победы над проблемой: щит, молния, энергия, взрыв‑бёрст, мощные стикеры/иконки. Продукт как “герой” в центре, высокая энергия, контрастные агрессивные цвета. Можно комикс/anti‑design подачу, но всё должно быть читабельно. Без лишнего текста. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека. Только продукт + power‑иконки.`,
108463
- headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108843
+ headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108464
108844
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + скорость + цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108465
108845
  },
108466
108846
  {
108467
108847
  name: 'Минимализм / Clean Big Text',
108468
- prompt: `МИНИМАЛИЗМ / CLEAN BIG TEXT (без человека): белый или мягкий градиентный фон, премиальное ощущение. ОГРОМНЫЙ HOOK как главный элемент (CAPS, жирный, на контрастной плашке). Продукт крупно, минимум реквизита, минимум шума. Тени/премиальные материалы допустимы, но без лишнего текста. БЕЗ человека. Urgency и trust‑печати в этом подходе НЕ рекомендуются — они нарушают чистоту и ощущение премиальности. Добавляй печати только если это критически необходимо, не более 1, но крупную — размер как у буллитов.`,
108469
- headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108848
+ prompt: `МИНИМАЛИЗМ / CLEAN BIG TEXT (без человека): белый или мягкий градиентный фон, премиальное ощущение. ОГРОМНЫЙ HOOK как главный элемент (прописные буквы, жирный, на контрастной плашке). Продукт крупно, минимум реквизита, минимум шума. Тени/премиальные материалы допустимы, но без лишнего текста. БЕЗ человека. Urgency и trust‑печати в этом подходе НЕ рекомендуются — они нарушают чистоту и ощущение премиальности. Добавляй печати только если это критически необходимо, не более 1, но крупную — размер как у буллитов.`,
108849
+ headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108470
108850
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + формула/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108471
108851
  },
108472
108852
  {
@@ -108476,14 +108856,14 @@ const CREO_APPROACHES = [
108476
108856
  - Фон: яркий сплошной цвет (красный/оранжевый) или агрессивный градиент
108477
108857
  - Центр: КРУПНАЯ иконка/схема проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав; для простаты — силуэт мужчины; для пищеварения — желудок), с красным свечением
108478
108858
  - Продукт: крупно рядом
108479
- - HOOK: 1–4 строки, до 12 слов, огромный, CAPS, ключевое слово проблемы явно
108859
+ - HOOK: 1–4 строки, до 12 слов, огромный, прописные буквы, ключевое слово проблемы явно
108480
108860
  - БЕЗ буллитов
108481
108861
  - Цена + скидка: крупно, заметно
108482
108862
  - CTA: контрастная яркая кнопка
108483
108863
 
108484
108864
  Цель: считывание за 1 секунду. Минимум текста, максимум визуального удара.
108485
108865
  БЕЗ человека. Никаких trust‑печатей/urgency бейджей — только HOOK + PRICE + -50% + CTA.`,
108486
- headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108866
+ headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108487
108867
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK и крупных надписях.`
108488
108868
  },
108489
108869
  {
@@ -108498,7 +108878,7 @@ const CREO_APPROACHES = [
108498
108878
  - Крупные текстовые надписи рядом с продуктом: цена, скидка; допустимо одно короткое слово-восклицание («РАБОТАЕТ!», «ПРОВЕРЕНО!») на языке GEO
108499
108879
  - Кнопка CTA: плоская, без градиентов, контрастный сплошной цвет
108500
108880
  - НИКАКИХ буллитов, trust‑печатей, urgency‑бейджей, иконок — только продукт и текст`,
108501
- headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
108881
+ headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
108502
108882
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Всё — в HOOK и крупных надписях вокруг продукта (цена, скидка, одно слово-восклицание).`
108503
108883
  },
108504
108884
  {
@@ -108510,7 +108890,7 @@ const CREO_APPROACHES = [
108510
108890
  - Фон: профессиональный (кабинет, аптечные полки, медицинский контекст). Высокий контраст, читабельность.
108511
108891
  - Опционально: 1–3 trust‑печати по низу (цветные, яркие, в стиле FDA). Размер как минимум как у буллитов.
108512
108892
  - ИЕРАРХИЯ: врач + продукт — главные элементы. HOOK сверху, BULLETS сбоку или снизу, CTA и PRICE/DISCOUNT в нижнем блоке.`,
108513
- headlineAngle: `HOOK: угол «врач/специалист рекомендует» или «эксперты знают». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108893
+ headlineAngle: `HOOK: угол «врач/специалист рекомендует» или «эксперты знают». 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108514
108894
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108515
108895
  },
108516
108896
  {
@@ -108519,11 +108899,11 @@ const CREO_APPROACHES = [
108519
108899
  prompt: `СКРИН ОТЗЫВОВ: имитация реалистичного скриншота с положительными благодарными отзывами и оценками 5 звёзд.
108520
108900
  - Визуал: реалистичный стиль — как скриншот приложения/сайта отзывов (App Store, Google Play, или страница отзывов). Высокое качество, читабельный текст.
108521
108901
  - Отзывы: 2–4 отзыва с аватарками, именами и возрастом (формат «Имя, 45 лет»), ОБЯЗАТЕЛЬНО 5 звёзд (★★★★★) в каждом, короткий текст благодарности. Текст отзывов СТРОГО на языке GEO — никакого английского. Релевантен категории продукта.
108522
- - HOOK ОБЯЗАТЕЛЕН: крупно, выше блока отзывов или поверх, CAPS, на яркой подложке. Ключевое слово проблемы явно.
108902
+ - HOOK ОБЯЗАТЕЛЕН: крупно, выше блока отзывов или поверх, прописные буквы, на яркой подложке. Ключевое слово проблемы явно.
108523
108903
  - Продукт: виден в кадре (рядом со скрином или интегрирован в композицию).
108524
108904
  - Цена, скидка «-50%», CTA — в нижней части. БЕЗ буллитов — отзывы заменяют их.
108525
108905
  - Стиль: не мультяшный, не комикс — максимально реалистичный скрин.`,
108526
- headlineAngle: `HOOK: угол «тысячи довольны» / «9 из 10 рекомендуют» / «реальный результат». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108906
+ headlineAngle: `HOOK: угол «тысячи довольны» / «9 из 10 рекомендуют» / «реальный результат». 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108527
108907
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK, отзывах (с 5 звёздами), цене и CTA.`
108528
108908
  }
108529
108909
  ];