🔒

Бесплатные анализы закончились

Вы использовали все 3 бесплатных анализа. Оформите подписку чтобы продолжить — без ограничений.

1 990 ₽
в месяц
Стандарт
4 990 ₽
3 месяца
Выгодно −17%
9 990 ₽
в год
Максимум −58%
Закрыть
Помогаем вашему бизнесу развиваться

Анализ тендера
за 60 секунд

Загрузите документацию — получите полный разбор закупки: условия, риски, все тонкости и нюансы. Никакой ручной работы.

✦ Передовые технологии ИИ анализа документов — точно, быстро, надёжно

44-ФЗ
Госзакупки
223-ФЗ
Корпоративные
Коммерческие
Закупки
ПП РФ №615
Капремонт МКД
Документы не сохраняются на серверах
Анализ за 60 секунд
Без установки — работает в браузере
Как это работает
01

Загрузите документы

PDF, Word, HTML — любые файлы тендерной документации с zakupki.gov.ru или от поставщика

02

Добавьте вопрос

Уточните если есть особые моменты — например проверить счёт поставщика или возможность аналогов

03

ИИ анализирует

Читает все документы, сравнивает позиции, выявляет риски и формирует структурированный отчёт

04

Получите отчёт

Все условия, риски, сроки и нюансы — в одном понятном документе за 60 секунд

01
Шаг первый
Что будет проанализировано
10+ параметров — риски, условия, сроки, оплаты и т.д.
⚡ 16 разделов
Что будет проанализировано
Анализ проводится по всем параметрам автоматически — ничего выбирать не нужно
Общая информация
НМЦК, заказчик, состав закупки, ключевые параметры
Риски и ловушки
Скрытые условия, опасные пункты, типичные ловушки
Условия доставки
Кто везёт, кто разгружает, за чей счёт, адрес объекта
Сроки поставки
Реалистичность сроков, риск срыва, критичные даты
Условия оплаты
Аванс, постоплата, срок перечисления, риски задержки
Обеспечение
Размер, способ внесения, условия возврата
Комиссия площадки
Расходы на ЭТП, скрытые платежи при победе
Штрафы и пени
Пени за просрочку, штрафы за нарушения, удержания
Национальный режим
ПП 719, 878, 1875 — требования к стране происхождения, требования РПП (ГИСП)
Требования к заявке
Документы, сертификаты, декларации для участия
Аналог / эквивалент
Разрешена ли замена на аналог, как правильно оформить
Конкурентная среда
Число участников, типичные победители, шансы на победу
Риск РНП
Условия попадания в реестр недобросовестных поставщиков
Досье на заказчика
Арбитраж, платёжная дисциплина, надёжность МУП/ГУП
Чек-лист участника
Пошаговый план до заявки, для заявки и после победы
Итоговое резюме
Участвовать / нет, оценка, мин. безопасная цена, рекомендация
02
Шаг второй
Загрузка документов
Документы тендера
Загрузите всю документацию: извещение, ТЗ, проект контракта — можно всё сразу

Нажмите или перетащите файлы сюда

PDF, Word, HTML, Excel — можно несколько сразу

Максимальный размер файла: 10 МБ
03
Финальный шаг
Запустить полный анализ

Анализирую документацию...

📊 Результаты анализа

