chto-ty-takoe-na-baze-kakih/index.html

412 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- Price Agent · дизайн: Apple. Все 5 CSS сохраняем; подробности в design.md. -->
<html lang="ru" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Цена Агент — агро-рынок</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.css">
<link rel="stylesheet" href="design-system/vibe-theme.css">
<style>
a.kt-ai-btn { text-decoration: none; }
.pa-plot { width: 100%; height: 250px; display: block; margin-top: var(--kt-ai-space-4); }
.pa-legend { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-4); }
.pa-legend .li { display: flex; align-items: center; gap: 8px; font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.pa-legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.pa-wx { display: flex; justify-content: space-between; padding: var(--kt-ai-space-3) 0; border-bottom: 1px solid var(--kt-ai-divider); font-size: var(--kt-ai-text-sm); }
.pa-wx:last-child { border-bottom: none; }
.pa-wx .num { font-variant-numeric: tabular-nums; color: var(--kt-ai-fg-muted); }
.pa-src { display: flex; flex-direction: column; gap: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-5); }
.pa-src .item { display: flex; align-items: center; gap: var(--kt-ai-space-4); }
.pa-src .nm { font-weight: var(--kt-ai-weight-medium); }
.pa-src .sub { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.pa-rows { margin-top: var(--kt-ai-space-6); }
.pa-chat-in { display: flex; gap: var(--kt-ai-space-3); }
.pa-chat-in input { flex: 1; }
.pa-user { align-self: flex-end; max-width: 88%; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl); font-size: var(--kt-ai-text-sm); white-space: pre-wrap; }
.pa-ai { flex: 1; min-width: 0; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-left: 3px solid var(--kt-ai-primary); font-size: var(--kt-ai-text-sm); line-height: 1.65; white-space: pre-wrap; }
.pa-ai.err { border-left-color: var(--kt-ai-status-risk-fg); color: var(--kt-ai-status-risk-fg); }
.pa-ai-label { font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-1); }
.pa-empty { padding: var(--kt-ai-space-6); text-align: center; color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
</style>
</head>
<body class="kt-ai-app">
<div class="kt-ai-shell">
<aside class="kt-ai-sidebar">
<div style="display:flex;align-items:center;gap:10px;padding:var(--kt-ai-space-5) var(--kt-ai-space-5) var(--kt-ai-space-4)">
<svg class="kt-icon" aria-hidden="true" style="color:var(--kt-ai-primary);width:22px;height:22px"><use href="#sparkles"></use></svg>
<div>
<div style="font-weight:var(--kt-ai-weight-semibold);color:var(--kt-ai-fg);font-size:var(--kt-ai-text-md)">Цена Агент</div>
<div class="kt-ai-meta">агро-рынок</div>
</div>
</div>
<div style="padding:0 var(--kt-ai-space-4) var(--kt-ai-space-4)">
<button class="kt-ai-btn" data-variant="primary" id="btnCollect" style="width:100%">
<svg class="kt-icon" aria-hidden="true"><use href="#refresh"></use></svg>
Обновить сейчас
</button>
</div>
<nav>
<div class="kt-ai-nav-group">
<a class="kt-ai-nav-item" data-active="true" href="#"><svg class="kt-icon" aria-hidden="true"><use href="#dashboard"></use></svg><span class="nav-label">Обзор</span></a>
<a class="kt-ai-nav-item" href="#" id="navHistory"><svg class="kt-icon" aria-hidden="true"><use href="#table"></use></svg><span class="nav-label">История</span></a>
<a class="kt-ai-nav-item" href="#" id="navAgents"><svg class="kt-icon" aria-hidden="true"><use href="#source"></use></svg><span class="nav-label">Агенты</span></a>
</div>
</nav>
</aside>
<main class="kt-ai-main" style="min-height:100dvh;display:flex;flex-direction:column;background:var(--kt-ai-bg)">
<header class="kt-ai-topbar">
<nav class="kt-ai-meta" style="display:flex;flex-direction:column;line-height:1.3">
<span id="pageTitle" style="font-size:var(--kt-ai-text-md);color:var(--kt-ai-fg);font-weight:var(--kt-ai-weight-semibold)">Обзор</span>
<span id="pageSub" style="font-size:var(--kt-ai-text-2xs)">открытые источники</span>
</nav>
<div class="spacer"></div>
<div class="kt-ai-search-field">
<input class="kt-ai-search" id="askInput" type="search" placeholder="Спросить агента: почему выросла цена?" style="width:100%">
</div>
</header>
<div style="overflow-y:auto;flex:1;padding:var(--kt-ai-space-8) var(--kt-ai-space-8) var(--kt-ai-space-11)">
<div id="view"></div>
</div>
</main>
</div>
<!-- Drawer: чат с агентом -->
<div class="kt-ai-drawer" id="drawer" role="dialog" aria-modal="false" aria-label="Ассистент" style="display:none">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--kt-ai-space-5)">
<strong style="font-size:var(--kt-ai-text-md)">Ассистент агента</strong>
<button class="kt-ai-btn" data-variant="ghost" id="drawerClose" style="padding:4px 8px"><svg class="kt-icon" aria-hidden="true"><use href="#x"></use></svg></button>
</div>
<div class="kt-ai-chat" data-fill="true" id="chatLog"></div>
<div class="kt-ai-drawer-actions" style="margin-top:var(--kt-ai-space-5)">
<div class="pa-chat-in">
<input class="kt-ai-input" id="chatInput" placeholder="Вопрос по ценам…" />
<button class="kt-ai-btn" data-variant="primary" id="chatSend"><svg class="kt-icon" aria-hidden="true"><use href="#send"></use></svg></button>
</div>
</div>
</div>
<div class="kt-ai-toast-stack" id="toasts"></div>
<script src="design-system/icons/kt-ai-icons.js"></script>
<script>
const $ = (s) => document.querySelector(s);
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
const fmtDay = (iso) => new Date(iso).toLocaleDateString("ru-RU", { day: "numeric", month: "short" });
const fmtTs = (iso) => new Date(iso).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit" }) + ", " + new Date(iso).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
const CODE_NAME = { USD: "Доллар США", EUR: "Евро", RUB: "Рубль РФ", CNY: "Юань КНР" };
const C = {
primary: "var(--kt-ai-primary)",
muted: "var(--kt-ai-fg-muted)",
grid: "var(--kt-ai-divider)",
warn: "var(--kt-ai-status-warn-fg)",
info: "var(--kt-ai-primary)",
line: "var(--kt-ai-primary)"
};
let DATA = null;
let VIEW = "overview";
function toast(msg) {
const t = document.createElement("div");
t.className = "kt-ai-toast";
t.textContent = msg;
$("#toasts").appendChild(t);
setTimeout(() => t.remove(), 4500);
}
function pill(txt, st) { return `<span class="kt-ai-status-pill" data-status="${st}">${txt}</span>`; }
// ---------- Графики (чистый SVG) ----------
function linePlot(series) {
const W = 640, H = 250, P = { t: 18, r: 14, b: 28, l: 44 };
if (!series || !series.length) return '';
const single = series.length === 1;
const vals = series.map((s) => s.avg);
const minV = Math.min(0, ...vals) - 0.15;
const maxV = Math.max(0.3, ...vals) + 0.15;
const x = (i) => P.l + (i * (W - P.l - P.r)) / (series.length - 1);
const y = (v) => P.t + (1 - (v - minV) / (maxV - minV)) * (H - P.t - P.b);
let out = "";
for (let g = Math.ceil(minV); g <= Math.floor(maxV); g++) {
out += `<line x1="${P.l}" y1="${y(g)}" x2="${W - P.r}" y2="${y(g)}" stroke="${C.grid}" stroke-width="1" ${g === 0 ? "" : 'stroke-dasharray="3 4"'} />`;
out += `<text x="${P.l - 8}" y="${y(g) + 4}" text-anchor="end" font-size="11" fill="${C.muted}">${g > 0 ? "+" : ""}${g}%</text>`;
}
series.forEach((s, i) => {
if (s.rain > 0.5) {
const h = Math.min(46, s.rain * 1.8);
out += `<rect x="${x(i) - 5}" y="${H - P.b - h}" width="10" height="${h}" rx="2" fill="${C.primary}" opacity="0.13" />`;
}
});
if (single) {
// Одна точка — рисуем вертикальный маркер
out += `<circle cx="${P.l + (W - P.l - P.r) / 2}" cy="${y(series[0].avg)}" r="5" fill="${C.primary}" />`;
out += `<text x="${W / 2}" y="${y(series[0].avg) - 12}" text-anchor="middle" font-size="12" font-weight="600" fill="${C.line}">${series[0].avg > 0 ? "+" : ""}${series[0].avg}%</text>`;
} else {
const d = series.map((s, i) => (i ? "L" : "M") + x(i).toFixed(1) + " " + y(s.avg).toFixed(1)).join(" ");
out += `<path d="${d}" fill="none" stroke="${C.primary}" stroke-width="2.5" stroke-linejoin="round" />`;
series.forEach((s, i) => { if (i % Math.ceil(series.length / 6) === 0 || i === series.length - 1) out += `<circle cx="${x(i)}" cy="${y(s.avg)}" r="3.2" fill="${C.primary}" />`; });
series.forEach((s, i) => {
if (i % Math.ceil(series.length / 7) === 0 || i === series.length - 1)
out += `<text x="${x(i)}" y="${H - 8}" text-anchor="middle" font-size="11" fill="${C.muted}">${fmtDay(s.date)}</text>`;
});
}
return `<svg class="pa-plot" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet">${out}</svg>`;
}
function barsPlot(items, unit) {
// items: [{label, value, sub}]
const W = 640, H = 250, P = { t: 18, r: 14, b: 34, l: 44 };
const maxV = Math.max(...items.map((i) => Math.abs(i.value))) * 1.1 || 1;
const y = (v) => P.t + (1 - (v + maxV) / (2 * maxV)) * (H - P.t - P.b);
const zero = y(0);
const bw = Math.min(56, (W - P.l - P.r) / items.length * 0.55);
const x = (i) => P.l + i * ((W - P.l - P.r) / items.length) + ((W - P.l - P.r) / items.length - bw) / 2;
let out = `<line x1="${P.l}" y1="${zero}" x2="${W - P.r}" y2="${zero}" stroke="${C.grid}" />`;
items.forEach((it, i) => {
const h = Math.abs((it.value / maxV) * ((H - P.t - P.b) / 2));
const top = it.value >= 0 ? zero - h : zero;
const col = it.value >= 0 ? C.primary : C.warn;
out += `<rect x="${x(i)}" y="${top}" width="${bw}" height="${Math.max(2, h)}" rx="4" fill="${col}" opacity="${it.dim ? 0.45 : 0.9}" />`;
out += `<text x="${x(i) + bw / 2}" y="${(it.value >= 0 ? zero - h - 5 : zero + h + 13)}" text-anchor="middle" font-size="11" fill="${C.muted}" ${it.dim ? 'opacity="0.6"' : ""}>${it.dim ? "нет данных" : (it.value >= 0 ? "+" : "") + it.value.toFixed(unit === "mm" ? 1 : 2) + (unit === "%" ? "%" : "")}</text>`;
const lbl = it.label.length > 9 ? it.label.slice(0, 8) + "…" : it.label;
out += `<text x="${x(i) + bw / 2}" y="${H - 10}" text-anchor="middle" font-size="11" fill="${C.muted}">${esc(lbl)}</text>`;
});
return `<svg class="pa-plot" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet">${out}</svg>`;
}
// ---------- Views ----------
function vOverview() {
const d = DATA;
const last = (d.meta && d.meta.lastRun) ? fmtTs(d.meta.lastRun) : "ещё не запускался";
const kpis = d.kpis.map((k) =>
`<div class="kt-ai-kpi"><span class="label">${esc(k.label)}</span><span class="value">${esc(k.value)}</span><span class="hint">${esc(k.hint)}</span></div>`
).join("");
const banner = d.banner
? `<div class="kt-ai-banner" data-tone="${d.banner.tone}" style="display:flex;align-items:center;justify-content:space-between;gap:var(--kt-ai-space-4)">
<span>${esc(d.banner.text)}</span>
<button class="kt-ai-btn" data-variant="ghost" data-action="collect" style="flex-shrink:0">Обновить</button>
</div>`
: "";
const series = d.series || [];
const legend = ` <div class="pa-legend">
<span class="li"><i style="background:${C.primary}"></i> среднее изменение курсов, %</span>
<span class="li"><i style="background:var(--kt-ai-primary);opacity:.6"></i> средняя температура, °C</span>
<span class="li"><i style="background:var(--kt-ai-status-info-bg)"></i> осадки, мм (столбцы)</span>
</div>`;
const wx = (d.weatherNow || []).map((w) =>
`<div class="pa-wx"><span>${esc(w.city)}</span><span class="num">${w.min}…${w.max}°C · ${w.rain} мм</span></div>`
).join("") || `<div class="pa-empty">Нет данных за сегодня</div>`;
const table = `<div class="kt-ai-table-wrap" data-framed="true">
<table class="kt-ai-table">
<thead><tr><th>Валюта</th><th style="text-align:right">Курс, ₸</th><th style="text-align:right">Изменение</th></tr></thead>
<tbody>${d.latest.map((q) => {
const st = q.changePct > 0.12 ? "ok" : q.changePct < -0.12 ? "risk" : "muted";
return `<tr><td><strong>${esc(q.code)}</strong><div class="meta" style="font-size:var(--kt-ai-text-2xs);color:var(--kt-ai-fg-muted)">${esc(CODE_NAME[q.code] || q.code)}</div></td>
<td class="num" style="text-align:right">${Number(q.value).toLocaleString("ru-RU")}</td>
<td class="num" style="text-align:right">${pill((q.changePct > 0 ? "+" : "") + q.changePct.toFixed(2) + "%", st)}</td></tr>`;
}).join("")}</tbody>
</table></div>`;
const summary = (d.meta && d.meta.summary) ? esc(d.meta.summary) : "Сводка формируется…";
const chips = ['Итог за день', "Что двигалось сильнее всего?", "Риски погоды"].map((s) =>
`<button class="kt-ai-btn" data-size="sm" style="border-color:var(--kt-ai-border-strong)" data-action="ask" data-q="${esc(s)}">${esc(s)}</button>`
).join("");
const alerts = (d.alerts || []).map((a) => `<div class="kt-ai-banner" data-tone="${a.tone}" style="margin-bottom:var(--kt-ai-space-2)">${esc(a.text)}</div>`).join("");
return `
<div style="max-width:1200px;margin:0 auto">
${banner}
${alerts}
<div class="kt-ai-kpi-strip" style="margin-top:var(--kt-ai-space-5)">${kpis}</div>
<div class="kt-ai-card">
<div style="display:flex;justify-content:space-between;align-items:baseline">
<h3 class="kt-ai-h3">Динамика: курсов валют и осадки</h3>
<span class="kt-ai-meta" id="seriesRange">${series.length ? fmtDay(series[0].date) + " — " + fmtDay(series[series.length - 1].date) : ""}</span>
</div>
${linePlot(series)}
${legend}
</div>
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-5)">
<h3 class="kt-ai-h3">Погода сегодня · 4 региона</h3>
<div style="margin-top:var(--kt-ai-space-4)">${wx}</div>
</div>
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-5)">
<div style="display:flex;justify-content:space-between;align-items:baseline">
<h3 class="kt-ai-h3">Сводка ИИ-аналитика</h3>
<span class="kt-ai-meta">обновлено ${last}</span>
</div>
<p style="margin:var(--kt-ai-space-4) 0 0;line-height:1.7">${summary}</p>
<div style="display:flex;gap:var(--kt-ai-space-3);flex-wrap:wrap;margin-top:var(--kt-ai-space-5)">${chips}</div>
</div>
<h3 class="kt-ai-h3" style="margin:var(--kt-ai-space-7) 0 var(--kt-ai-space-3)">Последние данные</h3>
${table}
</div>`;
}
function vHistory() {
const d = DATA;
const quotes = (d && d.quotes) || [];
const byDate = {};
for (const q of quotes) (byDate[q.date] = byDate[q.date] || []).push(q);
const dates = Object.keys(byDate).sort().reverse().slice(0, 10);
const uniq = [...new Set(quotes.map((q) => q.code))].sort();
const rows = dates.map((date) => {
const cells = uniq.map((code) => {
const q = byDate[date].find((x) => x.code === code);
if (!q) return `<td class="num" style="text-align:right;color:var(--kt-ai-fg-faint)">—</td>`;
const st = q.changePct > 0.12 ? "ok" : q.changePct < -0.12 ? "risk" : "muted";
return `<td class="num" style="text-align:right;white-space:nowrap">${Number(q.value).toLocaleString("ru-RU")} ${pill((q.changePct > 0 ? "+" : "") + (q.changePct || 0).toFixed(1) + "%", st)}</td>`;
}).join("");
return `<tr><td><strong>${fmtDay(date)}</strong></td>${cells}</tr>`;
}).join("");
return `
<div style="max-width:1200px;margin:0 auto">
<div class="kt-ai-card">
<h3 class="kt-ai-h3">История по дням · последние 10</h3>
<div style="margin-top:var(--kt-ai-space-4)"></div>
<div class="kt-ai-table-wrap" data-framed="true">
<table class="kt-ai-table">
<thead><tr><th>Дата</th>${uniq.map((c) => `<th style="text-align:right">${c}, ₸</th>`).join("")}</tr></thead>
<tbody>${rows || `<tr><td colspan="5"><div class="pa-empty">Дат нет — нажмите «Обновить сейчас»</div></td></tr>`}</tbody>
</table>
</div>
</div>
</div>`;
}
function vAgents() {
const d = DATA;
const agents = d && d.meta && d.meta.lastRun ? [
{ name: "Сбор данных", role: "Сканирует открытые источники: курсы валют и погоду по регионам", lastRun: d.meta.lastRun },
{ name: "ИИ-аналитик", role: "Формирует ежедневную сводку и отвечает на вопросы", lastRun: d.meta.lastRun }
] : [];
const sources = d && d.sources || [];
return `
<div style="max-width:1200px;margin:0 auto">
<div class="kt-ai-card">
<h3 class="kt-ai-h3">Агенты</h3>
<div class="pa-src">${agents.map((a) => `
<div class="item">
<svg class="kt-icon" aria-hidden="true" style="color:var(--kt-ai-primary)"><use href="#sparkles"></use></svg>
<div><div class="nm">${esc(a.name)}</div><div class="sub">${esc(a.role)}</div></div>
<span class="kt-ai-meta" style="margin-left:auto">запущен: ${esc(a.lastRun)}</span>
</div>`).join("") || `<div class="pa-empty">Запустите агента кнопкой «Обновить сейчас».</div>`}</div>
</div>
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-5)">
<h3 class="kt-ai-h3">Источники данных</h3>
<div class="pa-src">${sources.map((s) => `
<div class="item">
<svg class="kt-icon" aria-hidden="true" style="color:${s.ok ? "var(--kt-ai-status-ok-fg)" : "var(--kt-ai-status-warn-fg)"}"><use href="${s.ok ? "check-circle" : "warning"}"></use></svg>
<div><div class="nm">${esc(s.name)}</div><div class="sub">${s.ok ? esc(s.count + " записей сегодня") : esc(s.note || s.error || "нет данных")}</div></div>
</div>`).join("") || `<div class="pa-empty">Источники появятся после первого сбора.</div>`}</div>
</div>
</div>`;
}
const VIEWS = { overview: vOverview, history: vHistory, agents: vAgents };
const TITLES = { overview: ["Обзор", "открытые источники"], history: ["История", "еженедельно"], agents: ["Агенты", "инфраструктура"] };
function setView(v) {
VIEW = v;
$("#pageTitle").textContent = TITLES[v][0];
$("#pageSub").textContent = TITLES[v][1];
if (!DATA) return;
$("#view").innerHTML = VIEWS[v]();
document.querySelectorAll(".kt-ai-nav-item").forEach((n, i) => n.dataset.active = ["overview", "history", "agents"][i] === v ? "true" : "false");
bindActions();
}
function bindActions() {
document.querySelectorAll("[data-action='collect']").forEach((b) => (b.onclick = runCollect));
document.querySelectorAll("[data-action='ask']").forEach((b) => (b.onclick = () => ask(b.dataset.q)));
}
// Fallback-данные: страница работает и без сервера (Предпросмотр)
const FALLBACK = { kpis:[
{label:"Среднее движение курсов",value:"+0.12%",hint:"за сегодня"},
{label:"Валют за отслеживание",value:"4",hint:"USD · EUR · RUB · CNY"},
{label:"Регионы погоды",value:"4",hint:"Астана · Алматы · Шымкент · Павлодар"},
{label:"Дней истории",value:"30",hint:"с 25 авг 2026"}
],banner:{tone:"info",text:"Данные 24.09.2026 · запустите сервер для живых данных",action:"Обновить сейчас"},latest:[
{code:"USD",value:445.6,changePct:0,unit:"тенге за 1 доллар"},
{code:"EUR",value:508.1,changePct:0,unit:"тенге за 1 евро"},
{code:"RUB",value:5.294,changePct:0,unit:"тенге за 1 рубль"},
{code:"CNY",value:66.7,changePct:0,unit:"тенге за 1 юань"}
],series:[{date:"2026-08-25",avg:-0.05,rain:1.2,tmin:11,tmax:26},{date:"2026-08-26",avg:+0.02,rain:0.1,tmin:13,tmax:27},{date:"2026-08-27",avg:-0.03,rain:0,tmin:14,tmax:31},{date:"2026-08-28",avg:+0.08,rain:0.7,tmin:15,tmax:30},{date:"2026-08-29",avg:-0.01,rain:2,tmin:10,tmax:27},{date:"2026-08-30",avg:+0.04,rain:0.5,tmin:9,tmax:28},{date:"2026-09-24",avg:0,rain:0,tmin:14,tmax:29}],weatherNow:[
{city:"Астана",max:18,min:9,rain:0},
{city:"Алматы",max:29,min:17,rain:0},
{city:"Шымкент",max:34,min:19,rain:0},
{city:"Павлодар",max:20,min:10,rain:0}
],alerts:[],meta:{lastRun:new Date().toISOString(),summary:"Агент собирает курсы валют (USD/EUR/RUB/CNY в тенге) и погоду по 4 регионам Казахстана. Данные обновляются ежедневно. Для анализа агро-цен подключите источники vetlex.kz или ALAPI."},quotes:[],sources:[{id:"fx",name:"Forex open.er-api.com",ok:true,count:4},{id:"weather",name:"open-meteo.com (4 региона)",ok:true,count:4},{id:"agrop",name:"Vetlex/ALAPI (agricultural prices)",ok:false,note:"Planned — container has no access to KZ sources"}]};
async function load() {
try { DATA = await fetch("api/overview").then((r) => r.json()); }
catch (e) { DATA = FALLBACK; }
if (!DATA || !DATA.kpis) DATA = FALLBACK;
setView(VIEW);
}
let collecting = false;
async function runCollect() {
if (collecting) return;
collecting = true;
const b = $("#btnCollect");
if (b) { b.disabled = true; b.innerHTML = '<svg class="kt-icon" aria-hidden="true"><use href="#sync"></use></svg> Собираю…'; }
try {
const j = await fetch("api/collect", { method: "POST" }).then((r) => r.json());
if (j.error) toast("Ошибка сбора: " + j.error);
else toast("Источники обновлены, ИИ пишет сводку…");
} catch (e) { toast("Сбор не удался: " + e.message); }
await load();
if (b) { b.disabled = false; b.innerHTML = '<svg class="kt-icon" aria-hidden="true"><use href="#refresh"></use></svg> Обновить сейчас'; }
collecting = false;
}
// ---------- Чат ----------
function addChat(role, text) {
const log = $("#chatLog");
if (role === "user") log.insertAdjacentHTML("beforeend", `<div class="pa-user">${esc(text)}</div>`);
else {
const id = "m" + Date.now();
log.insertAdjacentHTML("beforeend", `<div><div class="pa-ai-label">Ассистент</div><div class="pa-ai" id="${id}">${esc(text || "Думаю…")}</div></div>`);
}
log.scrollTop = log.scrollHeight;
}
function setChat(id, text, err) {
const el = document.querySelector(id);
if (el) { el.textContent = text; if (err) el.classList.add("err"); }
}
async function ask(q) {
if (!q) return;
$("#drawer").style.display = "";
addChat("user", q);
const log = $("#chatLog");
const id = "m" + Date.now();
log.insertAdjacentHTML("beforeend", `<div><div class="pa-ai-label">Ассистент</div><div class="pa-ai" id="${id}">Думаю…</div></div>`);
log.scrollTop = log.scrollHeight;
try {
const j = await fetch("api/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ q }) }).then((r) => r.json());
setChat("#" + id, j.answer || j.error || "Нет ответа", !!j.error);
} catch (e) {
setChat("#" + id, "ИИ временно недоступен: " + e.message, true);
}
log.scrollTop = log.scrollHeight;
}
// ---------- Инициализация ----------
$("#btnCollect").onclick = runCollect;
$("#drawerClose").onclick = () => ($("#drawer").style.display = "none");
$("#chatSend").onclick = () => { const v = $("#chatInput").value.trim(); if (v) { $("#chatInput").value = ""; ask(v); } };
$("#chatInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { const v = e.target.value.trim(); if (v) { e.target.value = ""; ask(v); } } });
$("#askInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { const v = e.target.value.trim(); if (v) { e.target.value = ""; ask(v); } } });
document.querySelectorAll(".kt-ai-nav-item").forEach((n, i) => (n.onclick = (e) => { e.preventDefault(); setView(["overview", "history", "agents"][i]); }));
load();
</script>
</body>
</html>