235 lines
9.1 KiB
JavaScript
235 lines
9.1 KiB
JavaScript
const loginView = document.querySelector("#login-view");
|
|
const chatView = document.querySelector("#chat-view");
|
|
const loginForm = document.querySelector("#login-form");
|
|
const loginError = document.querySelector("#login-error");
|
|
const messages = document.querySelector("#messages");
|
|
const chatForm = document.querySelector("#chat-form");
|
|
const messageInput = document.querySelector("#message");
|
|
const sendButton = document.querySelector("#send");
|
|
const logoutButton = document.querySelector("#logout");
|
|
const activeFocus = document.querySelector("#active-focus");
|
|
const todayList = document.querySelector("#today-list");
|
|
const refreshToday = document.querySelector("#refresh-today");
|
|
const historyList = document.querySelector("#history-list");
|
|
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
|
|
|
|
let token = sessionStorage.getItem("domovoy.chat.token") || "";
|
|
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
|
let localHistory = [];
|
|
|
|
function showChat() {
|
|
loginView.hidden = true;
|
|
chatView.hidden = false;
|
|
if (!messages.children.length) {
|
|
appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов.");
|
|
}
|
|
messageInput.focus();
|
|
}
|
|
|
|
function showLogin() {
|
|
chatView.hidden = true;
|
|
loginView.hidden = false;
|
|
}
|
|
|
|
function appendMessage(kind, text, meta = "") {
|
|
const node = document.createElement("div");
|
|
node.className = `message ${kind}`;
|
|
node.textContent = text;
|
|
if (meta) {
|
|
const small = document.createElement("small");
|
|
small.textContent = meta;
|
|
node.appendChild(small);
|
|
}
|
|
messages.appendChild(node);
|
|
messages.scrollTop = messages.scrollHeight;
|
|
localHistory.push({kind, text, meta, at: new Date()});
|
|
renderHistory();
|
|
}
|
|
|
|
function updateActiveFocus(value, provenance = "") {
|
|
if (!activeFocus) return;
|
|
activeFocus.textContent = value ? `Сейчас обсуждаем: ${value}` : "";
|
|
activeFocus.hidden = !value;
|
|
}
|
|
|
|
async function api(path, options = {}) {
|
|
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
const response = await fetch(path, {...options, headers});
|
|
if (!response.ok) throw new Error(await response.text());
|
|
return response.json();
|
|
}
|
|
|
|
function humanMeta(result) {
|
|
if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам.";
|
|
if (result.capability_status && result.capability_status !== "AVAILABLE") return "Возможность пока не подключена.";
|
|
if (result.work_planned) return "Я разложил это как работу.";
|
|
return "";
|
|
}
|
|
|
|
function renderHistory() {
|
|
if (!historyList) return;
|
|
historyList.replaceChildren();
|
|
const items = localHistory.slice(-30).reverse();
|
|
if (!items.length) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "empty-state";
|
|
empty.textContent = "История появится после сообщений в этом окне.";
|
|
historyList.appendChild(empty);
|
|
return;
|
|
}
|
|
for (const item of items) {
|
|
const row = document.createElement("article");
|
|
row.className = `history-item ${item.kind}`;
|
|
const title = document.createElement("strong");
|
|
title.textContent = item.kind === "user" ? "Вы" : "Домовой";
|
|
const body = document.createElement("p");
|
|
body.textContent = item.text;
|
|
const time = document.createElement("span");
|
|
time.textContent = item.at.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
|
|
row.append(title, body, time);
|
|
historyList.appendChild(row);
|
|
}
|
|
}
|
|
|
|
async function loadToday() {
|
|
if (!todayList || !token) return;
|
|
todayList.replaceChildren();
|
|
const loading = document.createElement("p");
|
|
loading.className = "empty-state";
|
|
loading.textContent = "Смотрю, что требует внимания...";
|
|
todayList.appendChild(loading);
|
|
try {
|
|
const result = await api("/api/today", {method: "GET"});
|
|
todayList.replaceChildren();
|
|
const items = result.Items || result.items || [];
|
|
if (!items.length) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "empty-state";
|
|
empty.textContent = "Сейчас ничего срочного не требует внимания.";
|
|
todayList.appendChild(empty);
|
|
return;
|
|
}
|
|
for (const item of items) {
|
|
const card = document.createElement("article");
|
|
card.className = "attention-card";
|
|
const when = document.createElement("time");
|
|
const stamp = item.WhenUnixMillis || item.when_unix_millis || 0;
|
|
when.textContent = stamp ? new Date(stamp).toLocaleString("ru-RU", {day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit"}) : "сейчас";
|
|
const title = document.createElement("h2");
|
|
title.textContent = item.Title || item.title || "Событие Домового";
|
|
const state = document.createElement("p");
|
|
state.textContent = item.HumanState || item.human_state || "ожидает внимания";
|
|
const source = document.createElement("small");
|
|
source.textContent = [item.SourceDescription || item.source_description, item.DeliveryState || item.delivery_state].filter(Boolean).join(" · ");
|
|
const hintText = item.ResponseHint || item.response_hint || "";
|
|
const actions = document.createElement("div");
|
|
actions.className = "attention-actions";
|
|
for (const [label, text] of [["Готово", "Готово"], ["Отложить", "Отложи на 10 минут"], ["Отменить", "Отмени"]]) {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.textContent = label;
|
|
button.addEventListener("click", async () => {
|
|
await sendChatMessage(text, {silentUser: false});
|
|
await loadToday();
|
|
activateTab("chat-panel");
|
|
});
|
|
actions.appendChild(button);
|
|
}
|
|
if (hintText) {
|
|
const hint = document.createElement("small");
|
|
hint.textContent = hintText;
|
|
card.append(when, title, state, source, hint, actions);
|
|
} else {
|
|
card.append(when, title, state, source, actions);
|
|
}
|
|
todayList.appendChild(card);
|
|
}
|
|
} catch {
|
|
todayList.replaceChildren();
|
|
const error = document.createElement("p");
|
|
error.className = "empty-state error";
|
|
error.textContent = "Внимание сейчас недоступно. Чат можно продолжать.";
|
|
todayList.appendChild(error);
|
|
}
|
|
}
|
|
|
|
async function sendChatMessage(text, options = {}) {
|
|
const clean = text.trim();
|
|
if (!clean) return;
|
|
if (!options.silentUser) appendMessage("user", clean);
|
|
sendButton.disabled = true;
|
|
try {
|
|
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean})});
|
|
conversationID = result.conversation_id;
|
|
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
|
|
updateActiveFocus(result.active_focus, result.project_provenance);
|
|
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result));
|
|
if (/что сейчас требует моего внимания/i.test(clean)) {
|
|
await loadToday();
|
|
activateTab("today-panel");
|
|
}
|
|
} catch {
|
|
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
|
} finally {
|
|
sendButton.disabled = false;
|
|
messageInput.focus();
|
|
}
|
|
}
|
|
|
|
function activateTab(panelID) {
|
|
for (const tab of navTabs) tab.classList.toggle("active", tab.dataset.tab === panelID);
|
|
for (const panel of document.querySelectorAll(".surface-panel")) {
|
|
const active = panel.id === panelID;
|
|
panel.hidden = !active;
|
|
panel.classList.toggle("active-panel", active);
|
|
}
|
|
if (panelID === "today-panel") loadToday();
|
|
if (panelID === "history-panel") renderHistory();
|
|
}
|
|
|
|
loginForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
loginError.textContent = "";
|
|
const username = document.querySelector("#username").value;
|
|
const password = document.querySelector("#password").value;
|
|
try {
|
|
const result = await api("/api/session", {method: "POST", body: JSON.stringify({username, password})});
|
|
token = result.Token || result.token;
|
|
sessionStorage.setItem("domovoy.chat.token", token);
|
|
showChat();
|
|
} catch {
|
|
loginError.textContent = "Вход не выполнен";
|
|
}
|
|
});
|
|
|
|
chatForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const text = messageInput.value.trim();
|
|
if (!text) return;
|
|
messageInput.value = "";
|
|
await sendChatMessage(text);
|
|
});
|
|
|
|
for (const tab of navTabs) {
|
|
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
|
|
}
|
|
|
|
if (refreshToday) refreshToday.addEventListener("click", loadToday);
|
|
|
|
logoutButton.addEventListener("click", () => {
|
|
token = "";
|
|
conversationID = "";
|
|
sessionStorage.removeItem("domovoy.chat.token");
|
|
sessionStorage.removeItem("domovoy.chat.conversation");
|
|
messages.replaceChildren();
|
|
updateActiveFocus("");
|
|
showLogin();
|
|
});
|
|
|
|
if (token) {
|
|
showChat();
|
|
} else {
|
|
showLogin();
|
|
}
|