diff --git a/backend-py/app/main.py b/backend-py/app/main.py
index 030aa48..ac9e7cc 100644
--- a/backend-py/app/main.py
+++ b/backend-py/app/main.py
@@ -136,6 +136,7 @@ async def _dispatch_post(request: Request):
"proposal_detail": proposals_mod.handle_detail,
"proposal_vote": proposals_mod.handle_vote,
"proposal_client_submit": proposals_mod.handle_client_submit,
+ "contract_review": _handle_contract_review,
"ping": lambda b: {"pong": True, "time": _now_iso()},
"seed_admin": lambda b: _handle_seed_admin(),
"test_ai": lambda b: _handle_test_ai(),
@@ -431,6 +432,14 @@ async def api_proposal_client_submit(request: Request):
return JSONResponse(proposals_mod.handle_client_submit(body))
+@app.post("/api/contract_review")
+async def api_contract_review(request: Request):
+ """AI-анализ текста договора. GigaChat → структурированный разбор на русском."""
+ body = await _safe_json(request)
+ import asyncio
+ return JSONResponse(await asyncio.to_thread(_handle_contract_review, body))
+
+
def _handle_daily_reminders() -> dict[str, Any]:
"""Находит клиентов с годовщиной договора сегодня по МСК.
Дедуплицирует: один менеджер + один клиент = одно уведомление,
@@ -3024,6 +3033,83 @@ def _handle_measurements_list(body: dict[str, Any]) -> dict[str, Any]:
return {"ok": True, "count": len(out), "measurements": out}
+_CONTRACT_SYSTEM = """\
+Ты — помощник клиента мебельной фабрики ЗОВ (Россия). \
+Клиент получил договор на покупку и монтаж кухни и хочет понять его содержание.
+
+Твоя задача — проанализировать текст и дать ответ строго в формате JSON без Markdown-оберток. \
+Структура ответа:
+{
+ "summary": "2-3 предложения — о чём договор",
+ "payment": {
+ "total": "итоговая сумма если есть",
+ "schedule": "схема оплаты (предоплата %, доплата когда)",
+ "prepayment_pct": число_или_null
+ },
+ "deadlines": [
+ {"label": "Изготовление", "value": "дата или срок", "note": "подробности"}
+ ],
+ "risks": [
+ {"level": "high|medium|low", "title": "заголовок", "description": "что именно"}
+ ],
+ "recommendations": ["что уточнить у менеджера или на что обратить внимание"],
+ "missing_clauses": ["важные пункты которых нет в тексте"]
+}
+
+Риски level:
+- high — условие явно невыгодно клиенту или может привести к потере денег
+- medium — спорное, зависит от ситуации
+- low — мелочь, но лучше знать
+
+Если есть конкретный вопрос (поле question) — добавь поле "question_answer": "ответ на вопрос".
+Отвечай на русском. Будь конкретным, избегай юридического жаргона.
+"""
+
+
+def _handle_contract_review(body: dict[str, Any]) -> dict[str, Any]:
+ """Клиент вставляет текст договора — AI анализирует его простым языком.
+ body: { initData, text: str, question?: str }
+ """
+ cfg = get_config()
+ auth = verify_init_data(body.get("initData") or "", cfg.bot_token)
+ if not auth:
+ unsafe = body.get("initDataUnsafe") or {}
+ if not (isinstance(unsafe, dict) and unsafe.get("user", {}).get("id")):
+ return {"error": "invalid_init_data"}
+
+ text = str(body.get("text") or "").strip()
+ question = str(body.get("question") or "").strip()
+
+ if not text:
+ return {"error": "text_required"}
+ if len(text) > 16_000:
+ text = text[:16_000]
+
+ user_prompt = f"Текст договора:\n\n{text}"
+ if question:
+ user_prompt += f"\n\nВопрос клиента: {question}"
+
+ import asyncio
+ result = ai.call_ai(
+ user_prompt,
+ system_prompt=_CONTRACT_SYSTEM,
+ temperature=0.2,
+ max_tokens=3000,
+ )
+
+ if result.get("error"):
+ return {"error": result.get("text", "AI error")}
+
+ analysis = result.get("json") or {}
+ return {
+ "ok": True,
+ "analysis": analysis,
+ "raw_text": result.get("text", ""),
+ "tokens": result.get("tokens", 0),
+ "model": result.get("model", ""),
+ }
+
+
def _handle_test_ai() -> dict[str, Any]:
cfg = get_config()
res = ai.call_ai("Скажи одной фразой: что за фабрика ЗОВ?",
diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js
index fc25dd9..7fa81f1 100644
--- a/miniapp/assets/app.js
+++ b/miniapp/assets/app.js
@@ -615,7 +615,7 @@ function renderClient(me) {
items: [
{ icon: "ruler", color: "blue", label: "Замер кухни", href: "#/c/measure" },
{ icon: "wrench", color: "green", label: "Подобрать технику", href: "#/c/proposal" },
- { icon: "wallet", color: "gold", label: "Калькулятор бюджета", soon: true },
+ { icon: "wallet", color: "gold", label: "Проверить договор", href: "#/c/contract" },
],
},
{
@@ -1621,6 +1621,19 @@ async function init() {
hideSplash();
return;
}
+ if (location.hash.startsWith("#/c/contract")) {
+ app.innerHTML = "";
+ document.body.classList.remove("has-bottom-nav");
+ const oldNavC = document.getElementById("bottom-nav");
+ if (oldNavC) oldNavC.remove();
+ if (typeof Proposals !== "undefined") {
+ Proposals.mountContractReview(app);
+ } else {
+ app.innerHTML = `
Модуль не загружен
`;
+ }
+ hideSplash();
+ return;
+ }
if (me.role === "staff") {
renderStaff(me);
} else if (me.role === "manager") {
@@ -1659,6 +1672,16 @@ function routeByHash() {
} else {
app.innerHTML = `Модуль подбора не загружен
`;
}
+ } else if (location.hash.startsWith("#/c/contract")) {
+ app.innerHTML = "";
+ document.body.classList.remove("has-bottom-nav");
+ const oldNav3 = document.getElementById("bottom-nav");
+ if (oldNav3) oldNav3.remove();
+ if (typeof Proposals !== "undefined") {
+ Proposals.mountContractReview(app);
+ } else {
+ app.innerHTML = `Модуль не загружен
`;
+ }
} else {
// Главный экран по роли
const me = window.__zovMe;
diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css
index 0c99986..6eb9428 100644
--- a/miniapp/assets/podbor.css
+++ b/miniapp/assets/podbor.css
@@ -4374,3 +4374,233 @@
}
.prop-section-open-link:active { opacity: 0.6; }
+/* ============================================================
+ AI-проверка договора (Contract Review)
+ ============================================================ */
+
+/* Textarea + char counter */
+.cr-textarea {
+ font-family: var(--font-ui);
+ font-size: 13.5px;
+ line-height: 1.55;
+ resize: vertical;
+ min-height: 160px;
+}
+.cr-chars {
+ font-family: var(--font-mono);
+ font-size: 9.5px;
+ letter-spacing: 0.08em;
+ color: var(--muted);
+ margin-top: 4px;
+ text-align: right;
+}
+
+/* Preset question chips */
+.cr-presets {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 0;
+}
+
+/* Thinking animation */
+.cr-thinking {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ background: var(--warm);
+ border: 1px solid var(--line);
+ border-radius: var(--r-card);
+ padding: 16px 18px;
+ margin-top: 12px;
+}
+.cr-thinking-icon {
+ font-size: 28px;
+ animation: cr-rock 1.6s ease-in-out infinite;
+}
+@keyframes cr-rock {
+ 0%, 100% { transform: rotate(-8deg); }
+ 50% { transform: rotate(8deg); }
+}
+.cr-thinking-text {
+ font-family: var(--font-ui);
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--ink);
+ line-height: 1.4;
+}
+.cr-thinking-sub {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ letter-spacing: 0.08em;
+ color: var(--muted);
+}
+
+/* Analysis output */
+.cr-analysis {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ margin-top: 16px;
+}
+
+.cr-block {
+ background: var(--card, #FFFCF6);
+ border: 1px solid var(--line);
+ border-radius: var(--r-card);
+ padding: 14px 16px;
+}
+.cr-block-head {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 500;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--muted);
+ margin-bottom: 10px;
+}
+
+/* Summary */
+.cr-summary-block { border-left: 3px solid var(--accent-2); }
+.cr-summary-text {
+ font-family: var(--font-ui);
+ font-size: 14px;
+ line-height: 1.55;
+ color: var(--ink);
+}
+
+/* Q&A block */
+.cr-qa-block { border-left: 3px solid #1A62B0; background: rgba(26,98,176,0.04); }
+.cr-qa-answer {
+ font-family: var(--font-ui);
+ font-size: 14px;
+ line-height: 1.55;
+ color: var(--ink);
+}
+
+/* Key-value pairs (payment) */
+.cr-kv-list { display: flex; flex-direction: column; gap: 8px; }
+.cr-kv-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 12px;
+}
+.cr-kv-label {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 500;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--muted);
+ flex-shrink: 0;
+ white-space: nowrap;
+}
+.cr-kv-val {
+ font-family: var(--font-ui);
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--ink);
+ text-align: right;
+ flex: 1;
+}
+
+/* Deadlines */
+.cr-deadlines { display: flex; flex-direction: column; gap: 8px; }
+.cr-deadline-row {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-template-rows: auto auto;
+ column-gap: 12px;
+}
+.cr-deadline-label {
+ grid-column: 1;
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 500;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--muted);
+ align-self: center;
+ white-space: nowrap;
+}
+.cr-deadline-val {
+ grid-column: 2;
+ font-family: var(--font-ui);
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--ink);
+}
+.cr-deadline-note {
+ grid-column: 2;
+ font-size: 12px;
+ color: var(--muted);
+ line-height: 1.35;
+ margin-top: 1px;
+}
+
+/* Risks */
+.cr-risks { display: flex; flex-direction: column; gap: 10px; }
+.cr-risk {
+ padding: 10px 12px;
+ border-radius: 8px;
+ border-left: 3px solid;
+}
+.cr-risk-high { background: rgba(192,57,43,0.07); border-color: #C0392B; }
+.cr-risk-medium { background: rgba(230,126,34,0.07); border-color: #E67E22; }
+.cr-risk-low { background: rgba(39,174,96,0.06); border-color: #27AE60; }
+.cr-risk-head {
+ font-family: var(--font-ui);
+ font-size: 13.5px;
+ font-weight: 600;
+ color: var(--ink);
+ line-height: 1.25;
+ margin-bottom: 4px;
+}
+.cr-risk-desc {
+ font-size: 12.5px;
+ line-height: 1.45;
+ color: var(--ink-2);
+}
+
+/* Recommendations & missing */
+.cr-rec-list {
+ margin: 0;
+ padding-left: 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+.cr-rec-list li {
+ font-size: 13.5px;
+ line-height: 1.45;
+ color: var(--ink);
+}
+.cr-missing-block { border-left: 3px solid var(--muted); background: var(--paper-2); }
+.cr-missing-list li { color: var(--muted); }
+
+/* Footer note */
+.cr-footer-note {
+ font-family: var(--font-mono);
+ font-size: 9.5px;
+ letter-spacing: 0.06em;
+ color: var(--muted);
+ line-height: 1.5;
+ text-align: center;
+ padding: 4px 0 8px;
+}
+
+/* Raw fallback */
+.cr-raw {
+ white-space: pre-wrap;
+ font-family: var(--font-ui);
+ font-size: 13.5px;
+ line-height: 1.55;
+ color: var(--ink);
+ background: var(--paper-2);
+ border: 1px solid var(--line);
+ border-radius: var(--r-card);
+ padding: 14px 16px;
+ margin-top: 12px;
+}
+
diff --git a/miniapp/assets/proposals.js b/miniapp/assets/proposals.js
index 11c1c7f..f2b3709 100644
--- a/miniapp/assets/proposals.js
+++ b/miniapp/assets/proposals.js
@@ -810,10 +810,251 @@ const Proposals = (function () {
return wrap;
}
+ // ══════════════════════════════════════════════════════════
+ // CONTRACT REVIEW (AI-анализ договора для клиента)
+ // ══════════════════════════════════════════════════════════
+
+ // Preset questions that appear as quick-tap chips
+ const CONTRACT_PRESETS = [
+ "Какие условия оплаты?",
+ "Когда доставка и монтаж?",
+ "Что будет если я откажусь?",
+ "Есть ли штрафы?",
+ "На что обратить внимание?",
+ ];
+
+ async function mountContractReview(container) {
+ container.innerHTML = "";
+
+ container.appendChild(el(`
+
+ `));
+ container.querySelector(".podbor-back").addEventListener("click", () => history.back());
+
+ container.appendChild(el(`
+
+ Проверим
ваш договор
+ Вставьте текст договора — AI объяснит условия простым языком, найдёт риски и подскажет, что уточнить.
+
+
+
Текст договора *
+
+
0 / 16 000 символов
+
+
+
+
Конкретный вопрос (необязательно)
+
+ ${CONTRACT_PRESETS.map(q =>
+ ``
+ ).join("")}
+
+
+
+
+
+
+
+
+
+ `));
+
+ // Char counter
+ const textarea = container.querySelector("#cr_text");
+ const charEl = container.querySelector("#cr_chars");
+ textarea.addEventListener("input", () => {
+ const n = textarea.value.length;
+ charEl.textContent = `${n.toLocaleString("ru-RU")} / 16 000 символов`;
+ charEl.style.color = n > 14000 ? "#C0392B" : "var(--muted)";
+ });
+
+ // Preset chips → fill question input
+ container.querySelectorAll(".cr-preset").forEach(btn => {
+ btn.addEventListener("click", () => {
+ container.querySelector("#cr_question").value = btn.textContent.trim();
+ container.querySelectorAll(".cr-preset").forEach(b => b.classList.remove("on"));
+ btn.classList.add("on");
+ haptic && haptic("selection");
+ });
+ });
+
+ // Submit
+ container.querySelector("#cr_submit").addEventListener("click", async () => {
+ const btn = container.querySelector("#cr_submit");
+ const result = container.querySelector("#cr_result");
+ const text = textarea.value.trim();
+ const question = (container.querySelector("#cr_question")?.value || "").trim();
+
+ if (!text) {
+ result.innerHTML = `Вставьте текст договора
`;
+ return;
+ }
+
+ btn.disabled = true;
+ btn.innerHTML = `Анализируем…`;
+ result.innerHTML = `
+
+
🤔
+
AI читает договор…
Обычно занимает 10–25 секунд
+
`;
+
+ try {
+ const data = await apiFetch("contract_review", { text, question });
+ if (data.error) {
+ result.innerHTML = `Ошибка: ${escHtml(data.error)}
`;
+ btn.disabled = false; btn.textContent = "🤖 Анализировать";
+ return;
+ }
+ haptic && haptic("success");
+ result.innerHTML = "";
+ result.appendChild(renderContractAnalysis(data.analysis, data.raw_text, question));
+ btn.disabled = false; btn.textContent = "🤖 Анализировать снова";
+ // Scroll to result
+ result.scrollIntoView({ behavior: "smooth", block: "start" });
+ } catch (e) {
+ result.innerHTML = `Сеть: ${escHtml(e.message)}
`;
+ btn.disabled = false; btn.textContent = "🤖 Анализировать";
+ }
+ });
+ }
+
+ function renderContractAnalysis(analysis, rawText, question) {
+ // If AI returned unstructured text (not JSON), show it plain
+ if (!analysis || !Object.keys(analysis).length) {
+ return el(`${escHtml(rawText || "AI не вернул анализ")}
`);
+ }
+
+ const wrap = el(``);
+
+ // Summary
+ if (analysis.summary) {
+ wrap.appendChild(el(`
+
+
📋 Резюме
+
${escHtml(analysis.summary)}
+
+ `));
+ }
+
+ // Question answer (if specific question asked)
+ if (question && analysis.question_answer) {
+ wrap.appendChild(el(`
+
+
💬 ${escHtml(question)}
+
${escHtml(analysis.question_answer)}
+
+ `));
+ }
+
+ // Payment
+ const pay = analysis.payment;
+ if (pay && (pay.total || pay.schedule)) {
+ const rows = [];
+ if (pay.total) rows.push(["Итого", pay.total]);
+ if (pay.schedule) rows.push(["Схема оплаты", pay.schedule]);
+ if (pay.prepayment_pct != null) rows.push(["Предоплата", `${pay.prepayment_pct}%`]);
+ wrap.appendChild(el(`
+
+
💰 Оплата
+
+ ${rows.map(([k, v]) => `
+
+ ${escHtml(k)}
+ ${escHtml(String(v))}
+
`).join("")}
+
+
+ `));
+ }
+
+ // Deadlines
+ const deadlines = analysis.deadlines || [];
+ if (deadlines.length) {
+ const rows = deadlines.map(d => `
+
+ ${escHtml(d.label || "")}
+ ${escHtml(d.value || "—")}
+ ${d.note ? `${escHtml(d.note)}` : ""}
+
`).join("");
+ wrap.appendChild(el(`
+
+ `));
+ }
+
+ // Risks
+ const risks = analysis.risks || [];
+ if (risks.length) {
+ const riskItems = risks.map(r => {
+ const cls = r.level === "high" ? "high" : r.level === "medium" ? "medium" : "low";
+ const icon = r.level === "high" ? "🔴" : r.level === "medium" ? "🟡" : "🟢";
+ return `
+
+
${icon} ${escHtml(r.title || "")}
+
${escHtml(r.description || "")}
+
`;
+ }).join("");
+ wrap.appendChild(el(`
+
+
⚠️ Риски
+
${riskItems}
+
+ `));
+ }
+
+ // Recommendations
+ const recs = analysis.recommendations || [];
+ if (recs.length) {
+ wrap.appendChild(el(`
+
+
✅ Рекомендации
+
+ ${recs.map(r => `- ${escHtml(r)}
`).join("")}
+
+
+ `));
+ }
+
+ // Missing clauses
+ const missing = analysis.missing_clauses || [];
+ if (missing.length) {
+ wrap.appendChild(el(`
+
+
❓ Чего нет в договоре
+
+ ${missing.map(m => `- ${escHtml(m)}
`).join("")}
+
+
+ `));
+ }
+
+ // Footer note
+ wrap.appendChild(el(`
+
+ `));
+
+ return wrap;
+ }
+
// ══════════════════════════════════════════════════════════
// PUBLIC API
// ══════════════════════════════════════════════════════════
- return { mountClient, mountManager };
+ return { mountClient, mountManager, mountContractReview };
})();
diff --git a/miniapp/index.html b/miniapp/index.html
index 57f908e..bc8a687 100644
--- a/miniapp/index.html
+++ b/miniapp/index.html
@@ -12,8 +12,8 @@
-
-
+
+
@@ -35,16 +35,16 @@
CRM
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+