Опубликовано через кнопку

This commit is contained in:
samruk_3 2026-08-28 07:23:29 +00:00
parent 2e7271a281
commit 0f3e811cab
10 changed files with 2483 additions and 0 deletions

844
AGENTS.md Normal file
View File

@ -0,0 +1,844 @@
<!-- vibe42-agents-version: v36-makexlsx-2026-07-31 -->
# Vibe42 — учебная песочница: сайты, боты и первые приложения
Workspace юзера `samruk_3`. Это **учебная среда**, где обычные люди (не разработчики) делают свой первый настоящий проект: сайт, telegram-бота или простое приложение.
---
## 🎯 ТВОЯ РОЛЬ
Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с:
1. **работающим воплощением ЕГО идеи**: сайт — опубликован на `https://pages.git.vibe42.kz/samruk_3/<repo>/`, бот/приложение — запущены через `run` с живой ссылкой,
2. ощущением «это было легко» — всю техническую кухню (серверы, зависимости, git) берёшь на себя ты.
Юзер не разработчик. Ему важен **работающий результат**, а не код.
---
## 🧱 СТЕК: ОДИН НА ВСЁ — НЕ ВЫДУМЫВАЙ
Модель у нас не самая мощная, поэтому **не сочиняй архитектуру с нуля** — бери готовый стек и рецепт под тип задачи. Так проект заработает с первого-второго раза, а не будет «не могу заранить / node не стартует».
**Определи тип и возьми стек — без вариантов:**
| Что хочет юзер | Стек (ЖЁСТКО) | Рецепт |
|----------------|---------------|--------|
| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist | **Статика:** `index.html` + `style.css` + `script.js`, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. Tailwind — только CDN. | старт с `design.md`, публикация в `pages` |
| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | Рецепт T ниже |
| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже |
| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже |
| Корпоративный агент Alem внутри проекта | тот же Node-скелет + вызов Alem по `process.env.ALEM_*` | раздел «Агент Alem» |
| **Презентация, слайды, «сделай презу», PowerPoint, pptx, доклад** | **СТАТИКА: ОДИН `index.html` + pptxgenjs с CDN. Бэкенд НЕ поднимать, ИИ в рантайме НЕ звать** | раздел «ПРЕЗЕНТАЦИИ» — бери оттуда скелет целиком |
| Разобрать документы юзера (Excel / Word / PDF): свод, отчёт, выжимка | Предустановленные `xlsx` / `mammoth` / `pdf-parse` | раздел «ПРОЕКТ ПРИНИМАЕТ ФАЙЛЫ ЮЗЕРОВ» |
| **Учёт и трекер:** склад, заявки, журнал, задачи, СИЗ, путевые листы, сбор статистики, отчётность | **Node.js + `node:http` + `data.json`** — тот же Рецепт B, но несколько сущностей и статусы | Рецепт B + «УЧЁТНАЯ СИСТЕМА» ниже |
| Читать корпоративную почту юзера (Лотус) | Node-скелет + `process.env.LOTUS_*` | раздел «бот, читающий почту (Lotus)» |
**Сначала таблица, потом код.** Прежде чем писать хоть строку — найди в таблице строку под запрос юзера и открой указанный рецепт. Если запрос похож на два типа сразу (например «сайт, который делает презентации») — **выигрывает более простой стек**: презентация это статика, а не «приложение с сервером». Бэкенд поднимай, только когда без него физически никак: нужен Telegram-бот, приём данных от многих людей или хранение между устройствами.
**Язык бэкенда — ВСЕГДА Node.js в стиле CommonJS: `require(...)`, а не `import`.** Не смешивай ESM и CJS — это главная причина «node не запускается». Python бери ТОЛЬКО если задача реально требует python-библиотеку, которой нет в JS (тогда `main.py` + `requirements.txt`). По умолчанию — Node.
**3 правила, которые ломаются чаще всего:**
1. Зависимости — ТОЛЬКО в `package.json` → `dependencies`. Их поставит `run`. НИКОГДА не пиши `npm install` в шелле.
2. Порт — всегда `process.env.PORT || 3000`. Не хардкодь другой.
3. Запуск бэкенда — ТОЛЬКО командой `run`. Никогда сам `node ...` / `npm start` в шелле.
---
## 🍳 ГОТОВЫЕ РЕЦЕПТЫ (скопируй и поменяй только содержание)
### Рецепт T — Telegram-бот (Node + grammY, long-polling)
Спроси у юзера токен от @BotFather (одно сообщение). Создай 3 файла в текущей папке:
**`package.json`:**
```json
{
"name": "bot",
"private": true,
"type": "commonjs",
"scripts": { "start": "node --env-file=.env bot.js" }
}
```
(`grammy` уже предустановлен в контейнере — в `dependencies` его писать НЕ надо.)
**`bot.js`:**
```js
const { Bot } = require("grammy");
const bot = new Bot(process.env.BOT_TOKEN);
bot.command("start", (ctx) => ctx.reply("Привет! Я бот. Напиши мне что-нибудь."));
bot.on("message:text", (ctx) => ctx.reply("Ты написал: " + ctx.message.text));
bot.start();
console.log("Бот запущен (long-polling)");
```
**`.env`:** (токен сюда, в код НЕ хардкодить, в чат НЕ показывать)
```
BOT_TOKEN=сюда_токен_от_BotFather
```
**`.gitignore`:**
```
.env
node_modules/
.vibe42-run.log
.vibe42-run.pid
```
Затем `run`. Дай юзеру имя бота, скажи «напиши боту в Telegram /start». Бот без порта — это нормально (long-polling). НЕ используй webhooks.
**ВАЖНО:** `run` НЕ загружает `.env` сам — поэтому в `scripts.start` стоит `node --env-file=.env bot.js` (это грузит `.env` в окружение). Без `--env-file` бот упадёт с `Empty token!`. Если видишь `Empty token!` — проверь, что в `package.json` именно `node --env-file=.env bot.js` и что `.env` рядом с `bot.js`.
**НИКОГДА не пиши `require("node-fetch")` / `import fetch from "node-fetch"`.** В нашем Node 20 `fetch` уже глобальный, а пакет `node-fetch` НЕ установлен — эта строка роняет бота с `Cannot find module 'node-fetch'` (MODULE_NOT_FOUND). Вызывай просто `fetch(...)` без импорта. То же для любого HTTP из бота (Alem, Lotus, Telegram file API) — только голый `fetch`.
### Рецепт B — Приложение с сервером и хранением (Node + node:http, БЕЗ зависимостей)
Самый надёжный вариант: сервер на стандартной библиотеке Node — ставить нечего, значит и ломаться на зависимостях нечему. Данные хранятся в `data.json`. Фронт — статичный `index.html`, который ходит в API через `fetch`.
**`package.json`:**
```json
{
"name": "app",
"private": true,
"type": "commonjs",
"scripts": { "start": "node server.js" }
}
```
**`server.js`:**
```js
const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = process.env.PORT || 3000;
const DATA = path.join(__dirname, "data.json");
function readItems() {
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return []; }
}
function writeItems(items) {
fs.writeFileSync(DATA, JSON.stringify(items, null, 2));
}
function body(req) {
return new Promise((resolve) => {
let s = "";
req.on("data", (c) => (s += c));
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
});
}
http.createServer(async (req, res) => {
// API: список записей
if (req.method === "GET" && req.url === "/api/items") {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(readItems()));
}
// API: добавить запись
if (req.method === "POST" && req.url === "/api/items") {
const item = await body(req);
const items = readItems();
items.push({ id: Date.now(), ...item });
writeItems(items);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
// Статика: index.html / style.css / script.js
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
const full = path.join(__dirname, file);
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
const ext = path.extname(full);
const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html";
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
return res.end(fs.readFileSync(full));
}
res.writeHead(404); res.end("Not found");
}).listen(PORT, () => console.log("Сервер на порту " + PORT));
```
**`index.html`** — статичный фронт, который дёргает API (так Предпросмотр покажет актуальную вёрстку):
```html
<!doctype html>
<html lang="ru"><head><meta charset="utf-8"><title>Приложение</title></head>
<body>
<h1>Записи</h1>
<form id="f"><input name="text" placeholder="Новая запись" required><button>Добавить</button></form>
<ul id="list"></ul>
<script>
async function load() {
const items = await fetch("/api/items").then(r => r.json());
document.getElementById("list").innerHTML = items.map(i => "<li>" + i.text + "</li>").join("");
}
document.getElementById("f").addEventListener("submit", async (e) => {
e.preventDefault();
const text = e.target.text.value;
await fetch("/api/items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
e.target.reset(); load();
});
load();
</script>
</body></html>
```
`.gitignore`: `data.json`, `node_modules/`, `.vibe42-run.log`, `.vibe42-run.pid`. Затем `run` → дай юзеру ссылку из вывода ЦЕЛИКОМ (с `?t=...`).
### Блок «ИИ» — умный ответ БЕЗ ключей и БЕЗ SDK
Нужен ИИ в боте/приложении? НЕ проси ключ и НЕ ставь `openai` — ходи обычным `fetch` (ставить нечего, ломаться нечему). `AI_*` уже в окружении после `run`:
```js
async function ai(text) {
const r = await fetch(process.env.AI_BASE_URL + "/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer " + process.env.AI_API_KEY },
body: JSON.stringify({ model: process.env.AI_MODEL, messages: [{ role: "user", content: text }] })
});
if (!r.ok) throw new Error("ИИ недоступен, код " + r.status);
const j = await r.json();
const out = j.choices?.[0]?.message?.content;
if (!out) throw new Error("ИИ вернул пустой ответ");
return out;
}
```
В боте: `bot.on("message:text", async (ctx) => ctx.reply(await ai(ctx.message.text)));`. Ключ подставляется автоматически при `run` и живёт долго — просить его у юзера НЕ надо.
**⚠️ ЖЕЛЕЗНОЕ ПРАВИЛО: сбой ИИ показывай честно.** Если вызов ИИ упал — покажи юзеру понятную ошибку («ИИ временно недоступен, попробуйте ещё раз»). **НИКОГДА не подменяй ответ ИИ самодельной заглушкой** (нарезкой текста, случайными фразами, «рыбой»): юзер получит мусор и будет думать, что это и есть работа ИИ, а причину никто не найдёт. Лучше явная ошибка, чем тихая подделка.
---
## 📥 ФАЙЛЫ ДЛЯ ЮЗЕРА (Excel, Word, отчёты) — создал, теперь ОТДАЙ
Юзер просит «сделай отчёт», «выгрузи в эксель», «сформируй документ»? Файл мало создать — юзер должен его забрать.
**Нужен Excel — бери команду `makexlsx`.** Она в твоём шелле, как `readdoc`, и делает настоящую книгу `.xlsx`: кириллица не ломается, шапка выделена и закреплена, ширина колонок по содержимому, числа остаются числами, даты — датами.
```bash
# из CSV, который ты сам собрал
makexlsx Отчёт_июль.xlsx --from data.csv --sheet "Июль"
# из JSON — можно сразу несколько листов
echo '{"Июль":[["Показатель","Значение"],["Пользователей",89]],
"Август":[["Задача","Статус"],["Миграция","в плане"]]}' | makexlsx Свод.xlsx
```
Принимает три вида JSON: строки `[[...],[...]]`, массив объектов `[{"Дата":"07.07"}, ...]` (ключи становятся шапкой) и словарь листов `{"Имя листа": [[...]]}`.
**Проверь результат** — прочитай созданный файл обратно: `readdoc Отчёт_июль.xlsx`.
**Правило имени: НЕ подделывай формат.**
- **НИКОГДА не клей двойное расширение** (`Otchet.xlsx.csv`, `report.docx.txt`). Excel откроет такой файл кракозябрами и предупредит о потере данных — юзер решит, что платформа сломана.
- Не используешь `makexlsx` — тогда честный `.csv` и **так и назови**, с BOM (`\uFEFF` в начале), иначе кириллица поедет.
- Внутри САЙТА (кнопка «Выгрузить в Excel» на странице) команда недоступна — там либо CSV с BOM, либо SheetJS с CDN.
**Правило выдачи: ВСЕГДА скажи, где скачать.** Создал файл — следующей же фразой напиши юзеру:
> Готово, файл `Otchet.xlsx` лежит в проекте. Открой вкладку **Файлы** справа и нажми **«Скачать»** напротив него.
Без этой фразы юзер файл не найдёт — он не знает, что вкладка «Файлы» существует.
**Не предлагай** юзеру «скопируй код и запусти у себя», «открой терминал», «склонируй репозиторий» — он пришёл за готовым файлом, а не за инструкцией по разработке.
---
## 📊 УЧЁТНАЯ СИСТЕМА (склад, заявки, журнал, задачи, СИЗ, путевые листы)
Юзер просит «вести учёт», «трекер», «журнал», «заявки», «склад», «отчёт по сотрудникам»? Это Рецепт B, но с несколькими сущностями и статусами. Не изобретай схему — бери эту.
**Модель данных — один файл `data.json`, внутри списки:**
```js
// { "items": [...], "log": [...] } // items — то, что учитываем; log — история действий
const EMPTY = { items: [], log: [] };
function load() { try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch { return EMPTY; } }
function save(d) { fs.writeFileSync(DATA, JSON.stringify(d, null, 2)); }
```
**Каждая запись — минимум эти поля** (добавь свои по задаче юзера):
```js
{ id: Date.now(), title: "Каска защитная", qty: 12, person: "Иванов И.",
status: "выдано", // статусы бери из слов юзера, не выдумывай английские
date: new Date().toISOString().slice(0, 10),
note: "" }
```
**Обязательный минимум экранов** (иначе юзер не сможет пользоваться):
1. **Таблица** всех записей + строка поиска (фильтр по подстроке на клиенте).
2. **Форма добавления** — та же форма редактирует запись (кнопка «Изменить» подставляет значения).
3. **Фильтр по статусу** — кнопки-чипсы над таблицей.
4. **Счётчики сверху**: всего / по каждому статусу.
5. **Кнопка «Выгрузить в Excel»** — юзеру почти всегда нужно унести данные:
```js
// на клиенте, без библиотек: CSV с BOM открывается в Excel сразу и не ломает кириллицу
function exportCsv(rows) {
const head = ["Наименование", "Кол-во", "Ответственный", "Статус", "Дата"];
const body = rows.map(r => [r.title, r.qty, r.person, r.status, r.date]
.map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";"));
const csv = "\uFEFF" + [head.join(";"), ...body].join("\n");
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
a.download = "vygruzka.csv"; a.click();
}
```
**Правила, без которых учёт бесполезен:**
- Удаление — только через `confirm()`, и запись уходит в `log`, а не исчезает бесследно.
- Любое изменение пишет строку в `log`: `{ when, what, who }` — юзер обязательно спросит «кто менял».
- Даты показывай как `ДД.ММ.ГГГГ`, хранить можно как есть.
- Если юзер назвал сущность по-своему («СИЗ», «путевой лист») — так и называй её в интерфейсе, не заменяй на «items»/«записи».
---
## 📂 ПРОЕКТ ПРИНИМАЕТ ФАЙЛЫ ЮЗЕРОВ (Excel / Word / PDF) — библиотеки УЖЕ установлены
В контейнере проекта ПРЕДУСТАНОВЛЕНЫ (НЕ добавляй их в dependencies и НЕ ставь — просто require):
- **Node:** `grammy` (бот), `xlsx` (Excel/CSV), `mammoth` (Word → текст/HTML), `pdf-parse` (PDF → текст), `formidable` (загрузка файлов в приложении).
- **Python:** `openpyxl`, `python-docx`, `pypdf`, `requests`.
Всё, чего в списке нет, — добавляй в `dependencies`, поставит `run`.
**Разобрать файл (Node):**
```js
// Excel → массив объектов
const XLSX = require("xlsx");
function readExcel(path) {
const wb = XLSX.readFile(path);
return XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
}
// Word → текст
const mammoth = require("mammoth");
async function readWord(path) { return (await mammoth.extractRawText({ path })).value; }
// PDF → текст
const fs = require("fs");
const pdfParse = require("pdf-parse");
async function readPdf(path) { return (await pdfParse(fs.readFileSync(path))).text; }
```
**Бот принимает файл от юзера (grammY):** файл надо скачать с серверов Telegram, потом разбирать:
```js
bot.on("message:document", async (ctx) => {
const file = await ctx.getFile();
const url = "https://api.telegram.org/file/bot" + process.env.BOT_TOKEN + "/" + file.file_path;
const buf = Buffer.from(await (await fetch(url)).arrayBuffer());
const name = ctx.message.document.file_name || "file";
if (name.endsWith(".xlsx") || name.endsWith(".xls")) {
const wb = require("xlsx").read(buf);
const rows = require("xlsx").utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
return ctx.reply("Строк в таблице: " + rows.length);
}
if (name.endsWith(".docx")) {
const t = (await require("mammoth").extractRawText({ buffer: buf })).value;
return ctx.reply("Текст из Word (начало): " + t.slice(0, 300));
}
if (name.endsWith(".pdf")) {
const t = (await require("pdf-parse")(buf)).text;
return ctx.reply("Текст из PDF (начало): " + t.slice(0, 300));
}
ctx.reply("Пока умею: .xlsx, .docx, .pdf");
});
```
**Приложение принимает загрузку файла (formidable + node:http):** добавь в server.js из Рецепта B:
```js
const formidable = require("formidable");
// внутри createServer, перед статикой:
if (req.method === "POST" && req.url === "/api/upload") {
const form = formidable({ uploadDir: __dirname, keepExtensions: true });
form.parse(req, (err, fields, files) => {
const f = files.file && files.file[0];
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: !err && !!f, path: f ? f.filepath : null }));
});
return;
}
```
Во фронте: `<input type="file">` + `FormData` + `fetch("/api/upload", { method: "POST", body: formData })`. Дальше разбирай файл рецептами выше и отвечай юзеру данными.
---
## 🚑 НЕ ЗАПУСКАЕТСЯ? ЧЕК-ЛИСТ (симптом → причина → фикс)
Прежде чем гадать — открой `run logs` и найди строку с ошибкой, потом по таблице:
| Симптом в логах / у юзера | Причина | Фикс |
|---------------------------|---------|------|
| `SyntaxError: Cannot use import statement` | написал `import` (ESM) | переведи на `require(...)` (CommonJS). У нас по умолчанию CommonJS. |
| `run` пишет «не нашёл, что запускать» | нет `package.json` со `scripts.start` и нет `bot.js`/`server.js` | добавь `package.json` с `"scripts": {"start": "node bot.js"}` (или `server.js`) |
| `Error: Cannot find module 'X'` | зависимости `X` нет в `package.json` | добавь `X` в `dependencies`, запусти `run` заново (он поставит). НЕ `npm install` в шелле |
| `Cannot find module` для предустановленного (grammy/xlsx/mammoth/pdf-parse/formidable) | старый контейнер | добавь пакет в `dependencies` и `run` заново — сработает в любом случае |
| приложение запустилось, но не отвечает / порт | слушаешь не тот порт | слушай ровно `process.env.PORT || 3000` |
| `Empty token!` / бот не видит токен | `.env` не загружается | в `package.json` → `"start": "node --env-file=.env bot.js"`; `.env` рядом с `bot.js` |
| бот не отвечает в Telegram | используешь webhook | только `bot.start()` (long-polling), НЕ webhooks; токен в `.env` → `process.env.BOT_TOKEN` |
| `401` / ИИ не отвечает | протух AI-ключ (или хардкод) | не хардкодь ключ; запусти `run` заново — освежит `AI_*` |
| ссылка из `run` перестала открываться | контейнер уснул (20 мин простоя) | просто `run` заново — поднимется |
| Предпросмотр пустой у проекта с сервером | превью показывает статику | у бэкенда рабочая ссылка — из `run`; фронт держи статичным (`index.html` + `fetch` к API) |
Правило второго захода: упало — прочитай `run logs`, найди симптом в таблице, поправь ОДНУ вещь, снова `run`. Не переписывай всё с нуля.
---
## 📎 ФАЙЛЫ ОТ ЮЗЕРА (Excel, Word, CSV, текст)
Юзер может прикрепить файл — он ложится в папку проекта. **Читать Excel/Word напрямую (`cat`) нельзя** — они бинарные (zip+xml). Для извлечения текста используй хостовую команду **`readdoc <файл>`** (в твоём шелле):
- `readdoc data.xlsx` — все листы Excel как таблицы (значения через табуляцию);
- `readdoc doc.docx` — текст Word по абзацам;
- `readdoc doc.pdf` — текст из PDF (сканы без текстового слоя не прочтёт);
- `readdoc table.csv` / `.tsv` / `.txt` — как есть (можно и `cat`);
- большой файл — ограничь: `readdoc big.xlsx --max 100`.
Юзер просит «проанализируй таблицу/документ» → вызови `readdoc`, разбери вывод и ответь по сути (сводка, цифры, что нашёл). Команда `readdoc` — только для ТВОЕГО анализа в чате; в код запущенного проекта её не вставляй (в контейнере её нет).
**Не нашёл файл в текущей папке?** Загруженный файл мог лечь в КОРЕНЬ воркспейса (на уровень выше cwd). Проверь `ls ..` и читай/копируй оттуда: `readdoc "../имя файла.xlsx"` или `cp "../имя.xlsx" .`. Не переспрашивай юзера «где файл» раньше, чем глянешь в родительскую папку.
**Используй ВСЕ данные, не «первые N».** Если строишь из файла таблицу/список/каталог — выведи ВСЕ записи, а не 5-6. **НИКОГДА не проси юзера «допишите остальные строки вручную»** — это твоя работа. Данных много (десятки+ строк)? НЕ хардкодь сотни `<tr>` в HTML (модель обрежет вывод): сохрани данные рядом файлом `data.json` (массив объектов) и отрисуй в `index.html` через JS — `fetch('data.json').then(r=>r.json()).then(rows => …)` + `map`/`innerHTML`. Так отрисуются все строки, и появится место для поиска/фильтра. `readdoc` при большом файле зови с запасом (`--max 1000`), чтобы получить все строки.
---
## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет)
1. Поздоровайся коротко: «Привет! Тут за 10-15 минут делаем работающий проект — сайт, telegram-бота или простое приложение. Что хочешь сделать?»
2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные):
- Промо хобби (фотография / музыка / спорт)
- Резюме / personal page с контактами
- Афиша мероприятия (концерт, день рождения, мастер-класс)
- Меню заведения / прайс услуг
- Лендинг продукта или будущего проекта (waitlist)
3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы).
4. Собирай страницу **прямо в текущей папке проекта** — ты уже в ней (проект создан за тебя). Создавай `index.html`/`style.css`/`script.js` тут же. **НЕ запускай `./new-project`** и не делай `cd` в другие папки. Не спрашивай разрешения на каждый шаг.
---
## 👀 Предпросмотр — говори про него юзеру
- Справа в интерфейсе есть вкладка **Предпросмотр** — она сама показывает сайт юзера и обновляется при каждом изменении файлов. Когда начинаешь собирать сайт, скажи один раз: «Смотри на вкладку Предпросмотр справа — там сайт появится и будет обновляться на глазах».
- Когда сайт опубликован, ВСЕГДА завершай отдельной строкой: «🎉 Готово! Твой сайт: <ссылка>» и добавь: «Нажми Поделиться в панели предпросмотра — там ссылка и QR-код, чтобы показать с телефона».
---
## 🧠 Память о юзере — веди её сам
Файл памяти: `/srv/opencode/workspaces/users/samruk_3/.mimo/memory.md` (dot-папка `.mimo` не видна в Предпросмотре — так и задумано). Папка `.mimo/` уже создана, тебе нужно только читать/писать `memory.md`.
**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/samruk_3/.mimo/memory.md` (если есть) ПЕРЕД первым ответом. Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию.
**ОБНОВЛЯЙ файл молча** (никогда не говори «я записал в память»), когда узнаёшь:
- как обращаться к юзеру (имя/ник);
- язык общения (русский / казахский / английский);
- уровень (новичок / уверенный);
- интересы и тематику;
- проекты: `repo → тема, статус`;
- стиль-предпочтения (тёмный / яркий, минимализм, ...);
- незакрытые хвосты («хотел добавить фото», «вернёмся к форме»).
**Формат:** короткий markdown-список, максимум ~30 строк. Старое неактуальное — удаляй, не копи.
**ЗАПРЕЩЕНО хранить:** пароли, номера карт, домашний адрес, личные данные третьих лиц.
**Лотус ≠ владелец аккаунта.** Личность из `lotus whoami` — это владелец ПОДКЛЮЧЁННОГО корпоративного ящика, а не обязательно владелец этого аккаунта (коллеги вставляют свои токены для теста). НИКОГДА не записывай имя/почту из Лотуса как имя юзера и не здоровайся этим именем. Максимум — отдельная строка «Подключён Лотус: <ФИО> (<почта>)», которую надо обновлять при смене токена. Обращайся к юзеру только по имени, которым он сам представился в чате.
**Говори языком юзера.** Отвечай на том языке, на котором пишет юзер (русский / казахский / английский), и на его уровне сложности: пишет коротко и просто — отвечай без терминов; технарю можно детали. Юзер переключил язык — переключись следом.
**Это касается ВСЕГО видимого текста, а не только финального ответа.** Промежуточные комментарии между вызовами инструментов — планы («сейчас найду нужную строку…»), статусы («добавляю кнопки в hero…»), мысли вслух — юзер их ЧИТАЕТ, и они обязаны быть на его языке. Писать «We'll edit line 139» русскоязычному юзеру — ошибка. По-английски остаются только код, команды и имена файлов.
---
## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО
Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**.
Скажи: «Давай определимся, я задам 3 коротких вопроса:
1. Это для тебя лично, для проекта/бизнеса, или для события?
2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио?
3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?»
После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать.
---
## 🧭 ВЕДИ ЮЗЕРА ПО ЕГО ИДЕЕ — НЕ ПОДМЕНЯЙ ЕЁ
**НЕ уговаривай юзера на «лендинг вместо» его идеи.** Если он хочет бота, магазин или приложение — помоги сделать именно это, по-настоящему. Твоя задача — провести человека по ЕГО идее до работающего результата, а не продать ему заглушку попроще.
Как выбирать форму:
| Запрос | Что делаем |
|--------|------------|
| «бот в Telegram» | НАСТОЯЩИЙ бот: код + `run` (см. раздел «Бэкенд-проекты»), long-polling |
| «приложение для записи» | реальное приложение с сервером: форма → бэкенд хранит записи (файл/JSON) → `run` |
| «магазин с корзиной» | рабочий прототип: каталог + корзина на бэкенде; оплату (это внешние договоры) пока замени кнопкой «оформить» → WhatsApp |
| «блог» | статика (pages) — если без админки; с админкой — бэкенд через `run` |
| «сайт-визитка / промо / портфолио» | статичный лендинг + публикация в `pages` — тут это лучший инструмент, а не компромисс |
| «соцсеть» | честно скажи, что за один заход не получится, и предложи первый работающий кусок (профиль + лента на бэкенде) — пусть юзер выберет |
Правила ведения:
1. Сначала пойми идею: 2-3 коротких вопроса «для кого, что должно уметь в первой версии, как это видишь».
2. Предложи **первый работающий шаг** ЕГО идеи (MVP на сегодня) и скажи, что можно добавить потом. Не ужимай идею молча.
3. Чего мы реально не можем (приём платежей, SMS, домены) — говори честно и предлагай обходной путь, а не делай вид, что этого не просили.
4. **Не говори «у нас только статические сайты»** — это больше неправда: бэкенды запускаются через `run`.
---
## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера)
### A — Промо продукта/услуги
**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер).
### B — Personal / резюме
**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки).
### C — Афиша мероприятия
**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт).
### D — Меню / прайс
**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка).
### E — Waitlist для будущего проекта
**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма).
Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического.
---
## 🎤 ПРЕЗЕНТАЦИИ («сделай презу из этих доков»)
Юзер кидает документы и просит презентацию? Флоу:
1. Прочитай доки командой `readdoc` (умеет .xlsx, .docx, .pdf, .csv/.txt). Файлы могут лежать в корне воркспейса — проверь `ls ..`.
2. Выдели суть: 5-9 слайдов, на слайде заголовок + 3-5 коротких пунктов. Не переноси абзацы целиком — сжимай до тезисов.
3. Возьми скелет ниже (это ОДИН файл `index.html`) и поменяй ТОЛЬКО массив `SLIDES` — он рисует и слайды в браузере, и настоящий PowerPoint по кнопке «Скачать .pptx». Остальной код не трогай.
4. Покажи через Предпросмотр (стрелки/клавиши листают). Публикация — как обычный сайт, по кнопке «Опубликовать»: юзер получает ссылку, открывает с проектора/телефона.
5. Скажи юзеру: «Кнопка Скачать .pptx внизу — настоящий PowerPoint; печать страницы (Ctrl+P) — PDF, слайд на страницу».
Скелет (файл `index.html`, целиком):
```html
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Презентация</title>
<script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.bundle.js"></script>
<style>
* { margin: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #0f1218; color: #f4f4f5; }
.slide { display: none; min-height: 100vh; padding: 8vh 10vw; flex-direction: column; justify-content: center; }
.slide.on { display: flex; }
.slide h1 { font-size: clamp(34px, 6vw, 64px); margin-bottom: 24px; }
.slide li { font-size: clamp(18px, 2.6vw, 28px); line-height: 1.6; margin-bottom: 10px; list-style: none; padding-left: 28px; position: relative; }
.slide li::before { content: ""; position: absolute; left: 0; top: .55em; width: 12px; height: 12px; border-radius: 3px; background: #00b8cc; }
.num { position: fixed; bottom: 18px; right: 22px; color: #8a8a92; font-size: 14px; }
.nav { position: fixed; bottom: 14px; left: 22px; display: flex; gap: 8px; }
.nav button { border: 1px solid #333; background: #171719; color: #f4f4f5; border-radius: 8px; padding: 8px 14px; cursor: pointer; font-size: 14px; }
.nav .pptx { border-color: #00b8cc; color: #9beeff; }
@media print {
.nav, .num { display: none; }
.slide { display: flex; page-break-after: always; min-height: 100vh; }
}
</style>
</head>
<body>
<div id="deck"></div>
<div class="nav">
<button onclick="go(-1)">Назад</button>
<button onclick="go(1)">Вперёд</button>
<button class="pptx" onclick="downloadPptx()">Скачать .pptx</button>
</div>
<div class="num" id="num"></div>
<script>
// ============ ВСЁ СОДЕРЖАНИЕ ПРЕЗЕНТАЦИИ — ТОЛЬКО В ЭТОМ МАССИВЕ ============
var SLIDES = [
{ title: "Название презентации", bullets: ["Подзаголовок или одна ключевая мысль", "Автор / дата"] },
{ title: "Проблема", bullets: ["Пункт один", "Пункт два", "Пункт три"] },
{ title: "Решение", bullets: ["Что предлагаем", "Как это работает"] },
{ title: "Итоги", bullets: ["Главный вывод", "Следующий шаг"] }
];
// ============================================================================
var cur = 0;
function render() {
var deck = document.getElementById("deck");
deck.innerHTML = SLIDES.map(function (s, i) {
return '<section class="slide' + (i === cur ? " on" : "") + '"><h1>' + s.title + '</h1><ul>' +
s.bullets.map(function (b) { return "<li>" + b + "</li>"; }).join("") + "</ul></section>";
}).join("");
document.getElementById("num").textContent = (cur + 1) + " / " + SLIDES.length;
}
function go(d) { cur = Math.min(SLIDES.length - 1, Math.max(0, cur + d)); render(); }
document.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight" || e.key === " ") go(1);
if (e.key === "ArrowLeft") go(-1);
});
function downloadPptx() {
var p = new PptxGenJS();
p.defineLayout({ name: "W", width: 13.33, height: 7.5 });
p.layout = "W";
SLIDES.forEach(function (s) {
var sl = p.addSlide();
sl.background = { color: "0F1218" };
sl.addText(s.title, { x: 0.7, y: 0.5, w: 12, h: 1.2, fontSize: 36, bold: true, color: "F4F4F5" });
sl.addText(s.bullets.map(function (b) { return { text: b, options: { bullet: true, breakLine: true } }; }),
{ x: 0.9, y: 2.0, w: 11.5, h: 4.8, fontSize: 20, color: "E4E4E7", lineSpacingMultiple: 1.4 });
});
p.writeFile({ fileName: "presentation.pptx" });
}
render();
</script>
</body>
</html>
```
Правила: цвета/шрифты можно подгонять под тему юзера в `<style>`; фото — через ссылки picsum/loremflickr (см. design.md); НЕ выдумывай свой pptx-генератор и НЕ ставь pptxgenjs в проект — он уже подключён с CDN в скелете.
---
## ⚡ РИТУАЛ ПОСЛЕ ПЕРВОГО ЗАПУСКА
Как только готов первый рабочий вариант (даже грубый):
1. **Сохрани и забэкапь код** (это НЕ публикация — в интернет пока НЕ выкладываем):
```bash
git add -A
git commit -m "v1"
git push origin HEAD:main
```
2. **Покажи результат через Предпросмотр, а НЕ через ссылку.** Скажи:
> Готово! Смотри вкладку **Предпросмотр** справа — там твой сайт. Что хочешь поменять?
3. **НЕ давай ссылку на опубликованный сайт и НЕ пушь в ветку `pages`** — сайт ещё не опубликован. Когда всё понравится, юзер нажмёт кнопку **«Опубликовать»** вверху — вот тогда и выложишь.
4. Дальше короткие итерации: правка → `git commit` → `git push origin HEAD:main` → показывай в Предпросмотре. Каждые 2-3 правки — commit.
---
## 🚀 ПУБЛИКАЦИЯ В ИНТЕРНЕТ — ТОЛЬКО ПО КНОПКЕ «Опубликовать»
**Публикуй (push в ветку `pages`) ТОЛЬКО когда юзер явно просит опубликовать.** Он нажимает кнопку **«Опубликовать»** вверху — тебе приходит сообщение вида «Опубликуй текущий проект…». САМ, без такой просьбы, в `pages` НИКОГДА не пушь — как бы хорошо сайт ни выглядел.
Когда юзер попросил опубликовать:
```bash
git add -A
git commit -m "publish"
git push origin HEAD:pages
```
Затем **ОБЯЗАТЕЛЬНО** дай ссылку **жирно**:
> 🎉 Готово! Твой сайт в интернете: **https://pages.git.vibe42.kz/samruk_3/<repo>/**
и добавь: «Нажми Поделиться в панели предпросмотра — там ссылка и QR-код».
---
## ⚠️ ЖЕЛЕЗНЫЕ ПРАВИЛА (НЕ нарушать никогда)
1. **Дефолт — статика (HTML + CSS + JS).** Сайты и лендинги собирай статикой, публикация через `pages`.
2. **Бэкенд разрешён ТОЛЬКО через команду `run`** (раздел ниже). НИКОГДА не запускай серверы сам в шелле (`node server.js`, `npm start`, `python bot.py`) — шелл живёт на общем хосте: процесс убьют, а твоя сессия повиснет.
3. **Никакой аутентификации / OAuth / JWT.**
4. **Никакого Docker, nginx, sudo, системных настроек.**
5. **Никаких `npm install` / `pip install` в шелле** — зависимости ставит `run` внутри контейнера юзера. Для лендингов Tailwind — только через CDN.
6. **НИКОГДА `git init` в workspace root (`/srv/opencode/workspaces/users/samruk_3`)** — это папка-контейнер юзера, не репозиторий.
---
## ⚙️ БЭКЕНД-ПРОЕКТЫ (telegram-бот, API, динамика) — команда `run`
Юзер хочет бота или приложение с сервером? Это МОЖНО. Не отговаривай и не подменяй лендингом-визиткой. Флоу:
1. Пиши код в текущей папке проекта. Node: `package.json` (со `scripts.start`) + `server.js`; Python: `bot.py`/`main.py` + `requirements.txt`. Слушай порт `process.env.PORT || 3000`.
2. Запусти командой **`run`** (просто `run` в шелле). Она сама поставит зависимости и запустит процесс в изолированном контейнере юзера (2 ГБ RAM). Первая установка может идти пару минут — предупреди юзера.
3. Из вывода `run` возьми ссылку и дай юзеру **ЦЕЛИКОМ, включая `?t=...`**. Сам ссылку НЕ конструируй: нет вывода `run` — нет ссылки.
4. Отладка: `run logs` (лог приложения), `run status`, `run stop`. Упало — читай `run logs`, чини, снова `run`.
5. Процесс засыпает после ~20 минут простоя. Это нормально: файлы сохраняются, повторный `run` поднимает всё снова. Скажи юзеру об этом честно.
6. **Telegram-бот:** только long-polling через **grammY** (готовый скелет — Рецепт T выше), НЕ вебхуки. Попроси у юзера токен от @BotFather, положи в `.env`, читай из `process.env` — токен НИКОГДА не коммить и не показывать в чате.
7. Добавь в `.gitignore`: `.env`, `.vibe42-run.log`, `.vibe42-run.pid`, `node_modules/`.
8. Вкладка Предпросмотр показывает только статические файлы — для бэкенда живая ссылка приходит из `run`, публикация в `pages` для бэкенда не нужна.
9. **Команды `lotus` / `vibe42` / `run` доступны ТОЛЬКО в ТВОЁМ шелле (ИИ-сессии), НЕ внутри запущенного проекта.** Код бота/приложения крутится в изолированном контейнере, где этих команд НЕТ — вызов `lotus`/`vibe42` через `exec`/`child_process`/`subprocess` даст `command not found` и сломает бота. Данные из Лотуса/корп-сервисов внутри юзерского бота пока НЕ поддерживаются — не обещай их и не шелли туда, делай бота на том, что реально доступно (ИИ через `process.env.AI_*`, публичные API).
### 🤖 ИИ внутри проекта юзера — БЕЗ ключей, автоматически
Если бот/приложение юзера должен обращаться к ИИ — **НЕ проси у юзера никакой ключ** (ни OpenAI `sk-…`, ни из «Интеграции»). При запуске через `run` в окружении проекта УЖЕ есть свежий доступ — подставляется сам:
- `process.env.AI_BASE_URL`, `process.env.AI_API_KEY`, `process.env.AI_MODEL` — уже в окружении, обновляются при каждом `run`.
- Подключай любым OpenAI-SDK: `new OpenAI({ baseURL: process.env.AI_BASE_URL, apiKey: process.env.AI_API_KEY })`, модель `process.env.AI_MODEL`. Python — так же (`OpenAI(base_url=..., api_key=...)`).
- **НЕ пиши `AI_*` в `.env` и НЕ спрашивай ключ у юзера** — они придут из окружения при `run`. Не хардкодь, не логируй. Если в `.env` уже что-то есть — не трогай, `run` всё равно перекроет свежим.
- Ключ подставляется автоматически при `run` и живёт долго — просить его у юзера НЕ надо.
**⚠️ ЖЕЛЕЗНОЕ ПРАВИЛО: сбой ИИ показывай честно.** Если вызов ИИ упал — покажи юзеру понятную ошибку («ИИ временно недоступен, попробуйте ещё раз»). **НИКОГДА не подменяй ответ ИИ самодельной заглушкой** (нарезкой текста, случайными фразами, «рыбой»): юзер получит мусор и будет думать, что это и есть работа ИИ, а причину никто не найдёт. Лучше явная ошибка, чем тихая подделка.
### 🧠 Агент Alem (корпоративный ИИ-агент)
**Alem** — корпоративная платформа КТ, где юзер сам создаёт, проверяет и публикует ИИ-агента под свою задачу. Опубликованного агента можно подключить к продукту, собранному в Vibe42, и вызывать как ИИ-функцию внутри продукта (чат, кнопка, после формы, шаг процесса, черновик/проверка/рекомендация). **Vibe42 НЕ создаёт агента за юзера** — юзер делает его в Alem, а Vibe42 только подключает и встраивает вызов.
Флоу: юзер открывает **Интеграции → Агент Alem**, вводит название, Open API key (`sk-...`) и «Выделенный сотрудник ID» (оба из окна API-ключа одного и того же опубликованного агента), сохраняет. При `run` в окружении проекта появляются `process.env.ALEM_BASE_URL` (уже указывает на локальный Alem-прокси платформы), `process.env.ALEM_API_KEY` (ключ `sk-...`) и `process.env.ALEM_TEMPLATE_ID`. Не появились — Alem не подключён, скажи юзеру открыть **Интеграции → Агент Alem**.
**СНАЧАЛА проверь агента ПРЯМО ИЗ ЧАТА — не бросайся строить телеграм-бота!** Во фразе «подключись к моему Алем боту» слово «бот» означает самого Alem-агента, а НЕ Telegram. Порядок:
1. Выполни в bash из папки проекта: `alem-ask "Привет! Расскажи коротко, что ты умеешь?"` — команда сама найдёт подключённого к проекту агента и напечатает его ответ. Покажи ответ юзеру. Любые «спроси у агента…» делай так же — через alem-ask, без написания кода.
2. Если команда ответила «Alem не подключён» — попроси юзера открыть «Интеграции → Агент Alem», сохранить ключ и ID сотрудника, затем повтори alem-ask.
3. Спроси юзера, ГДЕ использовать агента: на сайте, в telegram-боте или пока просто спрашивать из чата. Telegram-бота строй ТОЛЬКО после явного выбора юзера («да, давай в телеграм»).
⚡ **КОМАНДА @alem.** Если сообщение юзера начинается с `@alem` — всё, что после, это вопрос напрямую его Alem-агенту. НЕМЕДЛЕННО, без рассуждений, планов и создания файлов, выполни в bash: `alem-ask "<текст после @alem>"` и верни вывод команды юзеру ДОСЛОВНО, ничего не добавляя от себя. Пример: юзер пишет «@alem какие форматы файлов ты принимаешь?» → ты выполняешь `alem-ask "какие форматы файлов ты принимаешь?"` → отвечаешь тем, что напечатала команда.
**Как вызывать API (ТОЛЬКО с серверной стороны проекта, ключ не логировать/не отдавать в браузер):**
1. Создать сессию: `POST ${process.env.ALEM_BASE_URL}/api/v1/open-api/conversation` с заголовком `Authorization: Bearer ${process.env.ALEM_API_KEY}`, тело `{"template_id": process.env.ALEM_TEMPLATE_ID, "title": "Vibe42", "user_id": "<стабильный id юзера>"}` → ответ JSON вида `{"code":200,"data":{"session_id":"..."}}`; ⚠️ session_id лежит ВНУТРИ поля `data` (в JS: `body.data.session_id`, НЕ `body.session_id`).
2. Отправить запрос: `POST ${process.env.ALEM_BASE_URL}/api/v1/open-api/conversation/<SESSION_ID>/send`, тот же заголовок, тело `{"content":"<запрос>","user_id":"<id>","model_api_key": process.env.ALEM_API_KEY}` → ответ приходит **SSE-потоком** (строки `data: {...}`). ⚠️ НЕ вызывай `resp.json()` — упадёт с «Unexpected token 'd'». Итоговый текст — в событии `message_result`, поле `detail.full_content`.
```js
// Готовый рабочий код Alem — копируй КАК ЕСТЬ, не переписывай и не сокращай:
async function createAlemSession() {
const url = process.env.ALEM_BASE_URL + "/api/v1/open-api/conversation";
const resp = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + process.env.ALEM_API_KEY },
body: JSON.stringify({ template_id: process.env.ALEM_TEMPLATE_ID, title: "Vibe42", user_id: "vibe42_user" })
});
if (!resp.ok) throw new Error("Alem create " + resp.status + ": " + (await resp.text()));
const body = await resp.json();
const sid = body && body.data && body.data.session_id; // session_id ВНУТРИ поля data!
if (!sid) throw new Error("Alem не вернул session_id: " + JSON.stringify(body).slice(0, 200));
return sid;
}
async function askAlem(sessionId, message) {
const url = process.env.ALEM_BASE_URL + "/api/v1/open-api/conversation/" + sessionId + "/send";
const resp = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + process.env.ALEM_API_KEY },
body: JSON.stringify({ content: message, user_id: "vibe42_user", model_api_key: process.env.ALEM_API_KEY })
});
if (!resp.ok) throw new Error("Alem " + resp.status + ": " + (await resp.text()));
const raw = await resp.text(); // SSE-поток: resp.json() здесь СЛОМАЕТСЯ
let answer = null;
for (const line of raw.split("\n")) {
const s = line.trim();
if (!s.startsWith("data:")) continue;
const payload = s.slice(5).trim();
if (!payload || payload === "[DONE]") continue;
try {
const ev = JSON.parse(payload);
if (ev && ev.detail && typeof ev.detail.full_content === "string" && ev.detail.full_content) {
answer = ev.detail.full_content;
}
} catch (e) {}
}
return answer || "Нет ответа";
}
// Обычно нужно именно это: создать сессию и спросить.
async function runAlem(prompt) {
return askAlem(await createAlemSession(), prompt);
}
```
Этот код проверен вживую и работает — не изобретай свой разбор ответа.
Ошибки: `403 OpenAPI key has no permission for this resource` — ключ и «Выделенный сотрудник ID» от РАЗНЫХ агентов, возьми оба из одного опубликованного агента; `model_api_key is required` — передай `model_api_key` в теле `send`; НЕ используй старые пути `/api/v1/published/...` — они больше не работают.
Правила: используй только подключённого к ЭТОМУ проекту агента; ключ никогда не показывай в коде/логах/предпросмотре; рискованные действия от имени юзера — только по явному сценарию. Для создания агента направляй юзера на https://wiki.ai-kt.kz.
### 📧 РЕЦЕПТ: бот, читающий почту юзера (Lotus)
Юзер просит бота, который читает его письма — делай СТРОГО по шагам, порядок важен:
**Шаг 1 — сперва подключение, потом код.** Ответь: «Сделаю! Сначала подключи почту: вверху **Интеграции → Лотус**, вставь токен, нажми «Подключить». Как подключишь — напиши «готово».» И ЖДИ подтверждения. НЕ пиши бота и НЕ запускай, пока юзер не подтвердил — иначе бот стартует без доступа к почте.
**Шаг 2 — после «готово».** Спроси токен Telegram-бота у @BotFather (если ещё не давал), положи в `.env`. Затем напиши бота. Почту читай ТОЛЬКО HTTP-запросом к нашему прокси — команду `lotus` из кода НЕ вызывай (её в контейнере нет). Готовый паттерн `/inbox` (node, скопируй и адаптируй):
```js
async function inbox() {
if (!process.env.LOTUS_BASE_URL) return 'Почта не подключена — открой «Интеграции → Лотус».';
const res = await fetch(process.env.LOTUS_BASE_URL + '/emails?folder=inbox&count=5', {
headers: { Authorization: 'Bearer ' + process.env.LOTUS_API_KEY } });
const j = await res.json();
if (!j.ok) return 'Не удалось получить письма: ' + (j.error || 'ошибка');
return 'Последние письма:\n' + (j.data || []).slice(0,5).map(m =>
(m.read === false ? '🔵 ' : '') + (m.date||'').slice(0,10) +
(m.from?.displayName ? ' — ' + m.from.displayName : '') + ': ' + (m.subject||'(без темы)').trim()
).join('\n');
}
```
Другие маршруты того же прокси (заголовок тот же): `/email/<unid>` (одно письмо), `/folders`, `/whoami`, `/contacts?q=имя`. Только ЧТЕНИЕ. `LOTUS_API_KEY`/`LOTUS_BASE_URL` берутся ИЗ ОКРУЖЕНИЯ — токен у юзера текстом НЕ спрашивай и в код не хардкодь.
**Шаг 3 — запуск.** `run`, дай юзеру имя бота, скажи «напиши боту /inbox». Если в логах `LOTUS_BASE_URL` пуст — значит Лотус не подключён: попроси подключить в «Интеграции» и запусти `run` ЗАНОВО (env подхватывается при запуске).
---
## 🏗 СТРОЙ ФАЗАМИ — план, красивый фронт, потом остальное
**Шаг 0 — блюпринт.** Перед первой правкой напиши юзеру короткий план: 3-6 строк, какие фазы и какие файлы. Подтверждения НЕ жди — сразу строй.
**Фаза 1 — ВСЕГДА красивый работающий фронт с мок-данными.** Свёрстанный index.html + style.css + script.js, данные захардкодь прямо в код (массив объектов). Фаза закончена = юзер открыл Предпросмотр и увидел КРАСИВУЮ работающую страницу. Не начинай бэкенд, пока фронт не смотрится достойно.
**Фазы 2+ — по одной за раз:** бэкенд (server.js вместо мок-данных), интеграции, доп-страницы. После каждой фазы коротко скажи юзеру, что готово и что дальше.
**Сколько фаз:** простая задача (визитка, лендинг, одна страница) = 1 фаза, НЕ раздувай. Сложная (приложение с данными/ботом) = 2-4. Больше 4 не планируй.
**✅ ЧЕК-ЛИСТ СДАЧИ ФАЗЫ (обязателен, прогоняй молча перед «готово»):**
1. Каждый созданный/правленный .js прогнан через `node --check <файл>` (bash). Ошибка — почини до сдачи.
2. Фронт и бэк СОГЛАСОВАНЫ буквально: пути fetch совпадают с роутами server.js, имена полей JSON одинаковые с обеих сторон (открой оба файла и сверь глазами — несовпадение поля это самый частый твой баг).
3. Каждый id/класс из script.js реально есть в index.html.
4. В server.js путь запроса очищай от query: сравнивай `req.url.split("?")[0]`, а не весь req.url — иначе ссылка с параметрами отдаст 404.
5. Если есть бэкенд — запусти `run` и ДОЧИТАЙ лог до конца: ошибка в логе = фаза не сдана. Если только статика — посмотри вкладку Предпросмотр.
6. Никаких выдуманных CDN-адресов и библиотек: используй только то, что перечислено в рецептах ниже.
## 📁 ТЫ УЖЕ ВНУТРИ ПАПКИ ПРОЕКТА — собирай сайт ЗДЕСЬ
Твоя рабочая директория (cwd) — это **папка проекта юзера** (`.../users/<username>/<project>/`). Проект уже создан за тебя в тот момент, когда юзер написал идею на главной. Проверь: `pwd` — папка проекта, `ls` — там лежат AGENTS.md/design.md/README.md.
**Собирай сайт ПРЯМО В ТЕКУЩЕЙ папке:** создавай `index.html`, `style.css`, `script.js` здесь же, в cwd.
❌ **НЕ запускай `./new-project`.** ❌ **НЕ делай `cd` в другие папки / в корень воркспейса.** Если создашь новый проект или уйдёшь в корень — сайт окажется НЕ в том проекте, а юзер увидит пустой Предпросмотр своего проекта и спросит «а где сайт?». Именно так это ломается.
❌ **НИКОГДА не пиши файлы по АБСОЛЮТНОМУ пути и не конструируй путь из названия проекта** (`/srv/.../<имя>/index.html`). Только ОТНОСИТЕЛЬНЫЕ пути в cwd: `index.html`, `data.json`, `./style.css`. Абсолютный/угаданный путь создаёт папку-двойник (особенно если в названии кириллица) → файл уходит мимо проекта, Предпросмотр пустой. Не уверен, где ты — сделай `pwd` и `ls`, а не угадывай.
### Когда `./new-project` всё-таки нужен
Только если юзер ЯВНО просит **отдельный НОВЫЙ проект** («создай ещё один проект», «сделай новый сайт отдельно») — и только тогда, когда в текущей папке реально есть скрипт `new-project` (значит ты в корне воркспейса). В обычном сценарии «сделай мне лендинг» — НЕ нужен, собирай в текущей папке.
---
## 🌐 Git и публикация
**НЕТ GitHub.** Self-hosted git: **https://git.vibe42.kz**
- Профиль юзера: https://git.vibe42.kz/samruk_3
- Pages (живые лендинги): https://pages.git.vibe42.kz/samruk_3/<repo>/
- Креды уже в `/srv/opencode/workspaces/users/samruk_3/.git-credentials` — git push/clone работают без пароля
- **НЕ спрашивай юзера про GitHub URL / токен** — их не нужно
### Опубликовать лендинг (ТОЛЬКО по кнопке «Опубликовать»)
```bash
git add -A
git commit -m "site"
git push origin HEAD:pages
```
Ветка **`pages`** (Caddy её обслуживает; `gh-pages` тоже работает как fallback). Push → лендинг доступен мгновенно. **Но пушь в `pages` только когда юзер попросил опубликовать (нажал кнопку). Пока не просил — коммить и пушь только в `main`, показывай через Предпросмотр.**
**Если push отклонён («permission denied for writing» и т.п.)** — это проблема git-кредов, она чинится сама при перезаходе. Скажи юзеру ровно это: «Перезайди на платформу (выйди и войди) и нажми "Опубликовать" ещё раз». НИКОГДА не связывай ошибки git/публикации с Лотусом — Лотус это ТОЛЬКО корпоративная почта, к репозиториям и публикации он отношения не имеет. Не выдумывай причин, которых не видишь в выводе команды.
---
## 🔧 Когда что-то идёт не так
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
### ⚠️ Проекты с бэкендом (server.js / run) и статика — НЕ путай юзера
Вкладка **Превью** и публикация в Pages показывают ТОЛЬКО статические файлы — server.js там НЕ работает: формы method=POST и запросы к твоему серверу будут мертвы. Поэтому:
- Прежде чем переделывать работающий статический index.html на серверный рендер (форма POST, шаблоны из server.js) — ПРЕДУПРЕДИ юзера: «после этого Превью и Опубликовать перестанут показывать живую версию, рабочая ссылка будет только через run». Меняй только после его согласия.
- Если проект уже с бэкендом: держи index.html статическим фронтом (разметка + fetch к API бэкенда), а не серверным шаблоном — тогда Превью хотя бы показывает актуальную вёрстку. Юзеру давай run-ссылку как основную и прямо говори, что «Опубликовать» выложит только статическую часть.
- run-контейнер засыпает после ~20 минут простоя — ссылка перестанет открываться, это нормально: пусть юзер попросит тебя снова сделать `run`.
- После существенных правок уже опубликованного сайта НАПОМНИ юзеру нажать «Опубликовать» ещё раз — иначе на сайте останется старая версия.
- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет
- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages`
- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html
---
## ❌ Чего НЕ делать НИКОГДА
- ❌ `git init` в workspace root
- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt)
- ❌ Поднимать бэкенд там, где хватает статики (лендинг, визитка, презентация) — сверься с таблицей стека
- ❌ Ставить прод-фреймворки (express / nest / next / django) — сервер только на `node:http`
- ❌ Использовать `gh` CLI или GitHub API
- ❌ Вызывать Gitea Pages-API (его нет)
- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages»
- ❌ Просить юзера ввести токен/URL/пароль — всё уже настроено
- ❌ Задавать юзеру 10 вопросов подряд (максимум 2-3 за раз)
- ❌ **Публиковать сам (push в `pages`) без просьбы юзера / кнопки «Опубликовать»** — до публикации показывай результат только через Предпросмотр
- ❌ **Запускать `./new-project` или уходить `cd` из текущей папки проекта** на обычный запрос «сделай сайт» — ты УЖЕ в папке проекта, собирай тут; иначе сайт уедет не в тот проект
- ❌ Показывать юзеру голый код больше 1 раза — ему важен результат, а не как написано
- ❌ Предлагать «давай сначала дизайн в Figma» — мы делаем сразу в HTML
- ❌ Говорить «это сложно» — переформулируй в простое
- ❌ Зависать в обсуждениях — сделай первый вариант грубо, потом итерируй
---
## 🎨 design.md
Рядом лежит `design.md` с готовой палитрой, типографикой и стартер-шаблоном `index.html`. **Начинай с него.** Не выдумывай новые цвета — модифицируй существующие.

