335 lines
13 KiB
JavaScript
335 lines
13 KiB
JavaScript
// Теория под себя — интерактив: сценарии, чат Töre, форма ожидания
|
||
(function () {
|
||
"use strict";
|
||
|
||
var STORE_KEY = "tore-theory-chat-v1";
|
||
var WAIT_KEY = "tore-theory-wait-v1";
|
||
var MAX_STORED = 30;
|
||
|
||
var SCENARIOS = {
|
||
habits: {
|
||
title: "Привычки",
|
||
question: "Покажи, какие мои утренние привычки повторяются каждый день.",
|
||
answer:
|
||
"Собрал за 14 дней подряд: подъём до 08:00 — 12 раз, прогулка 20 минут — 10, кофе до работы — 14 из 14.",
|
||
points: [
|
||
"Самая устойчивая связка: подъём + прогулка до 09:00.",
|
||
"В дни без прогулки уровень энергии к обеду заметно ниже.",
|
||
"Предлагаю закрепить прогулку в 08:15 на этой неделе."
|
||
],
|
||
followups: ["Что добавить в утро?", "Как выглядит типичный день?"]
|
||
},
|
||
time: {
|
||
title: "Распределение времени",
|
||
question: "Куда уходит больше всего времени по будням?",
|
||
answer:
|
||
"Разложил 24 рабочих часа по блокам. Топ: фокус-задачи — 9 ч, переписки и звонки — 6 ч, обучение — 3 ч.",
|
||
points: [
|
||
"Больше всего «съедает» переписка по средам и пятницам.",
|
||
"Фокус-задачи лучше всего получаются до 11:00.",
|
||
"Если собрать переписку в один слот в 13:00, высвобождается около 1,5 ч."
|
||
],
|
||
followups: [
|
||
"Где я теряю время?",
|
||
"Что сдвинуть, чтобы найти час на обучение?"
|
||
]
|
||
},
|
||
results: {
|
||
title: "Результаты за неделю",
|
||
question: "Сравни мою неделю с прошлой: что выросло?",
|
||
answer:
|
||
"Сверил две недели по трём метрикам: закрытых задач 18 против 14, новых навыков в работе 2 против 0, просроченных — 1 против 3.",
|
||
points: [
|
||
"Рост стабильный третий период подряд: в среднем +12% по закрытым задачам.",
|
||
"Просрочки упали после того, как приоритеты стали меняться только утром.",
|
||
"На следующей неделе стоит добавить ещё один глубокий фокус-слот."
|
||
],
|
||
followups: ["Составь план на след. неделю", "Что мешает росту?"]
|
||
}
|
||
};
|
||
|
||
var GENERAL_ANSWERS = [
|
||
{
|
||
keys: ["привычк", "утро", "подъём"],
|
||
text: {
|
||
answer: "Посмотрел по дням: устойчивых повторов больше, чем кажется. Твоя «сильная» зона — начало дня.",
|
||
points: [
|
||
"Регулярнее всего повторяются подъём, кофе и короткая разминка.",
|
||
"Новые вещи закрепляются, если ставить их сразу после устойчивого шага."
|
||
]
|
||
}
|
||
},
|
||
{
|
||
keys: ["время", "час", "растор", "потеря"],
|
||
text: {
|
||
answer: "Посчитаю по блокам времени: где оно собирается, а где разбивается на мелкие куски.",
|
||
points: [
|
||
"Длинные фокус-блоки редки: чаще 25–35 минут до прерывания.",
|
||
"Лучший приём недели — собрать мелкие дела в один вечерний слот."
|
||
]
|
||
}
|
||
},
|
||
{
|
||
keys: ["результат", "недел", "рост", "показател", "сравн"],
|
||
text: {
|
||
answer: "По последним периодам динамика вверх: ключевые цифры растут второй период кряду.",
|
||
points: [
|
||
"Рост ровный, без провалов — значит, методика сработала.",
|
||
"Следующая точка роста — добавить один новый фокус-слот."
|
||
]
|
||
}
|
||
},
|
||
{
|
||
keys: ["план", "рекомендац", "что делать", "совет"],
|
||
text: {
|
||
answer: "Вот короткая рекомендация на ближайшие 7 дней, под твой текущий ритм.",
|
||
points: [
|
||
"Фиксировать утро: два шага, без выбора и откладывания.",
|
||
"Один большой блок работы до обеда, без переписок.",
|
||
"В воскресенье — 15 минут на сверку: что сработало, что нет."
|
||
],
|
||
followups: ["Составь план на неделю", "Что мешает росту?"]
|
||
}
|
||
},
|
||
{
|
||
keys: ["план", "след"],
|
||
text: {
|
||
answer: "Собрал план на неделю по твоим метрикам.",
|
||
points: [
|
||
"Пн–чт: по одному фокус-блоку на главную задачу.",
|
||
"Пт: разбор недели, 20 минут, без новых задач."
|
||
],
|
||
}
|
||
}
|
||
];
|
||
|
||
|
||
function byId(id) { return document.getElementById(id); }
|
||
function esc(s) {
|
||
return String(s).replace(/[&<>"]/g, function (c) {
|
||
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
|
||
});
|
||
}
|
||
|
||
function loadChat() {
|
||
try {
|
||
var raw = localStorage.getItem(STORE_KEY);
|
||
if (!raw) return [];
|
||
var arr = JSON.parse(raw);
|
||
return Array.isArray(arr) ? arr : [];
|
||
} catch (e) { return []; }
|
||
}
|
||
|
||
function saveChat(list) {
|
||
try {
|
||
if (list.length > MAX_STORED) list = list.slice(list.length - MAX_STORED);
|
||
localStorage.setItem(STORE_KEY, JSON.stringify(list));
|
||
} catch (e) { /* квота — молча пропускаем */ }
|
||
}
|
||
|
||
function msgHtml(m) {
|
||
if (m.role === "user") {
|
||
return '<div class="tk-msg tk-msg-user"><div class="tk-msg-head">Ты</div>' + esc(m.text) + "</div>";
|
||
}
|
||
var html = '<div class="tk-msg tk-msg-ai"><div class="tk-msg-head">' +
|
||
'<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#sparkles"></use></svg>' +
|
||
"Töre · " + esc(m.time) + "</div>";
|
||
if (m.text) html += "<p>" + esc(m.text) + "</p>";
|
||
if (m.points && m.points.length) {
|
||
html += '<ul class="tk-msg-points">';
|
||
for (var i = 0; i < m.points.length; i++) html += "<li>" + esc(m.points[i]) + "</li>";
|
||
html += "</ul>";
|
||
}
|
||
if (m.followups && m.followups.length) {
|
||
html += '<div class="tk-msg-actions">';
|
||
for (var j = 0; j < m.followups.length; j++) {
|
||
html += '<button class="tk-prompt-chip" data-q="' + esc(m.followups[j]) + '">' + esc(m.followups[j]) + "</button>";
|
||
}
|
||
html += "</div>";
|
||
}
|
||
return html + "</div>";
|
||
}
|
||
|
||
function appendMsg(m, stored) {
|
||
var log = byId("chat-log");
|
||
if (!log) return;
|
||
var div = document.createElement("div");
|
||
div.innerHTML = msgHtml(m);
|
||
var node = div.firstChild;
|
||
log.appendChild(node);
|
||
log.scrollTop = log.scrollHeight;
|
||
if (stored) {
|
||
var list = loadChat();
|
||
list.push(m);
|
||
saveChat(list);
|
||
}
|
||
}
|
||
|
||
function renderStored(list) {
|
||
var log = byId("chat-log");
|
||
if (!log) return;
|
||
log.innerHTML = "";
|
||
for (var i = 0; i < list.length; i++) {
|
||
var div = document.createElement("div");
|
||
div.innerHTML = msgHtml(list[i]);
|
||
log.appendChild(div.firstChild);
|
||
}
|
||
log.scrollTop = log.scrollHeight;
|
||
}
|
||
|
||
function answerFor(text) {
|
||
var t = text.toLowerCase();
|
||
for (var i = 0; i < GENERAL_ANSWERS.length; i++) {
|
||
var keys = GENERAL_ANSWERS[i].keys;
|
||
for (var j = 0; j < keys.length; j++) {
|
||
if (t.indexOf(keys[j]) !== -1) return GENERAL_ANSWERS[i].text;
|
||
}
|
||
}
|
||
return FALLBACK;
|
||
}
|
||
|
||
function nowTime() {
|
||
var d = new Date();
|
||
var hh = ("0" + d.getHours()).slice(-2);
|
||
var mm = ("0" + d.getMinutes()).slice(-2);
|
||
return hh + ":" + mm;
|
||
}
|
||
|
||
function sendUser(text) {
|
||
if (!text) return;
|
||
appendMsg({ role: "user", text: text, time: nowTime() }, true);
|
||
setTimeout(function () {
|
||
var res = answerFor(text);
|
||
var msg = { role: "ai", text: res.answer, points: res.points || null, time: nowTime() };
|
||
if (res.points) msg.followups = res.followups || null;
|
||
appendMsg(msg, true);
|
||
}, 450);
|
||
}
|
||
|
||
function initChat() {
|
||
var log = byId("chat-log");
|
||
if (!log) return;
|
||
var stored = loadChat();
|
||
if (stored.length) {
|
||
renderStored(stored);
|
||
} else {
|
||
appendMsg({
|
||
role: "ai",
|
||
text: "Привет! Я Töre — ИИ-аналитик. Выбери сценарий выше или спроси меня о своих данных: привычки, время, результаты.",
|
||
points: null,
|
||
time: nowTime()
|
||
}, false);
|
||
}
|
||
var form = byId("chat-form");
|
||
var input = byId("chat-input");
|
||
if (form && input) {
|
||
form.addEventListener("submit", function (e) {
|
||
e.preventDefault();
|
||
var v = input.value.trim();
|
||
if (!v) return;
|
||
input.value = "";
|
||
input.style.height = "auto";
|
||
sendUser(v);
|
||
});
|
||
input.addEventListener("keydown", function (e) {
|
||
if (e.key === "Enter" && !e.shiftKey) {
|
||
e.preventDefault();
|
||
form.requestSubmit();
|
||
}
|
||
});
|
||
input.addEventListener("input", function () {
|
||
input.style.height = "auto";
|
||
input.style.height = Math.min(input.scrollHeight, 120) + "px";
|
||
});
|
||
}
|
||
// Чипы-продолжения: делегирование
|
||
log.addEventListener("click", function (e) {
|
||
var btn = e.target;
|
||
while (btn && btn.tagName !== "BUTTON") btn = btn.parentNode;
|
||
if (!btn || !btn.classList || !btn.classList.contains("tk-prompt-chip")) return;
|
||
var q = btn.getAttribute("data-q");
|
||
if (q) sendUser(q);
|
||
});
|
||
}
|
||
|
||
function initScenarios() {
|
||
var tabs = document.querySelectorAll("#scenario-list .kt-ai-tab");
|
||
for (var i = 0; i < tabs.length; i++) {
|
||
(function (tab) {
|
||
tab.addEventListener("click", function () {
|
||
var id = tab.getAttribute("data-scenario");
|
||
for (var k = 0; k < tabs.length; k++) tabs[k].setAttribute("data-active", "false");
|
||
tab.setAttribute("data-active", "true");
|
||
var s = SCENARIOS[id];
|
||
if (!s) return;
|
||
sendUser(s.question);
|
||
});
|
||
})(tabs[i]);
|
||
}
|
||
}
|
||
|
||
function initWait() {
|
||
var input = byId("wait-email");
|
||
var btn = byId("wait-submit");
|
||
var note = byId("wait-note");
|
||
var field = input ? input.closest(".kt-ai-field") : null;
|
||
if (!input || !btn) return;
|
||
var saved = "";
|
||
try { saved = localStorage.getItem(WAIT_KEY) || ""; } catch (e) {}
|
||
if (saved) {
|
||
input.value = saved;
|
||
if (note) note.textContent = "Сохранено: " + saved + ". При запуске пришлём уведомление.";
|
||
}
|
||
btn.addEventListener("click", function () {
|
||
var v = input.value.trim();
|
||
if (field) field.setAttribute("data-invalid", "false");
|
||
if (!v) {
|
||
if (field) field.setAttribute("data-invalid", "true");
|
||
input.focus();
|
||
if (note) note.textContent = "Введите адрес, чтобы сохранить заявку.";
|
||
return;
|
||
}
|
||
if (v.indexOf("@") === -1 || v.indexOf("@") === 0 || v.indexOf("@") === v.length - 1) {
|
||
if (field) field.setAttribute("data-invalid", "true");
|
||
if (note) note.textContent = "Похоже, в адресе опечатка — проверь формат.";
|
||
return;
|
||
}
|
||
try { localStorage.setItem(WAIT_KEY, v); } catch (e) {}
|
||
if (note) note.textContent = "Сохранено: " + v + ". При запуске пришлём уведомление.";
|
||
});
|
||
input.addEventListener("input", function () {
|
||
if (field) field.setAttribute("data-invalid", "false");
|
||
});
|
||
}
|
||
|
||
function initNav() {
|
||
var links = document.querySelectorAll("a[href^='#']");
|
||
for (var i = 0; i < links.length; i++) {
|
||
(function (a) {
|
||
a.addEventListener("click", function () {
|
||
var id = a.getAttribute("href");
|
||
if (id.length > 1) {
|
||
var el = byId(id.slice(1));
|
||
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}
|
||
});
|
||
})(links[i]);
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
initChat();
|
||
initScenarios();
|
||
initWait();
|
||
initNav();
|
||
var hint = byId("chat-hint");
|
||
if (hint) hint.textContent = "Диалог сохраняется в этом браузере — можно вернуться позже.";
|
||
}
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|