412 lines
25 KiB
HTML
412 lines
25 KiB
HTML
<!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) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[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>
|