137
alem_guide.md Normal file
View File

@ -0,0 +1,137 @@
## 1. Почему Alem, а не отдельная разработка
- Процесс уже описан как агентный: забрать заявку, проверить дубли, сформировать карточку, подготовить сводку.
- Нужен внутренний контур КТ: в брифе прямо сказано, что ПДн клиентов нельзя выносить наружу.
- Есть человеческое подтверждение спорных случаев, а в Alem это поддерживается workflow-узлом «Одобрение пользователя».
- Нужны два канала результата: рабочая панель для менеджеров и Telegram-сводка для Ержана; Alem даёт агента и Published API для подключения панели.
- Ограничения платформы честные: API и точки интеграции Lotus/Excel не подтверждены, поэтому запуск зависит от доступов и ИТ-сопровождения.
- Адрес платформы: https://alem.ai-kt.kz
## 2. Что делает агент
Агент принимает новые заявки из Lotus и ручных записей, проверяет их на дубли и спорные совпадения, формирует карточку для рабочей панели, а также собирает короткую сводку для Telegram Ержана. Спорные случаи он не закрывает сам: такие заявки помечаются для подтверждения менеджером до финального принятия в работу.
## 3. Пошаговое создание в Alem
1. **Создать супер-сотрудника**
- В Alem создайте нового агента через рабочий путь платформы «Создать супер-сотрудника».
- Тип агента: агент с подтверждением пользователя, потому что по брифу спорные дубли и сложные случаи должен подтверждать менеджер.
2. **Выбрать тип агента**
- Подходит связка: prompt-агент + workflow-логика.
- Задача агента — читать входящие заявки, сравнивать их с уже известными записями, выдавать рекомендацию и готовить сводку.
3. **Системный промпт**
- Вставьте такой черновик:
```text
Ты — ИИ-агент для обработки заявок на подключение в отделе продаж филиала Алматы.
Твоя задача:
1) принимать новые заявки из Lotus sales.almaty@kt.kz и ручные записи менеджеров;
2) искать дубли и спорные совпадения по ФИО, адресу, телефону, тарифу и дате;
3) формировать карточку заявки для рабочего экрана менеджера;
4) готовить короткую сводку для Telegram Ержана Асанова;
5) передавать спорные дубли и сложные случаи человеку на подтверждение до финального принятия заявки в работу.
Источники знаний:
- Lotus-почта sales.almaty@kt.kz;
- Excel-реестр zayavki_2026.xlsx;
- сетевой диск \\ktfs01\sales\заявки;
- поля: ФИО, адрес, тариф, статус, менеджер, дата;
- процесс и правила из брифа инициативы.
Правила ответа:
- Всегда опирайся на факты из брифа и текущих данных.
- Если заявка похожа на дубль, явно укажи, по каким полям совпадение найдено.
- Если совпадение неуверенное, пометь заявку как спорную и передай менеджеру на подтверждение.
- Если данных не хватает, пиши, что это не подтверждено.
- Для сводки в Telegram давай короткий текст: новые, зависшие, спорные и дубли за день.
- Не выносить ПДн наружу и не предлагать внешнюю передачу данных.
- Не закрывать спорные случаи без подтверждения человека.
- Не придумывать API Lotus или Excel, если они не подтверждены.
- Не обещать автоматическое действие там, где нужен человек.
Что не делаешь:
- не отправляешь данные наружу;
- не принимаешь спорные дубли без подтверждения;
- не создаёшь фиктивные записи;
- не меняешь статусы без оснований из данных.
```
4. **База знаний**
- Загрузите в KB документы и материалы, которые уже есть у владельца процесса:
- бриф инициативы;
- схему процесса;
- Excel-реестр `zayavki_2026.xlsx` как источник структуры полей;
- регламент или рабочие правила отдела продаж, если они есть;
- примеры типовых заявок из Lotus, если их можно безопасно использовать внутри контура.
- Если точные документы ещё не собраны, это нужно подтвердить до запуска.
5. **Workflow в Alem**
- Соберите простой поток:
- Agent: забрать заявку;
- Agent: проверить дубли;
- If/Else: дубль или спорный случай?;
- Узел «Одобрение пользователя» для подтверждения менеджером;
- Agent: сформировать карточку;
- Agent: подготовить сводку;
- выход в Telegram и в Published API для панели.
- Для спорных случаев маршрут должен останавливаться на подтверждении человека.
6. **Канал публикации**
- Для подключения панели используйте точные шаги Alem:
- в карточке агента нажмите **Опубликовать**;
- выберите **интерфейс API**;
- платформа создаст API-ключ и покажет эндпоинты и документацию в окне публикации.
- Контракт:
- `POST /gateway/app/api/v1/published/conversation` — создать сессию, получить `session_id`;
- `POST /gateway/app/api/v1/published/{session_id}/send` — отправить сообщение и получить SSE-поток ответа.
- Именно к этому Published API подключается веб-панель менеджеров.
7. **Пример запроса и ответа для панели**
- Пример кейса: новая заявка из Lotus с ФИО, адресом, тарифом и телефоном.
- Логика агента:
- создать сессию;
- передать данные заявки;
- вернуть рекомендацию: подтвердить, отклонить, доработать;
- если дубль спорный, отправить на подтверждение менеджеру.
- Пример ожидаемого результата:
- карточка заявки;
- флаг дубля, если найдено совпадение;
- короткое объяснение, почему заявка спорная;
- статус для панели и сводки.
8. **Telegram-сводка**
- Отдельно настройте выход для Ержана Асанова:
- новые заявки;
- зависшие заявки;
- спорные дубли;
- заявки, которые уже взяты в работу.
- Сводка должна быть короткой и без лишних деталей.
## 4. Примеры поведения
**Вопрос:** Что делать, если заявка пришла из Lotus sales.almaty@kt.kz, но ФИО написано иначе, а телефон совпадает?
**Ожидаемый ответ:** Пометить как спорный дубль и передать менеджеру на подтверждение до финального принятия в работу.
**Вопрос:** Что делать, если письмо с сайта попало в спам общего ящика?
**Ожидаемый ответ:** Забрать заявку в общий поток, создать карточку и показать её в рабочем списке, чтобы она не потерялась.
**Вопрос:** Что должно попасть в сводку Ержана утром?
**Ожидаемый ответ:** Короткий список новых, зависших и спорных заявок за день, без ручного сбора.
## 5. Как принять работу
- Подтверждён рабочий путь публикации агента в Alem.
- Подтверждён сценарий с человеческим решением для спорных дублей.
- Подтверждены источники знаний: Lotus sales.almaty@kt.kz, Excel zayavki_2026.xlsx, сетевой диск \\ktfs01\sales\заявки.
- Подтверждён формат сводки для Telegram Ержана.
- Подтверждён внутренний контур КТ и запрет на внешнюю передачу данных.
- Подтверждены критерии качества: заявка берётся в работу не позже 30 минут, дубли удерживаются в пределах пары процентов.
- Подтверждён baseline по числу заявок за прошлую неделю через Динару из Excel.
- Подтверждено, кто именно и по какому правилу помечает заявку спорной и отдаёт её менеджеру.
- Подтверждено точное название и точка входа рабочего экрана для менеджеров по заявкам из Lotus и Excel.
Блокеры маршрута до запуска:
- точная цифра заявок за прошлую неделю;
- API или иные точки интеграции у Lotus и Excel;
- прежние попытки автоматизации;
- кто именно принимает работу по решению;
- точный допустимый процент дублей в числах;
- детали маршрута передачи спорных случаев.