Здесь появится результат анализа после загрузки документов и нажатия кнопки "Проанализировать тендер".
Дополнительные инструменты
Работают независимо —
используйте когда нужно
A
Сравнение и аналоги
Сравнение ТЗ и счёта · Подбор аналогов
Построчное сравнение позиций счёта с ТЗ в виде PDF. Подбор аналогов по ТЗ.
Сравнение ТЗ и счёта поставщика
Загрузите ТЗ и счёт поставщика — ИИ сравнит позиции построчно и сформирует отчёт. Поддерживаются PDF, Word, Excel, изображения (JPG, PNG) и другие форматы.
Техническое задание
Перетащите файл или нажмите
PDF, Word, Excel, JPG, PNG и др.
Счёт поставщика
Перетащите файл или нажмите
PDF, Word, Excel, JPG, PNG и др.
Подбор аналогов по ТЗ
Вставьте требования из технического задания или описание товара — ИИ найдёт аналоги от независимых производителей и сравнит характеристики с ТЗ
💡 Совет: вставьте сюда характеристики прямо из ТЗ — чем точнее описание, тем точнее сравнение
Перетащите файл ТЗ сюда или нажмите — текст подставится автоматически PDF, Word, Excel, TXT
Подбор материала по ТЗ
Вставьте характеристики из ТЗ без привязки к бренду — ИИ через веб-поиск найдёт конкретные реальные материалы с указанием производителя и марки
💡 Совет: если в ТЗ нет конкретного бренда — просто перечислите параметры, ИИ подберёт подходящий материал
Перетащите файл ТЗ сюда или нажмите — текст подставится автоматически PDF, Word, Excel, TXT
B
Проверка контрагентов
Заказчики, риски, юридика, тендеры
Анализ поставщиков заказчика · Риски ФНС · Юридическая проверка договора
1 2 3 ! −92%
Анализ поставщиков заказчика
Введите ИНН заказчика — ИИ проверит историю закупок, выявит номинальных победителей и оценит реальные шансы на участие
! ФНС 12 крит.
Предпринимательские риски по методике ФНС
Оценка контрагента по 12 критериям ФНС (Приказ №ММ-3-06/333@) — налоговая нагрузка, убытки, зарплата, долги, арбитраж
ДОГОВОР
Юридическая проверка договора
Загрузите файл договора или вставьте текст — ИИ проверит опасные пункты, скрытые обязательства, штрафы и юридические риски

Прикрепить договор

PDF или Word (.docx) — файл будет прочитан и проверен

