552 lines
24 KiB
JavaScript
552 lines
24 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 sendLabel = document.querySelector("#send-label");
|
|
const messageCount = document.querySelector("#message-count");
|
|
const logoutButton = document.querySelector("#logout");
|
|
const passwordDialog = document.querySelector("#password-dialog");
|
|
const passwordForm = document.querySelector("#password-form");
|
|
const passwordClose = document.querySelector("#password-close");
|
|
const passwordLogout = document.querySelector("#password-logout");
|
|
const passwordStatus = document.querySelector("#password-status");
|
|
const activeFocus = document.querySelector("#active-focus");
|
|
const rightFocus = document.querySelector("#right-focus");
|
|
const headerFocus = document.querySelector("#header-focus");
|
|
const navToggle = document.querySelector("#nav-toggle");
|
|
const chatLayout = document.querySelector(".chat-layout");
|
|
const contextToggle = document.querySelector("#context-toggle");
|
|
const contextClose = document.querySelector("#context-close");
|
|
const contextBackdrop = document.querySelector("#context-backdrop");
|
|
const contextDrawer = document.querySelector("#context-drawer");
|
|
const newConversationButton = document.querySelector(".new-conversation");
|
|
const cognitiveStatus = document.querySelector("#cognitive-status");
|
|
const cognitiveConsoleLink = document.querySelector("#cognitive-console-link");
|
|
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"));
|
|
const aiDataForm = document.querySelector("#ai-data-form");
|
|
const aiDataInput = document.querySelector("#ai-data");
|
|
const aiDataError = document.querySelector("#ai-data-error");
|
|
const attachToggle = document.querySelector("#attach-toggle");
|
|
const attachmentMenu = document.querySelector("#attachment-menu");
|
|
const photoTrigger = document.querySelector("#photo-trigger");
|
|
const micTrigger = document.querySelector("#mic-trigger");
|
|
const sendIcon = document.querySelector("#send-icon");
|
|
const stopIcon = document.querySelector("#stop-icon");
|
|
|
|
let token = sessionStorage.getItem("domovoy.chat.token") || "";
|
|
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
|
let localHistory = [];
|
|
let cognitiveStatusTimer = null;
|
|
let activeRequestController = null;
|
|
let composingMessage = false;
|
|
|
|
function resizeComposerInput() {
|
|
if (!messageInput) return;
|
|
messageInput.style.height = "auto";
|
|
const maxHeight = 178;
|
|
messageInput.style.height = `${Math.min(messageInput.scrollHeight, maxHeight)}px`;
|
|
messageInput.style.overflowY = messageInput.scrollHeight > maxHeight ? "auto" : "hidden";
|
|
if (messageCount) messageCount.textContent = `${messageInput.value.length.toLocaleString("ru-RU")} / 16 384`;
|
|
}
|
|
|
|
function isChatBusy() {
|
|
return sendButton?.getAttribute("aria-busy") === "true";
|
|
}
|
|
|
|
function setChatBusy(busy) {
|
|
if (!sendButton) return;
|
|
sendButton.setAttribute("aria-busy", busy ? "true" : "false");
|
|
sendButton.setAttribute("aria-label", busy ? "Остановить обработку" : "Отправить");
|
|
sendButton.title = busy ? "Остановить обработку" : "Отправить";
|
|
if (sendLabel) sendLabel.textContent = busy ? "Остановить обработку" : "Отправить";
|
|
if (sendIcon) sendIcon.hidden = busy;
|
|
if (stopIcon) stopIcon.hidden = !busy;
|
|
}
|
|
|
|
class ChatApiError extends Error {
|
|
constructor(status, detail = "") {
|
|
super(detail || `chat api returned ${status}`);
|
|
this.name = "ChatApiError";
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
function containsSecretShapedText(value) {
|
|
return /(-----BEGIN [A-Z ]+PRIVATE KEY-----|\b(?:password|passwd|пароль|token|токен|api[_ -]?key|секрет|ключ)\s*[:=]|\bsk-[a-zA-Z0-9_-]{12,})/i.test(value);
|
|
}
|
|
|
|
function showAIDataError(message = "") {
|
|
if (!aiDataError) return;
|
|
aiDataError.textContent = message;
|
|
aiDataError.hidden = !message;
|
|
}
|
|
|
|
function renderCognitiveStatus(state) {
|
|
if (!cognitiveStatus) return;
|
|
const normalized = String(state || "UNKNOWN").toUpperCase();
|
|
let displayState = "degraded";
|
|
if (normalized === "AVAILABLE") displayState = "available";
|
|
if (normalized === "UNAVAILABLE") displayState = "unavailable";
|
|
const labels = {
|
|
available: "канал мышления доступен",
|
|
degraded: "канал мышления работает с ограничениями",
|
|
unavailable: "Домовой рядом · основной канал недоступен",
|
|
};
|
|
cognitiveStatus.dataset.state = displayState;
|
|
cognitiveStatus.textContent = labels[displayState];
|
|
cognitiveStatus.title = displayState === "available" ? "Канал доступен. Нажмите, чтобы проверить ещё раз" : "Проверить канал ещё раз";
|
|
if (cognitiveConsoleLink) cognitiveConsoleLink.hidden = displayState === "available";
|
|
}
|
|
|
|
async function loadCognitiveStatus() {
|
|
if (!cognitiveStatus || !token) return;
|
|
cognitiveStatus.dataset.state = "checking";
|
|
cognitiveStatus.textContent = "проверяю канал мышления";
|
|
try {
|
|
const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"});
|
|
renderCognitiveStatus(result.State || result.state || "UNKNOWN");
|
|
} catch (error) {
|
|
// Cognitive health is an auxiliary probe. Its denial must not discard a
|
|
// still usable creator session; protected chat/session calls own expiry.
|
|
renderCognitiveStatus("UNAVAILABLE");
|
|
}
|
|
}
|
|
|
|
function startCognitiveStatusPolling() {
|
|
if (cognitiveStatusTimer) return;
|
|
loadCognitiveStatus();
|
|
cognitiveStatusTimer = window.setInterval(loadCognitiveStatus, 30000);
|
|
}
|
|
|
|
function stopCognitiveStatusPolling() {
|
|
if (!cognitiveStatusTimer) return;
|
|
window.clearInterval(cognitiveStatusTimer);
|
|
cognitiveStatusTimer = null;
|
|
}
|
|
|
|
function showChat() {
|
|
loginView.hidden = true;
|
|
chatView.hidden = false;
|
|
startCognitiveStatusPolling();
|
|
messageInput.focus();
|
|
resizeComposerInput();
|
|
}
|
|
|
|
function showLogin() {
|
|
stopCognitiveStatusPolling();
|
|
chatView.hidden = true;
|
|
loginView.hidden = false;
|
|
}
|
|
|
|
function appendMessage(kind, text, meta = "", media = null) {
|
|
const node = document.createElement("div");
|
|
node.className = `message ${kind}`;
|
|
const head = document.createElement("div");
|
|
head.className = "message-head";
|
|
const role = document.createElement("strong");
|
|
role.className = "message-role";
|
|
role.textContent = kind === "user" ? "Вы" : "Домовой";
|
|
const time = document.createElement("time");
|
|
time.className = "message-time";
|
|
const now = new Date();
|
|
time.dateTime = now.toISOString();
|
|
time.textContent = now.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
|
|
head.append(role, time);
|
|
node.appendChild(head);
|
|
const content = document.createElement("div");
|
|
content.className = "message-content";
|
|
content.textContent = text;
|
|
node.appendChild(content);
|
|
if (meta) {
|
|
const small = document.createElement("small");
|
|
small.textContent = meta;
|
|
node.appendChild(small);
|
|
}
|
|
if (media?.data && media?.contentType) {
|
|
const image = document.createElement("img");
|
|
image.className = "generated-image";
|
|
image.alt = text || "Сгенерированное изображение";
|
|
image.src = `data:${media.contentType};base64,${media.data}`;
|
|
node.appendChild(image);
|
|
}
|
|
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;
|
|
if (rightFocus) rightFocus.textContent = value || "не выбран";
|
|
if (headerFocus) headerFocus.textContent = value || "Новый разговор";
|
|
}
|
|
|
|
function setContextOpen(open) {
|
|
if (!contextDrawer) return;
|
|
contextDrawer.classList.toggle("drawer-open", open);
|
|
contextDrawer.setAttribute("aria-hidden", String(!open));
|
|
if (contextToggle) contextToggle.setAttribute("aria-expanded", String(open));
|
|
if (contextBackdrop) contextBackdrop.hidden = !open;
|
|
}
|
|
|
|
if (contextToggle) contextToggle.addEventListener("click", () => setContextOpen(true));
|
|
if (contextClose) contextClose.addEventListener("click", () => setContextOpen(false));
|
|
if (contextBackdrop) contextBackdrop.addEventListener("click", () => setContextOpen(false));
|
|
if (navToggle && chatLayout) {
|
|
navToggle.addEventListener("click", () => {
|
|
const mobile = window.matchMedia("(max-width: 780px)").matches;
|
|
const open = mobile ? chatLayout.classList.toggle("nav-open") : !chatLayout.classList.toggle("nav-collapsed");
|
|
navToggle.setAttribute("aria-expanded", String(open));
|
|
navToggle.title = open ? "Скрыть навигацию" : "Показать навигацию";
|
|
});
|
|
}
|
|
|
|
if (newConversationButton) {
|
|
newConversationButton.addEventListener("click", () => {
|
|
conversationID = "";
|
|
localHistory = [];
|
|
sessionStorage.removeItem("domovoy.chat.conversation");
|
|
messages.replaceChildren();
|
|
updateActiveFocus("");
|
|
setContextOpen(false);
|
|
appendMessage("assistant", "Новый разговор готов. Чем займёмся?");
|
|
messageInput.focus();
|
|
});
|
|
}
|
|
|
|
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 ChatApiError(response.status, await response.text());
|
|
return response.json();
|
|
}
|
|
|
|
async function uploadMedia(path, file) {
|
|
const body = new FormData();
|
|
body.append("file", file, file.name);
|
|
const headers = {};
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
const response = await fetch(path, {method: "POST", headers, body});
|
|
if (!response.ok) throw new ChatApiError(response.status, await response.text());
|
|
return response.json();
|
|
}
|
|
|
|
function expireSession(message = "") {
|
|
token = "";
|
|
conversationID = "";
|
|
sessionStorage.removeItem("domovoy.chat.token");
|
|
sessionStorage.removeItem("domovoy.chat.conversation");
|
|
messages.replaceChildren();
|
|
updateActiveFocus("");
|
|
if (message) loginError.textContent = message;
|
|
showLogin();
|
|
}
|
|
|
|
function humanMeta(result) {
|
|
if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам.";
|
|
if (result.delegation_id && result.delegation_state) return `Передал в coding-agent: ${humanDelegationState(result.delegation_state)}.`;
|
|
if (result.capability_status && result.capability_status !== "AVAILABLE") return "Возможность пока не подключена.";
|
|
if (result.work_planned) return "Я разложил это как работу.";
|
|
return "";
|
|
}
|
|
|
|
function humanDelegationState(state) {
|
|
switch (state) {
|
|
case "RUNNING":
|
|
return "работа запущена";
|
|
case "WAITING":
|
|
return "ожидаю следующий шаг";
|
|
case "BLOCKED":
|
|
return "нужно решение человека";
|
|
case "VERIFYING":
|
|
return "результат проверяется";
|
|
case "COMPLETED":
|
|
return "работа завершена";
|
|
case "FAILED":
|
|
return "run завершился ошибкой";
|
|
default:
|
|
return state.toLowerCase();
|
|
}
|
|
}
|
|
|
|
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);
|
|
activeRequestController = new AbortController();
|
|
setChatBusy(true);
|
|
messages.setAttribute("aria-busy", "true");
|
|
try {
|
|
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean}), signal: activeRequestController.signal});
|
|
conversationID = result.conversation_id;
|
|
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
|
|
updateActiveFocus(result.active_focus, result.project_provenance);
|
|
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result), result.generated_image_base64 ? {contentType: result.generated_image_content_type || "image/png", data: result.generated_image_base64} : null);
|
|
if (/что сейчас требует моего внимания/i.test(clean)) {
|
|
await loadToday();
|
|
activateTab("today-panel");
|
|
}
|
|
} catch (error) {
|
|
if (error.name === "AbortError") {
|
|
appendMessage("assistant", "Обработка остановлена.");
|
|
} else if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
|
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
|
|
} else if (error instanceof ChatApiError && error.status >= 500) {
|
|
appendMessage("assistant", "Домовой не смог завершить обработку: основной runtime сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд.");
|
|
} else {
|
|
appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку.");
|
|
}
|
|
} finally {
|
|
activeRequestController = null;
|
|
setChatBusy(false);
|
|
messages.removeAttribute("aria-busy");
|
|
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();
|
|
if (isChatBusy()) return;
|
|
const text = messageInput.value.trim();
|
|
if (!text) return;
|
|
messageInput.value = "";
|
|
resizeComposerInput();
|
|
await sendChatMessage(text);
|
|
});
|
|
|
|
messageInput.addEventListener("input", resizeComposerInput);
|
|
messageInput.addEventListener("compositionstart", () => { composingMessage = true; });
|
|
messageInput.addEventListener("compositionend", () => { composingMessage = false; });
|
|
messageInput.addEventListener("keydown", (event) => {
|
|
if (event.key !== "Enter" || event.shiftKey || event.isComposing || composingMessage) return;
|
|
event.preventDefault();
|
|
if (!isChatBusy()) chatForm.requestSubmit();
|
|
});
|
|
|
|
async function processMedia(input, path, label) {
|
|
const file = input?.files?.[0];
|
|
if (!file || !mediaStatus) return;
|
|
mediaStatus.textContent = `${label} обрабатывается...`;
|
|
input.value = "";
|
|
try {
|
|
const result = await uploadMedia(path, file);
|
|
const text = String(result.text || "").trim();
|
|
if (!text) throw new Error("empty media result");
|
|
mediaStatus.textContent = "Готово";
|
|
await sendChatMessage(`Пользователь передал ${label.toLowerCase()}. Обработай содержание и ответь по смыслу:\n${text}`);
|
|
} catch (error) {
|
|
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
|
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
|
|
} else {
|
|
mediaStatus.textContent = `${label} сейчас недоступно.`;
|
|
}
|
|
}
|
|
}
|
|
|
|
const mediaStatus = document.querySelector("#media-status");
|
|
const audioInput = document.querySelector("#audio-input");
|
|
const imageInput = document.querySelector("#image-input");
|
|
const ocrInput = document.querySelector("#ocr-input");
|
|
if (audioInput) audioInput.addEventListener("change", () => processMedia(audioInput, "/api/multimodal/transcribe", "Распознанное аудио"));
|
|
if (imageInput) imageInput.addEventListener("change", () => processMedia(imageInput, "/api/multimodal/understand", "Описание изображения"));
|
|
if (ocrInput) ocrInput.addEventListener("change", () => processMedia(ocrInput, "/api/multimodal/ocr", "Текст с изображения"));
|
|
|
|
function toggleAttachmentMenu(open) {
|
|
if (!attachmentMenu) return;
|
|
const next = typeof open === "boolean" ? open : attachmentMenu.hidden;
|
|
attachmentMenu.hidden = !next;
|
|
if (attachToggle) attachToggle.setAttribute("aria-expanded", String(next));
|
|
}
|
|
|
|
if (attachToggle) attachToggle.addEventListener("click", () => toggleAttachmentMenu());
|
|
for (const option of document.querySelectorAll("[data-input]")) {
|
|
option.addEventListener("click", () => {
|
|
const input = document.querySelector(`#${option.dataset.input}`);
|
|
toggleAttachmentMenu(false);
|
|
if (input) input.click();
|
|
});
|
|
}
|
|
if (photoTrigger && imageInput) photoTrigger.addEventListener("click", () => imageInput.click());
|
|
if (micTrigger && audioInput) micTrigger.addEventListener("click", () => audioInput.click());
|
|
document.addEventListener("click", (event) => {
|
|
if (attachmentMenu && !attachmentMenu.hidden && !attachmentMenu.contains(event.target) && !attachToggle?.contains(event.target)) toggleAttachmentMenu(false);
|
|
});
|
|
if (sendButton) {
|
|
sendButton.addEventListener("click", (event) => {
|
|
if (!isChatBusy()) return;
|
|
event.preventDefault();
|
|
activeRequestController?.abort();
|
|
});
|
|
}
|
|
|
|
for (const tab of navTabs) {
|
|
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
|
|
}
|
|
|
|
if (refreshToday) refreshToday.addEventListener("click", loadToday);
|
|
if (cognitiveStatus) cognitiveStatus.addEventListener("click", loadCognitiveStatus);
|
|
if (aiDataForm) {
|
|
aiDataForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const details = aiDataInput.value.trim();
|
|
if (!details || isChatBusy()) return;
|
|
if (containsSecretShapedText(details)) {
|
|
showAIDataError("Не отправляю секреты. Удалите пароль, токен, API-ключ или приватный ключ и оставьте только описание модели.");
|
|
return;
|
|
}
|
|
showAIDataError();
|
|
aiDataInput.value = "";
|
|
await sendChatMessage(`Передаю сведения о локальном ИИ для текущего разговора:\n${details}`);
|
|
});
|
|
}
|
|
if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError());
|
|
|
|
logoutButton.addEventListener("click", () => passwordDialog?.showModal());
|
|
passwordClose?.addEventListener("click", () => passwordDialog?.close());
|
|
passwordLogout?.addEventListener("click", () => { passwordDialog?.close(); expireSession(); });
|
|
passwordForm?.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
passwordStatus.textContent = "Сохраняю…";
|
|
const submit = document.querySelector("#password-submit");
|
|
submit.disabled = true;
|
|
try {
|
|
await api("/api/account/password", {method: "POST", body: JSON.stringify({current_password: document.querySelector("#current-password").value, new_password: document.querySelector("#new-password").value})});
|
|
passwordForm.reset();
|
|
passwordStatus.textContent = "Пароль изменён без перезапуска.";
|
|
} catch (error) {
|
|
passwordStatus.textContent = error?.status === 401 ? "Текущий пароль не принят." : "Не удалось изменить пароль.";
|
|
} finally {
|
|
submit.disabled = false;
|
|
}
|
|
});
|
|
|
|
if (token) {
|
|
showChat();
|
|
} else {
|
|
showLogin();
|
|
}
|
|
resizeComposerInput();
|