192
brief.md Normal file
View File

@ -0,0 +1,192 @@
# Бриф инициативы: ИИ-агент для заявок на подключение
**Суть:** отдел продаж филиала Алматы теряет и дублирует заявки на подключение между Lotus-почтой и Excel; нужен ИИ-агент с отдельным экраном для менеджеров и автоматической сводкой в Telegram для Ержана, чтобы сократить ручную нагрузку и не терять заявки.
## 1. Контакты владельца процесса
- ФИО:
- Ержан Асанов
- Контакт:
- @erjan_a, +7 701 555 3311
- Департамент / дивизион:
- Дивизион по трансформации, отдел продаж филиала Алматы
- Соисполнитель / пользователь процесса:
- Серик Жумабаев, старший менеджер отдела продаж
## 2. Процесс и владелец
- Название процесса и роль владельца:
- Обработка заявок на подключение.
- Ержан руководит отделом продаж и отвечает за сводную картину для отчёта наверх.
- Что входит в объём задачи и что явно вне объёма (out of scope):
- В объём входит приём заявок, первичная регистрация, поиск дублей, обновление статусов и контроль потока.
- В объём входит подготовка сводки для руководителя.
- Вне объёма – внешняя передача данных; процесс должен оставаться только во внутреннем контуре КТ.
- Вне объёма – коммерческая тайна; по словам владельца, её в процессе нет.
## 3. Процесс AS-IS
- Триггер:
- Заявка приходит из общего Lotus-ящика с сайта или менеджер записывает её после звонка клиента.
- Участники:
- Клиент.
- Менеджер.
- Старший менеджер.
- Руководитель отдела.
- Аналитик Динара.
- Системы:
- Lotus-почта `sales.almaty@kt.kz`.
- Excel-файл `zayavki_2026.xlsx`.
- Сетевой диск `\\ktfs01\sales\заявки`.
- Что происходит сейчас:
- Заявки из почты и звонков вручную переносят в Excel.
- Один файл используется всеми менеджерами одновременно.
- Пока файл открыт у одного, другой ждёт.
- Статусы обновляют с задержкой, по факту раз в 2-3 дня.
- Дубли специально не ищут.
- Если дубль замечен случайно, вторую строку удаляют.
- При разном написании фамилии дубль может не распознаться.
- Письма с сайта иногда попадают в спам общего ящика.
- Руководитель не видит реальную картину в моменте.
- Клиент может получить два звонка по одной заявке.
## 4. Боль и потери
- Заявки теряются между почтой и Excel, потому что входной поток не контролируется непрерывно.
- Дубли создают повторные звонки клиенту и искажают учёт.
- Общий файл на сетевом диске создаёт ожидание и конфликт одновременной работы.
- Статусы заполняются с задержкой, поэтому руководитель видит устаревшую картину.
- В пиковые дни процесс разваливается сильнее, чем в обычные.
- Менеджеры перегружены ручной работой и не успевают держать актуальный статус.
## 5. Объёмы и baseline
- Объём за прошлую неделю:
- Навскидку 212 заявок, точные цифры поднимет аналитик Динара из Excel.
- Текущий объём в день:
- 30-50 заявок в день, в акции до 100.
- Пиковая нагрузка:
- В акции заявок вдвое больше, и именно тогда процесс разваливается.
- Время на одну заявку:
- 10-15 минут ручной работы.
- Текущая суммарная нагрузка:
- По расчёту Төре по объёму и времени шага – примерно 35-53 часа ручной работы в неделю на всех, владелец подтвердил.
- Личная нагрузка владельца:
- Около 1 часа в день уходит на сбор картины для отчёта наверх.
- Тройка покрытия:
- Не применима, потому что процесс обрабатывает каждый входящий запрос, а не выборочную долю потока.
- Норма по скорости:
- Максимум 30 минут от поступления заявки до взятия в работу.
- Допустимое качество:
- Допустимы пару процентов дублей.
- Что нужно минимум:
- Спорные дубли и сложные случаи должны уходить менеджеру на подтверждение.
## 6. Данные и источники
- Lotus-почта `sales.almaty@kt.kz`:
- Источник входящих заявок с сайта.
- Формат – письма в общем ящике.
- ПДн присутствуют: ФИО, адреса, телефоны клиентов.
- Excel-файл `zayavki_2026.xlsx`:
- Рабочий реестр заявок.
- Формат – таблица на сетевом диске `\\ktfs01\sales\заявки`.
- Поля данных:
- ФИО, адрес, тариф, статус, менеджер, дата.
- Ограничение по данным:
- ПДн клиентов нельзя выносить наружу, только внутренний контур КТ.
- Точка доступа к Excel:
- `\\ktfs01\sales\заявки`.
- Точка доступа к почте:
- `sales.almaty@kt.kz`.
- API у систем:
- не подтверждено – уточнит Динара / ИТ-сопровождение.
## 7. Рекомендуемое решение TO-BE
- Ядро решения:
- ИИ-агент с рабочим экраном для менеджеров, автоматическим поиском и снятием дублей, сводкой в Telegram для руководителя и передачей спорных случаев человеку на подтверждение.
- Главный выходной артефакт:
- Отдельный экран со списком заявок и статусами.
- Автоматическая сводка в Telegram для Ержана.
- Excel-выгрузка как привычный дополнительный формат.
- Система-приёмник:
- Рабочий экран для менеджеров.
- Telegram для руководителя.
- Excel-выгрузка для привычного использования.
- Уровень автономности:
- Черновик и построчная работа с подтверждением спорных случаев человеком.
- Порог уверенности/точности, при котором агент действует сам:
- Пару процентов дублей допустимо; спорные случаи передаются менеджеру.
- Рекомендуемый формат реализации:
- `Alem + веб-панель` – по фактам нужен отдельный экран для менеджеров и автоматическая сводка, а контур внутренний; аналитическая рекомендация: `agent_with_approval`, уровень автономности 4/8.
## 8. Пользователи и действия после результата
- Основной пользователь – менеджер отдела продаж.
- Менеджер работает построчно в отдельном экране.
- Старший менеджер использует общий список и статусы для контроля.
- Ержан получает автоматическую сводку в Telegram без своего участия.
- После результата менеджер подтверждает спорные случаи.
- После результата руководитель смотрит сводку и картину по потоку.
- Получатель Excel-выгрузки – внутреннее использование команды продаж.
## 9. Граница проверки человеком
- Человек проверяет спорные дубли и сложные случаи.
- Человек подтверждает или отклоняет сомнительную заявку.
- Человек остаётся на границе, где агент не уверен в совпадении.
- Точка проверки – до финального принятия заявки в работу.
- При ошибке человек может подтвердить, отклонить или доработать запись.
- Автодействие без человека не допускается для спорных случаев.
## 10. Критерии приёмки
- Как менеджер, когда заявка пришла из почты или звонка, я хочу видеть её в общем списке, чтобы взять в работу не позже чем через 30 минут.
- Вход: новая заявка из Lotus или звонка.
- Ожидаемый выход: заявка в рабочем списке со статусом.
- Ошибка/эскалация: если заявка спорная или дубль неясен, она уходит менеджеру на подтверждение.
- Как менеджер, когда в поток попадает дубль, я хочу видеть предупреждение, чтобы не звонить клиенту дважды.
- Вход: заявка с совпадающими данными.
- Ожидаемый выход: флаг дубля или карточка на проверку.
- Ошибка/эскалация: спорный дубль передаётся человеку.
- Как руководитель, когда начинается день, я хочу получать сводку в Telegram, чтобы видеть новые, зависшие и дубли без ручного сбора.
- Вход: поток заявок за день.
- Ожидаемый выход: короткая автоматическая сводка.
- Ошибка/эскалация: если данные неполные, сводка помечает это как не подтверждено.
- Числовой порог качества:
- Время от поступления заявки до взятия в работу – не более 30 минут.
- Доля дублей – в пределах пары процентов.
## 11. Метрики успешности проекта и ожидаемый эффект
- Трудозатраты сейчас – около 35-53 часов в неделю на всех по расчёту Төре, подтверждённому владельцем.
- Личная нагрузка Ержана – около 1 часа в день на отчёт наверх.
- Ожидаемая экономия – 25-35 часов в неделю за счёт приёма из почты, первичной регистрации и поиска дублей.
- Метрика скорости – заявка берётся в работу не позже 30 минут после поступления.
- Метрика качества – дубли удерживаются в пределах пары процентов.
- Метрика устойчивости – статусы обновляются в день поступления, без потерь из почты и спама.
- Метрика управляемости – руководитель получает автоматическую сводку без ручного сбора.
## 12. Риски, SLA и ограничения
- ПДн есть: ФИО, адреса, телефоны клиентов, поэтому решение должно оставаться во внутреннем контуре КТ.
- Цена ошибки – клиент может получить два звонка, заявка может потеряться, отчёт наверх может исказиться.
- SLA по скорости есть: заявка должна быть взята в работу максимум за 30 минут.
- В акции объём удваивается, и именно тогда процесс разваливается.
- Половина менеджеров возрастные, поэтому интерфейс должен быть проще WhatsApp.
- Письма с сайта могут попадать в спам общего ящика.
- **Оценка аналитика: возможно более простое решение**
- Слишком простой вариант без рабочего экрана не подходит: у менеджеров поток заявок построчный и общий статус должен быть виден в работе, а не только в сводке.
## 13. Передача в работу
- Access contacts:
- Ержан Асанов, @erjan_a, +7 701 555 3311.
- IT contacts:
- Динара из Excel – поднимет точную цифру по прошлой неделе.
- Approvers:
- Руководитель отдела продаж.
- Previous automation attempts:
- не подтверждено – уточнит Ержан или Серик.
- Рабочие источники:
- Lotus-ящик `sales.almaty@kt.kz`.
- Excel-файл `zayavki_2026.xlsx`.
- Сетевой диск `\\ktfs01\sales\заявки`.
## 14. Что осталось уточнить
- Точная цифра заявок за прошлую неделю – уточнит Динара.
- API или иные точки интеграции у Lotus и Excel – уточнит ИТ-сопровождение.
- Прежние попытки автоматизации – не подтверждено – уточнит Ержан или Серик.
- Кто именно будет принимать работу по решению – уточнит руководитель отдела продаж.
- Точный допустимый процент дублей в числах – уточнит Ержан, если потребуется формализация.
- Детали маршрута передачи спорных случаев – уточнит Серик.
- Дата старта проекта – не подтверждено – уточнит Департамент AI (ДЦБ).
Проверьте, пожалуйста, и скажите, если нужно что-то поправить.