или вставьте текст вручную
C
Помощники
Калькулятор единиц измерения
Перевод единиц: м³, м², кг, литры, пог.м, упаковки
3.14 м³→м²
Калькулятор единиц измерения
Переводите м³ ↔ м² ↔ пог.м ↔ упаковки ↔ кг ↔ литры — быстро и без ошибок
🎯 Персональный подбор
Подбор тендеров под
вашу компанию
Опишите чем занимается ваша компания — ИИ подберёт подходящие тендеры, сформирует поисковые запросы и ссылки на актуальные закупки
ОКПД2 коды подбираются автоматически
Прямые ссылки на zakupki.gov.ru
Анализ конкурентной среды в нише
`; const printWin = window.open('', '_blank'); if (!printWin) { alert('Разрешите всплывающие окна для этого сайта и попробуйте снова'); btn.innerHTML = 'Скачать PDF'; btn.disabled = false; return; } printWin.document.open(); printWin.document.write(reportHtml); printWin.document.close(); // Ждём загрузки шрифтов и стилей, потом печатаем setTimeout(() => { printWin.focus(); printWin.print(); btn.innerHTML = ' Скачать PDF'; btn.disabled = false; }, 1200); } function plural(n, one, few, many) { const mod10 = n % 10, mod100 = n % 100; if (mod10 === 1 && mod100 !== 11) return one; if ([2,3,4].includes(mod10) && ![12,13,14].includes(mod100)) return few; return many; } /* ── DEMO PDF ── */ function loadDemoAndPDF() { const demoText = `📋 АНАЛИЗ ТЕНДЕРА № 0362100021426000079 Комплектующие для инженерно-технической службы Заказчик: ФКУ ИК-6 ГУФСИН России по Свердловской области НМЦ контракта: 712 574,47 ₽ · Срок подачи заявок: 05.06.2026 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📅 СРОКИ ПОСТАВКИ И ОПЛАТЫ ✅ ПЛЮС: Оплата в течение 7 рабочих дней после приёмки товара ⚠️ ВНИМАНИЕ: Срок поставки — до 30 июня 2026 г. С момента подписания контракта (~09-12 июня) остаётся около 2-3 недель. Сроки крайне сжатые. ⚠️ ВНИМАНИЕ: Камеры IP (40 шт.) — заказной товар, срок поставки по счёту ЭТМ к 17.06.2026. Необходимо подтвердить готовность поставщика. ℹ️ Приёмка товара — 10 рабочих дней с момента доставки. ℹ️ Финансирование федеральное, лимиты 2026 года доведены в полном объёме. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⚠️ РИСКИ И ЛОВУШКИ ⛔ КРИТИЧНО: Место поставки — режимный объект (исправительная колония ФСИН). Необходимо заранее согласовать пропуск транспорта и водителей на территорию — это может занять до 5 рабочих дней. ⛔ КРИТИЧНО: При снижении цены на 25% и более (ниже 534 430 ₽) применяются антидемпинговые меры — обеспечение контракта вырастет в 1,5 раза (ст. 37 Закона 44-ФЗ). ⚠️ ВНИМАНИЕ: Заказчик вправе изменить объём поставки на ±10%. Готовьтесь к возможному пересчёту. ⚠️ ВНИМАНИЕ: Возможен односторонний отказ от контракта — при расторжении по вине поставщика сведения направляются в РНП (запрет на участие в госзакупках 2 года). ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔄 АНАЛОГИ РАЗРЕШЕНЫ? ℹ️ Частично. Большинство характеристик зафиксированы — менять нельзя. ✅ ПЛЮС: По ряду позиций (мощность PoE, угол обзора камеры, температурный диапазон) допускается указать конкретное значение в пределах диапазона — это пространство для аналогов. ⛔ КРИТИЧНО: Национальный режим — весь товар только российского производства (или ЕАЭС). Основание: ПП РФ № 1875 от 23.12.2024. Страна происхождения в заявке: Россия (643). ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📊 СРАВНЕНИЕ ТЗ И СЧЁТА ПОСТАВЩИКА (ЭТМ) Позиция 1 — Коммутатор SW-8244/L(400W): ✅ Соответствует. Уточнить поддержку Extended Fabrics. Позиция 2 — Разъём RJ-45 GENERICA: ✅ Соответствует полностью. Позиция 3 — Кабель СегментЛАН ZHнг(А)-FRHF: ⚠️ Уточнить цвет кабеля (ТЗ требует чёрный). Позиция 4 — Кабель ParLan PE: ⚠️ Уточнить цвет кабеля (ТЗ требует чёрный). Позиция 5 — Камера RVi-1NCD2032 (2.8): ⛔ Требует проверки datasheet! ТЗ требует: -50°C, угол ≥120°, микрофон, грозозащита. Позиция 6 — Коробка 60-0210: ✅ Соответствует полностью. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔒 ОБЕСПЕЧЕНИЕ И ШТРАФЫ ℹ️ Обеспечение исполнения контракта — 10% от цены (~71 257 ₽). Деньги на счёт или банковская гарантия. ✅ ПЛЮС: Если есть 3 исполненных контракта без штрафов за последние 3 года — обеспечение не требуется. ✅ ПЛЮС: Обеспечение заявки не требуется. ℹ️ Пени за просрочку — 1/300 ключевой ставки ЦБ в день (~323 ₽/день при ставке 14,5%). ℹ️ Штраф за нарушение — 1% от цены контракта, не менее 1 000 ₽ и не более 5 000 ₽. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🚚 УСЛОВИЯ ДОСТАВКИ И РАЗГРУЗКИ ⛔ КРИТИЧНО: Доставка и разгрузка полностью за счёт поставщика (п. 2.2 контракта). ⚠️ ВНИМАНИЕ: Товар доставляется одной партией — разбить на несколько поездок нельзя. ⚠️ ВНИМАНИЕ: Адрес — режимный объект. Уведомить заказчика за 2+ дня до доставки. Заранее оформить пропуск. ℹ️ При повреждении упаковки товар вернут — расходы на хранение и возврат за поставщика. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📌 ОБЯЗАТЕЛЬНЫЕ ТРЕБОВАНИЯ К ЗАЯВКЕ ⛔ КРИТИЧНО: Конкретные числовые значения по каждой характеристике. Нельзя писать "не менее", "соответствует", "аналог". ⛔ КРИТИЧНО: Страна происхождения — "Россия / 643" по каждой позиции. ℹ️ Сертификаты соответствия — передаются при поставке. ℹ️ Декларации участника — об отсутствии банкротства, долгов, аффилированности с заказчиком. ℹ️ Гарантийный срок — 12 месяцев с момента поставки. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🏢 ДОСЬЕ НА ЗАКАЗЧИКА ℹ️ ФКУ "Исправительная колония № 6 ГУФСИН России по Свердловской области" ℹ️ ИНН: 6669013523 · ОГРН: 1026601373424 · Нижний Тагил, ул. Западная, 3а ✅ ПЛЮС: Финансирование федеральное — риск неоплаты минимален. ⚠️ ВНИМАНИЕ: Учреждение ФСИН — платит медленнее коммерческих, но надёжно. ℹ️ Контакт: Ваулина Т.Н., тел. 7-3435-379147 ℹ️ Споры — Арбитражный суд Свердловской области. Претензионный порядок обязателен, срок ответа 20 дней. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✅ ИТОГОВОЕ РЕЗЮМЕ УЧАСТВОВАТЬ: ДА, с учётом следующих условий: 1. Срочно проверить datasheet камеры RVi-1NCD2032 — соответствие по температуре (-50°C), углу обзора (≥120°), наличие микрофона и грозозащиты 2. Уточнить у поставщика цвет кабелей (нужен чёрный) 3. Заблаговременно согласовать пропуск на территорию колонии 4. Подтвердить готовность поставки камер к 17.06.2026 ГЛАВНЫЕ РИСКИ: сжатые сроки + режимный объект + требования к камере ПОТЕНЦИАЛ: хорошая маржа при правильном подборе поставщиков`; const rb = document.getElementById('resultBody'); rb.dataset.rawText = demoText; rb.innerHTML = markdownToHtml(demoText); document.getElementById('result').classList.add('show'); setTimeout(() => downloadPDF(), 300); } /* ── INIT ── */ loadKey(); updateBanner(); // // initTicker() disabled; // disabled setCalcMode('volume'); document.addEventListener('DOMContentLoaded', initDropZones); // На случай если DOM уже загружен if (document.readyState !== 'loading') initDropZones(); /* ── БЛОКИРУЕМ открытие файлов браузером ── */ // Вешаем на window И document, capture phase, БЕЗ stopPropagation // (stopPropagation в capture убивал события до зон — это и была ошибка) (function() { function blockNav(e) { e.preventDefault(); } var opts = { capture: true, passive: false }; window.addEventListener('dragover', blockNav, opts); window.addEventListener('drop', blockNav, opts); document.addEventListener('dragover', blockNav, opts); document.addEventListener('drop', blockNav, opts); })(); /* ── TZ/INVOICE DRAG & DROP — через addEventListener ── */ function initDropZones() { document.querySelectorAll('.tz-drop-zone').forEach(function(zone) { var inputId = zone.dataset.input; var nameId = zone.dataset.nameid; zone.addEventListener('click', function() { document.getElementById(inputId).click(); }); zone.addEventListener('dragenter', function(e) { e.preventDefault(); e.stopPropagation(); zone.classList.add('drag-over'); }); zone.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); zone.classList.add('drag-over'); }); zone.addEventListener('dragleave', function(e) { e.stopPropagation(); if (!zone.contains(e.relatedTarget)) zone.classList.remove('drag-over'); }); zone.addEventListener('drop', function(e) { e.preventDefault(); e.stopPropagation(); zone.classList.remove('drag-over'); var file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if (!file) return; try { var dt2 = new DataTransfer(); dt2.items.add(file); document.getElementById(inputId).files = dt2.files; } catch(err) {} tzFileChosen({ files: [file] }, zone.id, nameId); }); }); } function tzFileChosen(input, zoneId, nameId) { var file = input.files ? input.files[0] : input; if (!file) return; var zone = document.getElementById(zoneId); var nameEl = document.getElementById(nameId); if (zone) zone.classList.add('has-file'); if (nameEl) nameEl.innerHTML = '✓ ' + file.name + ''; } function tzDragOver(e) { e.preventDefault(); } function tzDragLeave() {} function tzDrop(e) { e.preventDefault(); } /* Определяем категорию файла */ function getFileCategory(file) { const name = file.name.toLowerCase(); if (name.endsWith('.pdf')) return 'pdf'; if (name.match(/\.(jpg|jpeg|png|gif|webp)$/)) return 'image'; if (name.match(/\.(docx|doc)$/)) return 'docx'; if (name.match(/\.(xlsx|xls)$/)) return 'xlsx'; return 'text'; // txt, csv, etc. } function getImageMime(file) { const name = file.name.toLowerCase(); if (name.endsWith('.jpg') || name.endsWith('.jpeg')) return 'image/jpeg'; if (name.endsWith('.png')) return 'image/png'; if (name.endsWith('.gif')) return 'image/gif'; if (name.endsWith('.webp')) return 'image/webp'; return 'image/jpeg'; } function readFileB64(file) { return new Promise((res, rej) => { const reader = new FileReader(); reader.onload = e => res(e.target.result.split(',')[1]); reader.onerror = rej; reader.readAsDataURL(file); }); } function readFileText(file) { return new Promise((res, rej) => { const reader = new FileReader(); reader.onload = e => res(e.target.result); reader.onerror = rej; reader.readAsText(file, 'utf-8'); }); } /* Извлекаем текст из DOCX через mammoth (CDN) */ async function extractDocxText(file) { try { if (typeof mammoth === 'undefined') { await loadScript('https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js'); } const arrayBuffer = await file.arrayBuffer(); const result = await mammoth.extractRawText({ arrayBuffer }); return result.value || '[Не удалось извлечь текст из Word-файла]'; } catch(e) { return `[Ошибка чтения Word-файла: ${e.message}]`; } } /* Извлекаем текст из XLSX через SheetJS (CDN) */ async function extractXlsxText(file) { try { if (typeof XLSX === 'undefined') { await loadScript('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js'); } const arrayBuffer = await file.arrayBuffer(); const workbook = XLSX.read(arrayBuffer, { type: 'array' }); let text = ''; workbook.SheetNames.forEach(sheetName => { const sheet = workbook.Sheets[sheetName]; text += `\n[Лист: ${sheetName}]\n`; text += XLSX.utils.sheet_to_csv(sheet); }); return text || '[Не удалось извлечь текст из Excel-файла]'; } catch(e) { return `[Ошибка чтения Excel-файла: ${e.message}]`; } } function loadScript(src) { return new Promise((res, rej) => { if (document.querySelector(`script[src="${src}"]`)) { res(); return; } const s = document.createElement('script'); s.src = src; s.onload = res; s.onerror = rej; document.head.appendChild(s); }); } /* Извлекаем текст из PDF через pdf.js (CDN) */ async function extractPdfText(file) { try { if (typeof pdfjsLib === 'undefined') { await loadScript('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'); pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; } const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; let text = ''; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const content = await page.getTextContent(); text += content.items.map(it => it.str).join(' ') + '\n'; } return text.trim() || '[PDF не содержит текстового слоя — попробуйте Word/Excel версию]'; } catch(e) { return `[Ошибка чтения PDF: ${e.message}]`; } } /* ── Drag & drop для блока "Подбор аналогов" ── */ (function(){ var zone = document.getElementById('analogDropZone'); var input = document.getElementById('analogFileInput'); var status = document.getElementById('analogFileStatus'); var textarea = document.getElementById('analogInput'); if (!zone || !input || !textarea) return; function showStatus(msg, isError) { status.textContent = msg; status.className = 'analog-file-status show' + (isError ? ' error' : ''); } async function handleFile(file) { if (!file) return; showStatus('Читаю файл «' + file.name + '»…'); var text = ''; var name = file.name.toLowerCase(); try { if (name.endsWith('.pdf')) { text = await extractPdfText(file); } else if (name.match(/\.(docx|doc)$/)) { text = await extractDocxText(file); } else if (name.match(/\.(xlsx|xls)$/)) { text = await extractXlsxText(file); } else { text = await readFileText(file); } } catch(e) { showStatus('Не удалось прочитать файл: ' + e.message, true); return; } if (!text || !text.trim()) { showStatus('Не удалось извлечь текст из файла', true); return; } var prefix = textarea.value.trim() ? textarea.value.trim() + '\n\n' : ''; textarea.value = prefix + text.trim(); zone.classList.add('has-file'); showStatus('✓ Текст из «' + file.name + '» добавлен в поле выше'); } zone.addEventListener('dragover', function(e){ e.preventDefault(); zone.classList.add('drag-over'); }); zone.addEventListener('dragleave', function(){ zone.classList.remove('drag-over'); }); zone.addEventListener('drop', function(e){ e.preventDefault(); zone.classList.remove('drag-over'); var files = e.dataTransfer.files; if (files && files.length) handleFile(files[0]); }); input.addEventListener('change', function(e){ if (e.target.files && e.target.files.length) handleFile(e.target.files[0]); }); })(); /* Формируем content-блок для API */ async function buildContentBlock(file, label) { const cat = getFileCategory(file); if (cat === 'pdf') { const b64 = await readFileB64(file); return { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: b64 } }; } if (cat === 'image') { const b64 = await readFileB64(file); return { type: 'image', source: { type: 'base64', media_type: getImageMime(file), data: b64 } }; } if (cat === 'docx') { const text = await extractDocxText(file); return { type: 'text', text: `[${label} — Word-файл: ${file.name}]\n\n${text}` }; } if (cat === 'xlsx') { const text = await extractXlsxText(file); return { type: 'text', text: `[${label} — Excel-файл: ${file.name}]\n\n${text}` }; } // txt, csv const text = await readFileText(file); return { type: 'text', text: `[${label} — файл: ${file.name}]\n\n${text}` }; } async function compareTzInvoice() { const tzFile = document.getElementById('tzFile').files[0]; const invFile = document.getElementById('invoiceFile').files[0]; if (!tzFile || !invFile) { showNotification('Загрузите оба файла: ТЗ и счёт поставщика'); return; } const btn = event.target; btn.disabled = true; btn.textContent = 'Читаю файлы...'; const resultEl = document.getElementById('tzCompareResult'); resultEl.style.display = 'none'; try { const [tzBlock, invBlock] = await Promise.all([ buildContentBlock(tzFile, 'Техническое задание'), buildContentBlock(invFile, 'Счёт поставщика') ]); btn.textContent = 'Анализирую...'; const contentParts = [ tzBlock, invBlock, { type: 'text', text: `Первый файл (${tzFile.name}) — это Техническое задание (ТЗ), второй файл (${invFile.name}) — это счёт поставщика. Твоя задача: сравнить каждую позицию ТЗ с тем, что указано в счёте поставщика. СТРОГИЕ ПРАВИЛА ЗАПОЛНЕНИЯ ПОЛЕЙ: "tz_req" — ТОЛЬКО ключевые технические параметры из ТЗ для данной позиции. Кратко, без воды. Например: "RJ-45, UTP, CAT5e, 100 шт." "invoice" — ТОЛЬКО наименование товара и артикул из счёта. БЕЗ цен, БЕЗ сумм, БЕЗ НДС. Например: "CS3-1C5EU-G (GENERICA), RJ-45 UTP Cat5e, 100 шт." "status" и "comment" — ВАЖНЕЙШЕЕ ПРАВИЛО: - Если параметр явно и чётко указан в счёте — это ПОДТВЕРЖДЕНО. НЕ пиши "уточнить" для того, что уже написано в счёте. - "ok" → параметр из ТЗ прямо подтверждён данными счёта. - "warn" → параметр НЕ указан в счёте вообще (невозможно проверить), или есть реальное противоречие которое требует уточнения у поставщика. - "fail" → параметр из счёта явно противоречит требованию ТЗ. ПРИМЕРЫ ПРАВИЛЬНОЙ ЛОГИКИ: - ТЗ требует 28 портов, в счёте написано "28 портов" → status: "ok", НЕ "warn". - ТЗ требует чёрный цвет кабеля, в счёте цвет не указан вообще → status: "warn", comment: "Цвет в счёте не указан — необходимо уточнить у поставщика". - ТЗ требует 100 шт., в счёте 50 шт. → status: "fail". В "comment" пиши ТОЛЬКО реальные риски и несоответствия. Не дублируй то, что уже очевидно из колонок tz_req и invoice. Верни ТОЛЬКО JSON без пояснений, без markdown, без \`\`\`json: { "title": "Сравнительный анализ ТЗ и счёта поставщика", "date": "${new Date().toLocaleDateString('ru-RU')}", "tz_file": "${tzFile.name}", "invoice_file": "${invFile.name}", "rows": [ { "num": "1", "position": "Название позиции", "tz_req": "Только ключевые параметры ТЗ", "invoice": "Только номенклатура и артикул из счёта, без цен", "status": "ok|warn|fail", "comment": "Только реальные риски/расхождения. Если всё OK — пустая строка." } ], "summary": "Итог в 2-3 предложениях", "can_accept": true, "critical_count": 0, "warn_count": 0, "ok_count": 0 }` } ]; const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 4096, messages: [{ role: 'user', content: contentParts }] }) }); if (!response.ok) { const e = await response.json(); throw new Error(e.detail || response.status); } const data = await response.json(); const rawText = data.result || '{}'; btn.textContent = 'Формирую PDF...'; // Парсим JSON let report; try { const clean = rawText.replace(/```json|```/g, '').trim(); report = JSON.parse(clean); } catch(e) { throw new Error('Не удалось разобрать ответ ИИ. Попробуйте ещё раз.'); } // Генерируем PDF через jsPDF + AutoTable await generateComparisonPDF(report); // Показываем мини-превью на странице resultEl.innerHTML = renderResultPreview(report); resultEl.style.display = 'block'; resultEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch(err) { showNotification('Ошибка: ' + err.message); } finally { btn.disabled = false; btn.textContent = 'Сравнить →'; } } /* ── PDF генерация ── */ async function generateComparisonPDF(report) { // Загружаем html2canvas + jsPDF if (typeof html2canvas === 'undefined') { await loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js'); } if (typeof window.jspdf === 'undefined') { await loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'); } const canAccept = report.can_accept !== false; const statusIcon = { ok: '✅', warn: '⚠️', fail: '🔴' }; const statusLabel = { ok: 'Соответствует', warn: 'Уточнить', fail: 'Не соответствует' }; const statusStyle = { ok: 'background:#ecfdf5;color:#059669;border:1.5px solid #6ee7b7;', warn: 'background:#fffbeb;color:#d97706;border:1.5px solid #fcd34d;', fail: 'background:#fef2f2;color:#dc2626;border:1.5px solid #fca5a5;', }; const rows = (report.rows || []).map((r, i) => { const s = r.status || 'ok'; const bg = i % 2 === 0 ? '#ffffff' : '#f8f8fe'; const rowBg = s === 'fail' ? '#fff8f8' : bg; return ` ${r.num||''} ${r.position||''} ${r.tz_req||''} ${r.invoice||''} ${statusIcon[s]} ${statusLabel[s]||s} ${r.comment||''} `; }).join(''); const date = new Date().toLocaleDateString('ru-RU'); // Создаём скрытый div с HTML-таблицей для рендеринга const container = document.createElement('div'); container.style.cssText = 'position:fixed;left:-9999px;top:0;width:1100px;background:white;font-family:Inter,Arial,sans-serif;'; container.innerHTML = `
ТендерУМ — Сравнительный анализ ТЗ и счёта поставщика
ТЗ: ${report.tz_file||'—'}  |  Счёт: ${report.invoice_file||'—'}  |  Дата: ${date}
✅ Соответствует: ${report.ok_count||0} ⚠️ Уточнить: ${report.warn_count||0} 🔴 Не соответствует: ${report.critical_count||0} ${canAccept?'✅ Счёт можно принять':'❌ Требует доработки'}
${rows}
Позиция Требование ТЗ Счёт поставщика Статус Комментарий / Риск
Итоговый вывод: ${report.summary||''}
Сформировано сервисом ТендерУМ · tenderum.ru
`; document.body.appendChild(container); try { const canvas = await html2canvas(container, { scale: 2, useCORS: true, allowTaint: true, backgroundColor: '#ffffff', logging: false, }); const { jsPDF } = window.jspdf; const imgW = 297; // A4 landscape width mm const imgH = (canvas.height * imgW) / canvas.width; const pageH = 210; // A4 landscape height mm const doc = new jsPDF({ orientation: imgH > pageH ? 'portrait' : 'landscape', unit: 'mm', format: 'a4' }); const docW = doc.internal.pageSize.getWidth(); const docH = doc.internal.pageSize.getHeight(); const ratio = canvas.height / canvas.width; const finalH = docW * ratio; if (finalH <= docH) { doc.addImage(canvas.toDataURL('image/png'), 'PNG', 0, 0, docW, finalH); } else { // Разбиваем на страницы const pageCanvas = document.createElement('canvas'); const ctx = pageCanvas.getContext('2d'); const pxPerPage = Math.floor(canvas.width * (docH / docW)); let yOffset = 0; let page = 0; while (yOffset < canvas.height) { pageCanvas.width = canvas.width; pageCanvas.height = Math.min(pxPerPage, canvas.height - yOffset); ctx.clearRect(0, 0, pageCanvas.width, pageCanvas.height); ctx.drawImage(canvas, 0, yOffset, canvas.width, pageCanvas.height, 0, 0, canvas.width, pageCanvas.height); if (page > 0) doc.addPage(); const ph = docW * (pageCanvas.height / canvas.width); doc.addImage(pageCanvas.toDataURL('image/png'), 'PNG', 0, 0, docW, ph); yOffset += pxPerPage; page++; } } doc.save(`Анализ_ТЗ_${date.replace(/\./g,'_')}.pdf`); } finally { document.body.removeChild(container); } } /* ── Превью на странице (после генерации PDF) ── */ function renderResultPreview(report) { const statusIcon = { ok: '✅', warn: '⚠️', fail: '🔴' }; const statusLabel = { ok: 'OK', warn: 'Уточнить', fail: 'Не соответствует' }; const statusStyle = { ok: 'background:#ecfdf5;color:#059669;border:1px solid #6ee7b7;', warn: 'background:#fffbeb;color:#d97706;border:1px solid #fcd34d;', fail: 'background:#fef2f2;color:#dc2626;border:1px solid #fca5a5;', }; const rows = (report.rows || []).map(r => { const s = r.status || 'ok'; const st = statusStyle[s] || statusStyle.ok; return ` ${r.num} ${r.position} ${r.tz_req} ${r.invoice} ${statusIcon[s]} ${statusLabel[s] || s} ${r.comment} `; }).join(''); const canAccept = report.can_accept !== false; return `
📊 Анализ завершён — PDF скачан
✅ ${report.ok_count||0} OK ⚠️ ${report.warn_count||0} уточнить 🔴 ${report.critical_count||0} не соответствует
${rows}
Позиция Требование ТЗ Счёт поставщика Статус Комментарий
Итог: ${report.summary||''}
${canAccept?'✅ Можно принять':'❌ Требует доработки'}
`; }