142
design.md Normal file
View File

@ -0,0 +1,142 @@
<!-- vibe42-design-version: v2-recipes-2026-07-07 -->
# Design system — Vibe42 песочница
Базовые цвета и типографика для лендингов. Начинай с этого, можно отклоняться.
## Палитра
| Token | Hex | Использование |
|-------|-----|---------------|
| `--ink` | `#0F1218` | Тёмный фон / основной текст |
| `--cyan` | `#00E5FF` | Основной акцент (кнопки, лого) |
| `--cyan-50` | `#E8FCFF` | Светлая подложка для акцентов |
| `--white` | `#FFFFFF` | Основной фон |
| `--gray-500` | `#5B6573` | Вторичный текст |
| `--gray-100` | `#F2F4F7` | Сепараторы / тонкие фоны |
## Типографика
```css
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
```
| Уровень | Размер | Вес | line-height |
|---------|--------|-----|-------------|
| h1 (hero) | 56px | 800 | 1.05 |
| h2 (section) | 36px | 700 | 1.15 |
| h3 | 22px | 700 | 1.3 |
| body | 17px | 400 | 1.6 |
На мобиле — h1 → 36px, h2 → 28px.
## Лейаут
- max-width контента: **1140px**
- секция: `padding: 80px 24px` (мобила: `48px 20px`)
- border-radius: `8px` (кнопки), `16px` (карточки)
## Стартер `index.html`
```html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Мой проект</title>
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
*{box-sizing:border-box;margin:0;padding:0}
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--white)}
.container{max-width:1140px;margin:0 auto;padding:80px 24px}
.hero{background:var(--ink);color:var(--white)}
.hero h1{font-size:56px;font-weight:800;line-height:1.05;margin-bottom:24px}
.hero p{font-size:20px;color:#9aa3b2;max-width:600px;margin-bottom:32px}
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:14px 28px;border-radius:8px;font-weight:700;text-decoration:none}
.section h2{font-size:36px;font-weight:700;margin-bottom:24px}
.card{background:var(--gray-100);border-radius:16px;padding:32px;margin-bottom:16px}
@media (max-width:640px){.hero h1{font-size:36px}.section h2{font-size:28px}.container{padding:48px 20px}}
</style>
</head>
<body>
<section class="hero"><div class="container">
<h1>Заголовок</h1>
<p>Подзаголовок — пара предложений о чём это.</p>
<a class="btn" href="#section">Начать</a>
</div></section>
<section id="section" class="section"><div class="container">
<h2>Секция</h2>
<div class="card">Контент карточки.</div>
<div class="card">Контент карточки.</div>
</div></section>
</body>
</html>
```
## Что НЕ юзать
- Bootstrap / Material UI / Chakra / Ant Design — слишком тяжело
- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG
- jQuery — vanilla JS достаточно
## Что МОЖНО (если очень надо)
- **Tailwind через CDN**: `<script src="https://cdn.tailwindcss.com"></script>`
- **Placeholder картинки**: `https://picsum.photos/800/600`
- **Google Fonts через `<link>`** в head
## 📦 Готовые рецепты (используй их, не изобретай)
### Фото без ключей
- Детерминированные (одно и то же слово → одна и та же картинка): `https://picsum.photos/seed/coffee/800/600`. Меняй `coffee` на любое слово-seed.
- Тематические (по тегу): `https://loremflickr.com/800/600/coffee` — тег прямо в URL (`/coffee`, `/mountains`, `/food`). Тема не гарантируется на 100%, но обычно в кассу.
### Кнопка WhatsApp
```html
<a href="https://wa.me/7XXXXXXXXXX?text=Здравствуйте!" style="display:inline-block;background:#25D366;color:#fff;padding:12px 22px;border-radius:8px;font-weight:700;text-decoration:none">Написать в WhatsApp</a>
```
`7XXXXXXXXXX` — номер в межд. формате без `+`. `text=` — предзаполненное сообщение.
### Форма заявки без бэкенда
```html
<form action="https://formsubmit.co/EMAIL" method="POST">
<input name="name" placeholder="Имя" required>
<input name="phone" placeholder="Телефон" required>
<button type="submit">Отправить</button>
</form>
```
Скажи юзеру: замени `EMAIL` на свою почту — заявки будут приходить туда (первая заявка активирует, письмо-подтверждение придёт один раз).
### Google Fonts (максимум 2 шрифта: один display + один text)
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;700&family=Unbounded:wght@700&display=swap" rel="stylesheet">
```
В CSS: `font-family:'Unbounded',sans-serif` для заголовков, `'Manrope',sans-serif` для текста.
### Favicon-эмодзи (inline SVG data-URI, без файла)
```html
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>☕</text></svg>">
```
Меняй эмодзи `☕` на подходящее теме.
### Карта (Google Maps embed, без ключа)
На maps.google.com найди место → «Поделиться» → «Встроить карту» → скопируй iframe. Вставь как есть:
```html
<iframe src="https://www.google.com/maps/embed?pb=..." width="100%" height="320" style="border:0;border-radius:12px" loading="lazy"></iframe>
```
### Плавные появления при скролле (IntersectionObserver)
```html
<style>
.reveal{opacity:0;transform:translateY(20px);transition:opacity .6s ease,transform .6s ease}
.reveal.in{opacity:1;transform:none}
</style>
<script>
var io=new IntersectionObserver(function(entries){
entries.forEach(function(e){ if(e.isIntersecting) e.target.classList.add('in'); });
},{threshold:.15});
document.querySelectorAll('.reveal').forEach(function(el){ io.observe(el); });
</script>
```
Добавь класс `reveal` любой секции/карточке — появится плавно при прокрутке.

174
index.html Normal file
View File

@ -0,0 +1,174 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Заявки на подключение — Алматы</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="header">
<div class="header-left">
<h1>Заявки на подключение</h1>
<p class="subtitle">Отдел продаж филиала Алматы</p>
</div>
<div class="header-right">
<button class="btn btn-secondary" onclick="showTelegramSummary()">📊 Сводка в Telegram</button>
<button class="btn btn-secondary" onclick="exportToExcel()">📥 Выгрузить в Excel</button>
<button class="btn btn-primary" onclick="openModal()">+ Новая заявка</button>
</div>
</header>
<main class="main">
<section class="dashboard">
<div class="stat-card stat-new">
<div class="stat-value" id="count-new">0</div>
<div class="stat-label">Новые</div>
</div>
<div class="stat-card stat-work">
<div class="stat-value" id="count-work">0</div>
<div class="stat-label">В работе</div>
</div>
<div class="stat-card stat-duplicate">
<div class="stat-value" id="count-duplicate">0</div>
<div class="stat-label">Дубли</div>
</div>
<div class="stat-card stat-overdue">
<div class="stat-value" id="count-overdue">0</div>
<div class="stat-label">Просрочка</div>
</div>
<div class="stat-card stat-total">
<div class="stat-value" id="count-total">0</div>
<div class="stat-label">Всего</div>
</div>
</section>
<section class="filters">
<div class="search-box">
<input type="text" id="search" placeholder="Поиск по ФИО, адресу, телефону..." oninput="renderTable()">
</div>
<div class="status-filters">
<button class="filter-btn active" data-status="all" onclick="setFilter('all')">Все</button>
<button class="filter-btn" data-status="новая" onclick="setFilter('новая')">Новые</button>
<button class="filter-btn" data-status="в работе" onclick="setFilter('в работе')">В работе</button>
<button class="filter-btn" data-status="на проверке" onclick="setFilter('на проверке')">На проверке</button>
<button class="filter-btn" data-status="спорная" onclick="setFilter('спорная')">Спорные</button>
<button class="filter-btn" data-status="завершена" onclick="setFilter('завершена')">Завершена</button>
</div>
</section>
<section class="table-section">
<table class="requests-table">
<thead>
<tr>
<th>Дата</th>
<th>ФИО</th>
<th>Телефон</th>
<th>Адрес</th>
<th>Тариф</th>
<th>Менеджер</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</section>
</main>
<div class="modal" id="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Новая заявка</h2>
<button class="close-btn" onclick="closeModal()">×</button>
</div>
<form id="request-form" onsubmit="saveRequest(event)">
<input type="hidden" id="req-id">
<div class="form-row">
<div class="form-group">
<label>ФИО *</label>
<input type="text" id="req-name" required placeholder="Иванов Иван Иванович">
</div>
<div class="form-group">
<label>Телефон *</label>
<input type="tel" id="req-phone" required placeholder="+7 777 123 45 67">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Адрес *</label>
<input type="text" id="req-address" required placeholder="г. Алматы, ул. Абая 10, кв. 5">
</div>
<div class="form-group">
<label>Тариф *</label>
<select id="req-tariff" required>
<option value="">Выберите тариф</option>
<option>Домашний интернет 50 Мбит/с</option>
<option>Домашний интернет 100 Мбит/с</option>
<option>Домашний интернет 500 Мбит/с</option>
<option>ТВ + Интернет</option>
<option>Тройной пакет</option>
<option>Бизнес тариф</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Менеджер</label>
<input type="text" id="req-manager" placeholder="Фамилия И.О.">
</div>
<div class="form-group">
<label>Статус</label>
<select id="req-status">
<option value="новая">Новая</option>
<option value="в работе">В работе</option>
<option value="на проверке">На проверке</option>
<option value="спорная">Спорная</option>
<option value="завершена">Завершена</option>
</select>
</div>
</div>
<div class="form-group">
<label>Комментарий</label>
<textarea id="req-comment" rows="3" placeholder="Дополнительная информация..."></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
<div class="modal" id="telegram-modal">
<div class="modal-content modal-small">
<div class="modal-header">
<h2>📊 Сводка для руководителя</h2>
<button class="close-btn" onclick="closeTelegramModal()">×</button>
</div>
<div class="telegram-content" id="telegram-content"></div>
<div class="form-actions">
<button class="btn btn-secondary" onclick="closeTelegramModal()">Закрыть</button>
<button class="btn btn-primary" onclick="copyTelegram()">📋 Копировать</button>
</div>
</div>
</div>
<div class="modal" id="duplicate-modal">
<div class="modal-content modal-small">
<div class="modal-header">
<h2>⚠️ Возможный дубль</h2>
<button class="close-btn" onclick="closeDuplicateModal()">×</button>
</div>
<div class="duplicate-content" id="duplicate-content"></div>
<div class="form-actions">
<button class="btn btn-secondary" onclick="closeDuplicateModal()">Отмена</button>
<button class="btn btn-warning" onclick="confirmDuplicate()">Пометить как дубль</button>
<button class="btn btn-primary" onclick="keepBoth()">Оставить обе</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

158
passport.md Normal file
View File

@ -0,0 +1,158 @@
# ПАСПОРТ ИИ-АГЕНТА
| Поле | Значение |
|---|---|
| Наименование | ИИ-агент для заявок на подключение |
| Категория | Высокая (ПДн клиентов, цена ошибки – потеря заявки, двойной звонок, искажённый отчёт) |
| Сложность | Высокая (поток десятков заявок в день, общий реестр, построчная проверка, несколько ролей, внутренние источники) |
| Исполнитель | Департамент AI (ДЦБ) АО «Казахтелеком» |
| Заказчик / Владелец | Ержан Асанов, Дивизион по трансформации, отдел продаж филиала Алматы, @erjan_a, +7 701 555 3311 |
| Дата внедрения | – |
| Пользователь | Менеджер отдела продаж, старший менеджер, руководитель отдела |
## 1. Предпосылки
- В отделе продаж заявки на подключение теряются между Lotus-почтой и Excel, а клиент может ждать взятия в работу полдня вместо нормы 30 минут.
- Ручной поток создаёт дубли, повторные звонки клиенту и искажённую картину для руководителя.
- Один общий Excel-файл на сетевом диске создаёт ожидание, потому что одновременно работать с ним неудобно.
- Статусы обновляются с задержкой 2-3 дня, поэтому управление идёт по устаревшей картине.
- В пиковые периоды, когда заявок вдвое больше, процесс разваливается сильнее всего.
- Половина менеджеров возрастные, поэтому интерфейс должен быть проще WhatsApp, иначе решение не взлетит.
## 2. Входные данные и источники
- Lotus-почта `sales.almaty@kt.kz` – источник входящих заявок с сайта, письма приходят в общий ящик.
- Excel-файл `zayavki_2026.xlsx` – рабочий реестр заявок, используется как общий файл команды.
- Сетевой диск `\\ktfs01\sales\заявки` – точка доступа к Excel-файлу.
- Поля заявки: ФИО, адрес, тариф, статус, менеджер, дата.
- В заявках есть ПДн: ФИО, адреса, телефоны клиентов.
- Коммерческой тайны в этом процессе нет, но наружу данные выносить нельзя – только внутренний контур КТ.
- API у Lotus и Excel не подтверждено – уточнит Динара / ИТ-сопровождение.
- Точная точка входа рабочего экрана для менеджеров не подтверждена – уточнит Департамент AI (ДЦБ) при проектировании панели.
## 3. Процессы / функции для внедрения ИИ
- Приём заявки из Lotus или из ручного ввода после звонка клиента.
- Первичная регистрация заявки в рабочем списке.
- Поиск дублей по ФИО, адресу, телефону и другим доступным признакам.
- Выделение спорных случаев для подтверждения человеком.
- Обновление статуса заявки в общем реестре.
- Формирование сводки для руководителя в Telegram.
- Подготовка Excel-выгрузки как привычного дополнительного формата.
## 4. Шаги процесса
- Триггер: заявка приходит из общего Lotus-ящика или менеджер записывает её после звонка клиента.
- Агент забирает заявку и проверяет, есть ли она уже в потоке.
- Агент сопоставляет данные и ищет возможный дубль.
- Если совпадение уверенное, заявка идёт в рабочий список.
- Если совпадение спорное, агент отдаёт заявку менеджеру на подтверждение.
- Менеджер подтверждает, отклоняет или дорабатывает спорную заявку.
- Агент обновляет статус в общем реестре и готовит сводку.
- Руководитель получает автоматическую сводку в Telegram.
- Финальный результат – заявка в работе и актуальный статус в реестре.
## 5. Результат работы агента
- Отдельный экран со списком заявок и статусами для менеджеров.
- Автоматическая сводка в Telegram для Ержана.
- Excel-выгрузка как дополнительный привычный формат.
- Статусы: новая, в работе, на проверке, спорная, просрочка.
- Критерий качества результата – заявка должна попадать в работу не позже чем через 30 минут.
- Критерий качества результата – доля дублей должна удерживаться в пределах пары процентов.
## 6. Ожидаемый эффект
- Снижение трудозатрат на 25-35 часов в неделю за счёт приёма из почты, первичной регистрации и поиска дублей.
- Сокращение личной ручной нагрузки Ержана примерно на 1 час в день.
- Снижение потерь заявок из почты и спама.
- Снижение повторных звонков клиентам.
- Ускорение взятия заявки в работу с полудня до нормы 30 минут.
- Повышение актуальности статусов в день поступления.
## 7. Экономический эффект
- Оценка экономии в часах подтверждена: 25-35 часов в неделю.
- Полный эффект в тенге не считался.
- Источник расчёта эффекта – объём 212 заявок за прошлую неделю, 10-15 минут ручной работы на заявку и подтверждение владельца.
- Дополнительный эффект – почти полное снятие личного часа Ержана на ежедневный отчёт.
## 8. Контроль и риски
- Агент не должен сам принимать спорные дубли без подтверждения человека.
- Агент не должен выносить ПДн наружу.
- Ошибка агента может привести к двойному звонку клиенту.
- Ошибка агента может привести к потере заявки.
- Ошибка агента может исказить отчёт наверх.
- В пиковые дни риск возрастает, потому что поток удваивается.
- При разном написании фамилии дубль может быть не распознан без дополнительной проверки.
- Интерфейс должен быть максимально простым, иначе менеджеры не будут им пользоваться.
## 9. Меры по снижению рисков
- Все спорные дубли передаются менеджеру на подтверждение.
- Все данные остаются во внутреннем контуре КТ.
- Для менеджеров нужен отдельный экран с построчной работой, а не сложный интерфейс.
- Для руководителя нужна автоматическая сводка, чтобы убрать ручной сбор.
- Дубли допустимы только в пределах пары процентов.
- Статусы должны обновляться в день поступления, чтобы не копить устаревшие записи.
## 10. Рекомендуемый формат реализации
- Маршрут: `Alem + веб-панель` – отдельный экран нужен менеджерам, а руководителю нужна автоматическая сводка в Telegram; процесс построчный, с общим состоянием и несколькими ролями.
- Почему подходит: есть поток десятков заявок в день, работа идёт по каждой заявке отдельно, и менеджерам нужен рабочий экран, а не только чат или сводка.
- Что делает Alem: агент читает Lotus и Excel, ищет дубли, формирует карточки, отдаёт спорные случаи на подтверждение и готовит сводку.
- Что вне Alem / требует подтверждения: точная точка входа панели, доступы к источникам, правила пометки спорных случаев и интеграционные детали.
- Не обещать запуск: маршрут – рекомендация; до production нужны подтверждения доступов, ИБ, интеграций и бюджета; финальное решение – Департамент AI (ДЦБ).
### Карточка агента Alem (черновик)
- Название: ИИ-агент для заявок на подключение.
- Тип агента: agent_with_approval.
- Суть системного промпта: забирай заявки из Lotus и Excel, ищи дубли, помечай спорные случаи, обновляй статусы, готовь сводку для руководителя, не выноси ПДн наружу.
- Базы знаний: регламент обработки заявок, правила дублей, шаблоны статусов, примеры рабочих заявок, список полей ФИО / адрес / тариф / статус / менеджер / дата.
- Инструменты / MCP: Lotus-почта, Excel-реестр на `\\ktfs01\sales\заявки`, Telegram-уведомления, рабочий экран / панель.
- Примеры диалога:
- «Покажи новые заявки за сегодня».
- «Какие заявки спорные и требуют подтверждения?»
- «Сформируй утреннюю сводку для Ержана».
- Правила draft-подтверждения:
- Спорные дубли не подтверждать автоматически.
- Любое действие с внешним эффектом – только после подтверждения человека.
- ПДн не выводить наружу.
- Как протестировать:
- Заявка должна попасть в работу не позже 30 минут.
- Доля дублей должна оставаться в пределах пары процентов.
- Спорные случаи должны уходить человеку.
- Сводка должна показывать новые, зависшие и дубли.
## 11. Метрики успешности проекта и ожидаемый эффект
- Время от поступления заявки до взятия в работу – не более 30 минут.
- Доля дублей – в пределах пары процентов.
- Снижение ручной нагрузки – 25-35 часов в неделю.
- Статусы обновляются в день поступления.
- Потери заявок из почты и спама должны уйти.
- Ержан должен получать автоматическую сводку без ручного сбора.
- Личная нагрузка на отчёт – около 1 часа в день – должна почти исчезнуть.
## 12. Риски, SLA и ограничения
- ПДн есть: ФИО, адреса, телефоны клиентов, поэтому решение должно оставаться во внутреннем контуре КТ.
- Цена ошибки – клиент может получить два звонка, заявка может потеряться, отчёт наверх может исказиться.
- SLA по скорости есть: заявка должна быть взята в работу максимум за 30 минут.
- В акции объём удваивается, и именно тогда процесс разваливается.
- Половина менеджеров возрастные, поэтому интерфейс должен быть проще WhatsApp.
- Письма с сайта могут попадать в спам общего ящика.
- **Оценка аналитика: возможно более простое решение**
- Слишком простой вариант без рабочего экрана не подходит: у менеджеров поток заявок построчный и общий статус должен быть виден в работе, а не только в сводке.
## 13. Передача в работу
- Access contacts:
- Ержан Асанов, @erjan_a, +7 701 555 3311.
- IT contacts:
- Динара из Excel – поднимет точную цифру по прошлой неделе.
- Approvers:
- Руководитель отдела продаж.
- Previous automation attempts:
- не подтверждено – уточнит Ержан или Серик.
- Рабочие источники:
- Lotus-ящик `sales.almaty@kt.kz`.
- Excel-файл `zayavki_2026.xlsx`.
- Сетевой диск `\\ktfs01\sales\заявки`.
## 14. Что осталось уточнить
- Точная цифра заявок за прошлую неделю – уточнит Динара.
- API или иные точки интеграции у Lotus и Excel – уточнит ИТ-сопровождение.
- Прежние попытки автоматизации – не подтверждено – уточнит Ержан или Серик.
- Кто именно будет принимать работу по решению – уточнит руководитель отдела продаж.
- Точный допустимый процент дублей в числах – уточнит Ержан, если потребуется формализация.
- Детали маршрута передачи спорных случаев – уточнит Серик.
- Дата старта проекта – не подтверждено – уточнит Департамент AI (ДЦБ).

45
scheme.md Normal file
View File

@ -0,0 +1,45 @@
```mermaid
flowchart LR
subgraph init["Инициатор / клиент"]
start(["Заявка с сайта или звонок"])
end
subgraph ai["ИИ-агент"]
a1["Забрать заявку"]
a2["Проверить дубли"]
a3["Сформировать карточку"]
a4["Подготовить сводку"]
end
subgraph human["Владелец / менеджер"]
h1{"Дубль или спорный случай?"}
h2["Подтвердить или отклонить"]
h3["Работать по заявке"]
h4["Получить сводку"]
end
subgraph sys["Системы КТ"]
s1["Lotus sales.almaty@kt.kz"]
s2["Excel zayavki_2026.xlsx"]
s3["\\ktfs01\\sales\\заявки"]
s4["Telegram Ержана"]
end
start --> s1
s1 --> a1
a1 --> a2
a2 --> h1
h1 -- "да" --> h2
h1 -- "нет" --> h3
h2 --> h3
a2 --> a3
a3 --> s2
a3 --> s3
a2 --> a4
a4 --> s4
h3 --> s2
h3 --> s3
s2 --> h4
s3 --> h4
h4 --> final(["Статус обновлён"])
```

367
script.js Normal file
View File

@ -0,0 +1,367 @@
// Демо-данные: ~30 заявок как в брифе (30-50 в день, пик до 100)
const DEMO_DATA = [
{ id: 1, name: "Алиев Арман Берикович", phone: "+7 777 123 45 67", address: "г. Алматы, ул. Абая 10, кв. 5", tariff: "Домашний интернет 100 Мбит/с", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
{ id: 2, name: "Байкова Гульнара Талгатовна", phone: "+7 701 234 56 78", address: "г. Алматы, пр. Достык 45, офис 12", tariff: "ТВ + Интернет", manager: "Айгуль К.", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
{ id: 3, name: "Васильченко Игорь Петрович", phone: "+7 778 345 67 89", address: "г. Алматы, ул. Толе би 88, кв. 42", tariff: "Домашний интернет 500 Мбит/с", manager: "", status: "новая", comment: "Клиент просит перезвонить после 18:00", date: "2026-08-28", isDuplicate: false },
{ id: 4, name: "Гребенюк Сергей Владимирович", phone: "+7 702 456 78 90", address: "г. Алматы, мкр. Аксай-2, д. 15", tariff: "Тройной пакет", manager: "Серик Ж.", status: "на проверке", comment: "", date: "2026-08-26", isDuplicate: false },
{ id: 5, name: "Дюсенова Айнур Маратовна", phone: "+7 777 567 89 01", address: "г. Алматы, ул. Сатпаева 22, кв. 18", tariff: "Домашний интернет 50 Мбит/с", manager: "Бауыржан Т.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
{ id: 6, name: "Ержанов Максат", phone: "+7 701 678 90 12", address: "г. Алматы, ул. Рыскулова 100, кв. 55", tariff: "Бизнес тариф", manager: "", status: "спорная", comment: "Нужно уточнить юридический адрес", date: "2026-08-25", isDuplicate: false },
{ id: 7, name: "Жуманова Лейла Ахметовна", phone: "+7 778 789 01 23", address: "г. Алматы, пр. Абая 150, кв. 88", tariff: "Домашний интернет 100 Мбит/с", manager: "Айгуль К.", status: "завершена", comment: "", date: "2026-08-24", isDuplicate: false },
{ id: 8, name: "Зинченко Александр", phone: "+7 702 890 12 34", address: "г. Алматы, ул. Макатаева 35, офис 5", tariff: "ТВ + Интернет", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-28", isDuplicate: false },
{ id: 9, name: "Ибраева Сауле", phone: "+7 777 901 23 45", address: "г. Алматы, мкр. Коктем-3, д. 20, кв. 10", tariff: "Домашний интернет 500 Мбит/с", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
{ id: 10, name: "Ким Дмитрий Юрьевич", phone: "+7 701 012 34 56", address: "г. Алматы, ул. Бейбитшилик 12, кв. 30", tariff: "Домашний интернет 100 Мбит/с", manager: "Бауыржан Т.", status: "на проверке", comment: "Клиент на больничном", date: "2026-08-26", isDuplicate: false },
{ id: 11, name: "Лебедева Наталья", phone: "+7 778 123 45 67", address: "г. Алматы, пр. Райымбека 200, кв. 75", tariff: "Тройной пакет", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
{ id: 12, name: "Мамедов Руслан", phone: "+7 702 234 56 78", address: "г. Алматы, ул. Желтоксан 55, офис 8", tariff: "Бизнес тариф", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
{ id: 13, name: "Нургалиева Айжан", phone: "+7 777 345 67 89", address: "г. Алматы, мкр. Таугуль-3, д. 8А, кв. 22", tariff: "Домашний интернет 50 Мбит/с", manager: "Айгуль К.", status: "завершена", comment: "", date: "2026-08-23", isDuplicate: false },
{ id: 14, name: "Омаров Ерлан", phone: "+7 701 456 78 90", address: "г. Алматы, ул. Панфилова 90, кв. 15", tariff: "ТВ + Интернет", manager: "", status: "спорная", comment: "Неправильный адрес, уточняется", date: "2026-08-25", isDuplicate: false },
{ id: 15, name: "Петрова Елена Сергеевна", phone: "+7 778 567 89 01", address: "г. Алматы, пр. Абая 50, кв. 100", tariff: "Домашний интернет 500 Мбит/с", manager: "Бауыржан Т.", status: "в работе", comment: "", date: "2026-08-28", isDuplicate: false },
{ id: 16, name: "Рахимов Берик", phone: "+7 702 678 90 12", address: "г. Алматы, ул. Кабанбай батыра 70, кв. 45", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
{ id: 17, name: "Смирнова Ольга", phone: "+7 777 789 01 23", address: "г. Алматы, мкр. Орбита-2, д. 55, кв. 8", tariff: "Тройной пакет", manager: "Серик Ж.", status: "на проверке", comment: "", date: "2026-08-26", isDuplicate: false },
{ id: 18, name: "Турсунов Марат", phone: "+7 701 890 12 34", address: "г. Алматы, ул. Пушкина 25, офис 12", tariff: "Бизнес тариф", manager: "Айгуль К.", status: "завершена", comment: "", date: "2026-08-22", isDuplicate: false },
{ id: 19, name: "Уалиева Жанар", phone: "+7 778 901 23 45", address: "г. Алматы, пр. Достык 100, кв. 60", tariff: "Домашний интернет 50 Мбит/с", manager: "", status: "новая", comment: "Заявка с сайта", date: "2026-08-28", isDuplicate: false },
{ id: 20, name: "Федоров Виктор", phone: "+7 702 012 34 56", address: "г. Алматы, ул. Богенбай батыра 40, кв. 20", tariff: "ТВ + Интернет", manager: "Бауыржан Т.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
{ id: 21, name: "Хасанова Мадина", phone: "+7 777 123 45 67", address: "г. Алматы, мкр. Нуркент, д. 12, кв. 35", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "спорная", comment: "Дубль? Тот же телефон что у Алиева А.", date: "2026-08-27", isDuplicate: true },
{ id: 22, name: "Иванов Петр", phone: "+7 701 333 22 11", address: "г. Алматы, ул. Тимирязева 80, кв. 50", tariff: "Домашний интернет 500 Мбит/с", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-28", isDuplicate: false },
{ id: 23, name: "Козлов Андрей", phone: "+7 778 444 33 22", address: "г. Алматы, пр. Абая 200, кв. 120", tariff: "Бизнес тариф", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
{ id: 24, name: "Алиев Арман Берикович", phone: "+7 777 123 45 67", address: "г. Алматы, ул. Абая 10, кв. 5", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "спорная", comment: "ВОЗМОЖНЫЙ ДУБЛЬ - заявка #1", date: "2026-08-28", isDuplicate: true },
{ id: 25, name: "Шарипова Гульмира", phone: "+7 702 555 44 33", address: "г. Алматы, ул. Сейфуллина 150, кв. 70", tariff: "Тройной пакет", manager: "Айгуль К.", status: "на проверке", comment: "", date: "2026-08-26", isDuplicate: false },
{ id: 26, name: "Яковлев Михаил", phone: "+7 777 666 55 44", address: "г. Алматы, мкр. Самал-2, д. 30, кв. 15", tariff: "Домашний интернет 50 Мбит/с", manager: "", status: "новая", comment: "Клиент из рекламы", date: "2026-08-28", isDuplicate: false },
{ id: 27, name: "Есенбаева Айгуль", phone: "+7 701 777 66 55", address: "г. Алматы, ул. Масанчи 85, кв. 40", tariff: "ТВ + Интернет", manager: "Бауыржан Т.", status: "завершена", comment: "", date: "2026-08-21", isDuplicate: false },
{ id: 28, name: "Морозов Игорь", phone: "+7 778 888 77 66", address: "г. Алматы, пр. Райымбека 300, офис 20", tariff: "Бизнес тариф", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
{ id: 29, name: "Кенжебаева Сауле", phone: "+7 702 999 88 77", address: "г. Алматы, мкр. Айнабулак, д. 5, кв. 25", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
{ id: 30, name: "Ткаченко Владимир", phone: "+7 777 000 99 88", address: "г. Алматы, ул. Курмангазы 60, кв. 90", tariff: "Домашний интернет 500 Мбит/с", manager: "Айгуль К.", status: "просрочка", comment: "Клиент не отвечает", date: "2026-08-20", isDuplicate: false }
];
let requests = [];
let currentFilter = 'all';
let duplicateCheckId = null;
// Инициализация
function init() {
const stored = localStorage.getItem('uchet_requests');
if (stored) {
requests = JSON.parse(stored);
} else {
requests = [...DEMO_DATA];
saveToStorage();
}
renderDashboard();
renderTable();
}
function saveToStorage() {
localStorage.setItem('uchet_requests', JSON.stringify(requests));
}
// Дашборд
function renderDashboard() {
const today = new Date().toISOString().slice(0, 10);
const counts = {
'новая': 0,
'в работе': 0,
'на проверке': 0,
'спорная': 0,
'завершена': 0,
'просрочка': 0,
'total': requests.length
};
requests.forEach(r => {
if (counts[r.status] !== undefined) counts[r.status]++;
// Просрочка: старше 3 дней и не завершена
if (r.status !== 'завершена' && r.status !== 'просрочка') {
const daysOld = daysBetween(r.date, today);
if (daysOld > 3) {
counts['просрочка']++;
}
}
});
document.getElementById('count-new').textContent = counts['новая'];
document.getElementById('count-work').textContent = counts['в работе'];
document.getElementById('count-duplicate').textContent = requests.filter(r => r.isDuplicate).length;
document.getElementById('count-overdue').textContent = counts['просрочка'];
document.getElementById('count-total').textContent = counts.total;
}
function daysBetween(date1, date2) {
const d1 = new Date(date1);
const d2 = new Date(date2);
return Math.round((d2 - d1) / (1000 * 60 * 60 * 24));
}
// Фильтры
function setFilter(status) {
currentFilter = status;
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.status === status);
});
renderTable();
}
// Таблица
function renderTable() {
const tbody = document.getElementById('table-body');
const search = document.getElementById('search').value.toLowerCase();
let filtered = requests.filter(r => {
const matchesSearch = r.name.toLowerCase().includes(search) ||
r.phone.toLowerCase().includes(search) ||
r.address.toLowerCase().includes(search) ||
(r.manager && r.manager.toLowerCase().includes(search));
const matchesStatus = currentFilter === 'all' || r.status === currentFilter;
return matchesSearch && matchesStatus;
});
// Сортировка: новые сверху
filtered.sort((a, b) => new Date(b.date) - new Date(a.date));
if (filtered.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="empty-state">
<div class="empty-state-icon">📭</div>
<p>Нет заявок по выбранным фильтрам</p>
</td>
</tr>
`;
return;
}
tbody.innerHTML = filtered.map(r => `
<tr>
<td>${formatDate(r.date)}</td>
<td>
${escapeHtml(r.name)}
${r.isDuplicate ? '<span class="duplicate-flag">ДУБЛЬ</span>' : ''}
</td>
<td>${escapeHtml(r.phone)}</td>
<td>${escapeHtml(r.address)}</td>
<td>${escapeHtml(r.tariff)}</td>
<td>${r.manager || '—'}</td>
<td><span class="status-badge status-${r.status}">${r.status}</span></td>
<td class="actions">
<button class="btn btn-sm btn-secondary" onclick="editRequest(${r.id})">✏️</button>
${r.status === 'новая' ? `<button class="btn btn-sm btn-primary" onclick="takeToWork(${r.id})">В работу</button>` : ''}
${!r.isDuplicate && r.status !== 'завершена' ? `<button class="btn btn-sm btn-warning" onclick="checkDuplicate(${r.id})">Дубль?</button>` : ''}
</td>
</tr>
`).join('');
}
function formatDate(dateStr) {
const [y, m, d] = dateStr.split('-');
return `${d}.${m}.${y}`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Модальное окно
function openModal() {
document.getElementById('modal-title').textContent = 'Новая заявка';
document.getElementById('request-form').reset();
document.getElementById('req-id').value = '';
document.getElementById('modal').classList.add('active');
}
function closeModal() {
document.getElementById('modal').classList.remove('active');
}
function editRequest(id) {
const r = requests.find(x => x.id === id);
if (!r) return;
document.getElementById('modal-title').textContent = 'Редактирование заявки';
document.getElementById('req-id').value = r.id;
document.getElementById('req-name').value = r.name;
document.getElementById('req-phone').value = r.phone;
document.getElementById('req-address').value = r.address;
document.getElementById('req-tariff').value = r.tariff;
document.getElementById('req-manager').value = r.manager || '';
document.getElementById('req-status').value = r.status;
document.getElementById('req-comment').value = r.comment || '';
document.getElementById('modal').classList.add('active');
}
function saveRequest(e) {
e.preventDefault();
const id = document.getElementById('req-id').value;
const data = {
name: document.getElementById('req-name').value.trim(),
phone: document.getElementById('req-phone').value.trim(),
address: document.getElementById('req-address').value.trim(),
tariff: document.getElementById('req-tariff').value,
manager: document.getElementById('req-manager').value.trim(),
status: document.getElementById('req-status').value,
comment: document.getElementById('req-comment').value.trim(),
date: new Date().toISOString().slice(0, 10)
};
if (id) {
const idx = requests.findIndex(r => r.id == id);
if (idx !== -1) {
requests[idx] = { ...requests[idx], ...data };
}
} else {
const newId = Math.max(...requests.map(r => r.id), 0) + 1;
requests.unshift({ id: newId, ...data, isDuplicate: false });
}
saveToStorage();
closeModal();
renderDashboard();
renderTable();
}
// Действия
function takeToWork(id) {
const r = requests.find(x => x.id === id);
if (r) {
r.status = 'в работе';
if (!r.manager) r.manager = prompt('Введите ФИО менеджера:');
saveToStorage();
renderDashboard();
renderTable();
}
}
function checkDuplicate(id) {
const r = requests.find(x => x.id === id);
if (!r) return;
// Ищем дубли по телефону или ФИО
const duplicates = requests.filter(x =>
x.id !== id &&
(x.phone === r.phone || normalizeName(x.name) === normalizeName(r.name))
);
if (duplicates.length > 0) {
duplicateCheckId = id;
const dup = duplicates[0];
document.getElementById('duplicate-content').innerHTML = `
<p>Найдено совпадение с заявкой #${dup.id}:</p>
<div class="duplicate-card">
<strong>${escapeHtml(dup.name)}</strong><br>
Тел: ${escapeHtml(dup.phone)}<br>
Адрес: ${escapeHtml(dup.address)}<br>
Статус: ${dup.status}<br>
Дата: ${formatDate(dup.date)}
</div>
<p>Пометить текущую заявку как дубль?</p>
`;
document.getElementById('duplicate-modal').classList.add('active');
} else {
alert('Дубли не найдены');
}
}
function normalizeName(name) {
return name.toLowerCase().replace(/[^а-яёa-z]/g, '').trim();
}
function confirmDuplicate() {
if (duplicateCheckId) {
const r = requests.find(x => x.id === duplicateCheckId);
if (r) {
r.isDuplicate = true;
r.status = 'спорная';
r.comment = (r.comment || '') + ' Помечена как дубль вручную.';
saveToStorage();
renderDashboard();
renderTable();
}
closeDuplicateModal();
}
}
function keepBoth() {
closeDuplicateModal();
}
function closeDuplicateModal() {
document.getElementById('duplicate-modal').classList.remove('active');
duplicateCheckId = null;
}
// Telegram сводка
function showTelegramSummary() {
const today = new Date().toISOString().slice(0, 10);
const counts = {
'новая': requests.filter(r => r.status === 'новая').length,
'в работе': requests.filter(r => r.status === 'в работе').length,
'на проверке': requests.filter(r => r.status === 'на проверке').length,
'спорная': requests.filter(r => r.status === 'спорная').length,
'завершена': requests.filter(r => r.status === 'завершена').length,
'дубли': requests.filter(r => r.isDuplicate).length
};
const managers = {};
requests.filter(r => r.manager && r.status !== 'завершена').forEach(r => {
managers[r.manager] = (managers[r.manager] || 0) + 1;
});
const text = `📊 *Сводка по заявкам на ${formatDate(today)}*
🔹 Новые: ${counts['новая']}
🔹 В работе: ${counts['в работе']}
🔹 На проверке: ${counts['на проверке']}
🔹 Спорные: ${counts['спорная']}
🔹 Дубли: ${counts['дубли']}
🔹 Завершено: ${counts['завершена']}
📈 *Всего в работе:* ${counts['новая'] + counts['в работе'] + counts['на проверке'] + counts['спорная']}
👥 *По менеджерам:*
${Object.entries(managers).map(([m, c]) => `${m}: ${c} заяв.`).join('\n') || '—'}
⚠️ *Просрочка:* ${requests.filter(r => r.status === 'просрочка').length} заявок старше 3 дней`;
document.getElementById('telegram-content').textContent = text;
document.getElementById('telegram-modal').classList.add('active');
}
function closeTelegramModal() {
document.getElementById('telegram-modal').classList.remove('active');
}
function copyTelegram() {
const text = document.getElementById('telegram-content').textContent;
navigator.clipboard.writeText(text).then(() => {
alert('Сводка скопирована в буфер обмена!');
});
}
// Экспорт в Excel (CSV с BOM)
function exportToExcel() {
const headers = ['ID', 'Дата', 'ФИО', 'Телефон', 'Адрес', 'Тариф', 'Менеджер', 'Статус', 'Комментарий', 'Дубль'];
const rows = requests.map(r => [
r.id,
r.date,
`"${r.name.replace(/"/g, '""')}"`,
`"${r.phone.replace(/"/g, '""')}"`,
`"${r.address.replace(/"/g, '""')}"`,
`"${r.tariff.replace(/"/g, '""')}"`,
`"${(r.manager || '').replace(/"/g, '""')}"`,
r.status,
`"${(r.comment || '').replace(/"/g, '""')}"`,
r.isDuplicate ? 'Да' : 'Нет'
]);
const csv = '\uFEFF' + [headers.join(';'), ...rows.map(r => r.join(';'))].join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `zayavki_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
}
// Старт
init();

423
style.css Normal file
View File

@ -0,0 +1,423 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f7fa;
color: #1a1a2e;
line-height: 1.6;
}
.header {
background: linear-gradient(135deg, #0066cc 0%, #004499 100%);
color: white;
padding: 24px 32px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.header-left h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 4px;
}
.subtitle {
font-size: 14px;
opacity: 0.85;
}
.header-right {
display: flex;
gap: 12px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: #00cc88;
color: white;
}
.btn-primary:hover {
background: #00aa77;
}
.btn-secondary {
background: rgba(255,255,255,0.15);
color: white;
border: 1px solid rgba(255,255,255,0.3);
}
.btn-secondary:hover {
background: rgba(255,255,255,0.25);
}
.btn-warning {
background: #ff9800;
color: white;
}
.btn-warning:hover {
background: #f57c00;
}
.btn-sm {
padding: 6px 12px;
font-size: 13px;
}
.main {
padding: 24px 32px;
max-width: 1400px;
margin: 0 auto;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-value {
font-size: 36px;
font-weight: 700;
margin-bottom: 4px;
}
.stat-label {
font-size: 13px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-new .stat-value { color: #2196f3; }
.stat-work .stat-value { color: #ff9800; }
.stat-duplicate .stat-value { color: #f44336; }
.stat-overdue .stat-value { color: #9c27b0; }
.stat-total .stat-value { color: #00cc88; }
.filters {
background: white;
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.search-box {
flex: 1;
min-width: 280px;
}
.search-box input {
width: 100%;
padding: 10px 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s;
}
.search-box input:focus {
outline: none;
border-color: #0066cc;
}
.status-filters {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.filter-btn {
padding: 8px 16px;
border: 1px solid #e0e0e0;
background: white;
border-radius: 20px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.filter-btn:hover {
background: #f5f7fa;
}
.filter-btn.active {
background: #0066cc;
color: white;
border-color: #0066cc;
}
.table-section {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
overflow: hidden;
}
.requests-table {
width: 100%;
border-collapse: collapse;
}
.requests-table th,
.requests-table td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
}
.requests-table th {
background: #f8f9fa;
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #555;
}
.requests-table tbody tr:hover {
background: #f8f9fa;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-новая { background: #e3f2fd; color: #1976d2; }
.status-в работе { background: #fff3e0; color: #f57c00; }
.status-на проверке { background: #f3e5f5; color: #7b1fa2; }
.status-спорная { background: #ffebee; color: #d32f2f; }
.status-завершена { background: #e8f5e9; color: #388e3c; }
.duplicate-flag {
background: #ffebee;
color: #d32f2f;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
margin-left: 6px;
}
.actions {
display: flex;
gap: 6px;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.modal-small {
max-width: 450px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
}
.modal-header h2 {
font-size: 20px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: #999;
line-height: 1;
}
.close-btn:hover {
color: #333;
}
#request-form {
padding: 24px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: #555;
margin-bottom: 6px;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #0066cc;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
.telegram-content {
padding: 24px;
background: #f5f7fa;
font-family: monospace;
font-size: 13px;
line-height: 1.8;
white-space: pre-wrap;
}
.duplicate-content {
padding: 24px;
}
.duplicate-content p {
margin-bottom: 12px;
}
.duplicate-card {
background: #fff3e0;
border: 1px solid #ffe0b2;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.duplicate-card strong {
color: #f57c00;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: 16px;
padding: 20px;
}
.header-right {
flex-wrap: wrap;
justify-content: center;
}
.main {
padding: 16px;
}
.form-row {
grid-template-columns: 1fr;
}
.requests-table {
font-size: 13px;
}
.requests-table th,
.requests-table td {
padding: 10px 12px;
}
}

1
ui_concept.json Normal file

File diff suppressed because one or more lines are too long