303 lines
12 KiB
JavaScript
303 lines
12 KiB
JavaScript
// Вспомогательная функция для автоматического изменения высоты текстового поля
|
|
function updateInputHeight(el) {
|
|
if (!el) return;
|
|
el.style.height = "24px";
|
|
const newHeight = Math.min(el.scrollHeight, 120);
|
|
el.style.height = newHeight + "px";
|
|
}
|
|
|
|
let selectedFile = null;
|
|
|
|
function handleFileSelect(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
if (file.size > 15 * 1024 * 1024) {
|
|
alert("Файл слишком большой. Максимальный размер: 15 МБ");
|
|
e.target.value = "";
|
|
return;
|
|
}
|
|
|
|
selectedFile = file;
|
|
const fileNameEl = document.getElementById("file-name-display");
|
|
const fileSizeEl = document.getElementById("file-size-display");
|
|
const previewContainer = document.getElementById("file-preview-container");
|
|
|
|
if (fileNameEl) fileNameEl.innerText = file.name;
|
|
if (fileSizeEl) fileSizeEl.innerText = `(${(file.size / 1024).toFixed(1)} KB)`;
|
|
if (previewContainer) previewContainer.classList.remove("hidden");
|
|
}
|
|
|
|
function clearAttachedFile() {
|
|
selectedFile = null;
|
|
const fileInput = document.getElementById("file-input");
|
|
const previewContainer = document.getElementById("file-preview-container");
|
|
if (fileInput) fileInput.value = "";
|
|
if (previewContainer) previewContainer.classList.add("hidden");
|
|
}
|
|
|
|
// Деактивация всех старых кнопок в истории
|
|
function disableAllActionButtons() {
|
|
const allBtnContainers = document.querySelectorAll(".action-buttons-container");
|
|
allBtnContainers.forEach(container => {
|
|
container.querySelectorAll("button").forEach(btn => {
|
|
btn.disabled = true;
|
|
btn.classList.add("opacity-40", "cursor-not-allowed");
|
|
});
|
|
});
|
|
}
|
|
|
|
// Быстрая отправка текста кнопки
|
|
function handleActionButtonClick(text) {
|
|
disableAllActionButtons();
|
|
const input = document.getElementById("user-input");
|
|
if (input) {
|
|
input.value = text;
|
|
sendMessage();
|
|
}
|
|
}
|
|
|
|
async function sendMessage(e) {
|
|
if (e && e.preventDefault) e.preventDefault();
|
|
|
|
const input = document.getElementById("user-input");
|
|
const chatWindow = document.getElementById("chat-window");
|
|
const sendBtn = document.getElementById("send-btn");
|
|
|
|
if (!input || !chatWindow) return;
|
|
const text = input.value.trim();
|
|
|
|
if (!text && !selectedFile) return;
|
|
|
|
// Деактивируем предыдущие интерактивные кнопки
|
|
disableAllActionButtons();
|
|
|
|
let userDisplayHtml = escapeHtml(text);
|
|
if (selectedFile) {
|
|
userDisplayHtml = `<div class="font-bold border-b border-indigo-400/40 pb-1 mb-1 text-[11px] flex items-center gap-1.5">
|
|
<i class="fa-solid fa-file"></i> ${escapeHtml(selectedFile.name)}
|
|
</div>` + userDisplayHtml;
|
|
}
|
|
|
|
const userMsgHtml = `
|
|
<div class="flex justify-end mb-3">
|
|
<div class="bg-indigo-600 text-white rounded-2xl px-4 py-2.5 max-w-2xl text-xs sm:text-sm shadow-sm">
|
|
${userDisplayHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
chatWindow.insertAdjacentHTML("beforeend", userMsgHtml);
|
|
|
|
input.value = "";
|
|
updateInputHeight(input);
|
|
chatWindow.scrollTop = chatWindow.scrollHeight;
|
|
|
|
if (sendBtn) {
|
|
sendBtn.disabled = true;
|
|
sendBtn.classList.add("opacity-50");
|
|
}
|
|
|
|
const token = typeof API_TOKEN !== 'undefined' && API_TOKEN ? API_TOKEN : localStorage.getItem("scud_api_auth_token");
|
|
const isGuest = typeof IS_GUEST !== 'undefined' ? IS_GUEST : (localStorage.getItem("scud_is_guest") === "true");
|
|
|
|
const endpoint = isGuest ? "/api/v1/chat/guest" : "/api/v1/chat";
|
|
|
|
const formData = new FormData();
|
|
formData.append("session_id", "web_session_main");
|
|
formData.append("message", text || "Проанализируй прикрепленный файл");
|
|
|
|
if (selectedFile instanceof File) {
|
|
formData.append("file", selectedFile, selectedFile.name);
|
|
}
|
|
|
|
const headers = {};
|
|
if (!isGuest && token) {
|
|
headers["Authorization"] = "Bearer " + token;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(endpoint, {
|
|
method: "POST",
|
|
headers: headers,
|
|
body: formData
|
|
});
|
|
|
|
if (res.status === 401 && !isGuest) {
|
|
if (typeof logout === 'function') logout();
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
const assistantTitle = isGuest ? "Локальная нейросеть (Гость)" : "ИИ-Ассистент SCUD Orion AI";
|
|
const replyText = data.reply || "Пустой ответ от нейросети";
|
|
|
|
// Генерация блока кнопок подтверждения при необходимости
|
|
let actionButtonsHtml = "";
|
|
if (data.action_type === "PROMPT_PREVIEW") {
|
|
actionButtonsHtml = `
|
|
<div class="action-buttons-container flex items-center gap-2 mt-3 pt-2.5 border-t border-slate-100">
|
|
<button type="button" onclick="handleActionButtonClick('подтверждаю')"
|
|
class="bg-emerald-600 hover:bg-emerald-700 active:bg-emerald-800 text-white font-semibold px-3.5 py-1.5 rounded-xl text-xs flex items-center gap-1.5 shadow-sm transition">
|
|
<i class="fa-solid fa-check"></i>
|
|
<span>Подтвердить</span>
|
|
</button>
|
|
<button type="button" onclick="handleActionButtonClick('отмена')"
|
|
class="bg-slate-100 hover:bg-slate-200 active:bg-slate-300 text-slate-700 font-semibold px-3.5 py-1.5 rounded-xl text-xs flex items-center gap-1.5 border border-slate-300 transition">
|
|
<i class="fa-solid fa-xmark"></i>
|
|
<span>Отменить</span>
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const botMsgHtml = `
|
|
<div class="bg-white border border-slate-200 rounded-2xl p-3.5 shadow-sm max-w-2xl mb-3">
|
|
<p class="text-[11px] font-bold text-indigo-600 uppercase tracking-wider mb-1">
|
|
<i class="fa-solid fa-robot mr-1"></i> ${assistantTitle}
|
|
</p>
|
|
<p class="text-slate-800 text-xs sm:text-sm whitespace-pre-wrap leading-relaxed">${escapeHtml(replyText)}</p>
|
|
${actionButtonsHtml}
|
|
</div>
|
|
`;
|
|
chatWindow.insertAdjacentHTML("beforeend", botMsgHtml);
|
|
chatWindow.scrollTop = chatWindow.scrollHeight;
|
|
|
|
clearAttachedFile();
|
|
|
|
if (!isGuest && typeof loadTasks === 'function') {
|
|
loadTasks();
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error("[Chat Error]", err);
|
|
const errorHtml = `
|
|
<div class="bg-red-50 border border-red-200 rounded-2xl p-3.5 max-w-2xl text-red-700 text-xs sm:text-sm mb-3">
|
|
Ошибка связи с сервером.
|
|
</div>
|
|
`;
|
|
chatWindow.insertAdjacentHTML("beforeend", errorHtml);
|
|
chatWindow.scrollTop = chatWindow.scrollHeight;
|
|
} finally {
|
|
if (sendBtn) {
|
|
sendBtn.disabled = false;
|
|
sendBtn.classList.remove("opacity-50");
|
|
}
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return "";
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const input = document.getElementById("user-input");
|
|
const dropZone = document.getElementById("chat-window")?.parentElement;
|
|
const dropOverlay = document.getElementById("drop-overlay");
|
|
|
|
if (input) {
|
|
let historyIndex = -1;
|
|
let localHistory = JSON.parse(localStorage.getItem("scud_chat_input_history") || "[]");
|
|
|
|
input.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
const text = input.value.trim();
|
|
if (text) {
|
|
if (localHistory.length === 0 || localHistory[0] !== text) {
|
|
localHistory.unshift(text);
|
|
if (localHistory.length > 50) localHistory.pop();
|
|
localStorage.setItem("scud_chat_input_history", JSON.stringify(localHistory));
|
|
}
|
|
historyIndex = -1;
|
|
}
|
|
sendMessage(e);
|
|
updateInputHeight(input);
|
|
return;
|
|
}
|
|
|
|
if (e.key === "ArrowUp") {
|
|
const textBeforeCursor = input.value.substring(0, input.selectionStart);
|
|
const isFirstLine = !textBeforeCursor.includes("\n");
|
|
|
|
if (isFirstLine && input.selectionStart === 0 && localHistory.length > 0) {
|
|
if (historyIndex < localHistory.length - 1) {
|
|
e.preventDefault();
|
|
if (historyIndex === -1) {
|
|
input.dataset.draft = input.value;
|
|
}
|
|
historyIndex++;
|
|
input.value = localHistory[historyIndex];
|
|
updateInputHeight(input);
|
|
input.setSelectionRange(input.value.length, input.value.length);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (e.key === "ArrowDown") {
|
|
const textAfterCursor = input.value.substring(input.selectionEnd);
|
|
const isLastLine = !textAfterCursor.includes("\n");
|
|
|
|
if (isLastLine && input.selectionEnd === input.value.length && historyIndex >= 0) {
|
|
e.preventDefault();
|
|
if (historyIndex > 0) {
|
|
historyIndex--;
|
|
input.value = localHistory[historyIndex];
|
|
} else {
|
|
historyIndex = -1;
|
|
input.value = input.dataset.draft || "";
|
|
}
|
|
updateInputHeight(input);
|
|
input.setSelectionRange(input.value.length, input.value.length);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (dropZone && dropOverlay) {
|
|
["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}, false);
|
|
});
|
|
|
|
["dragenter", "dragover"].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, () => {
|
|
dropOverlay.classList.remove("hidden");
|
|
dropOverlay.classList.add("flex");
|
|
}, false);
|
|
});
|
|
|
|
["dragleave", "drop"].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, (e) => {
|
|
if (eventName === "drop" || e.target === dropZone || !dropZone.contains(e.relatedTarget)) {
|
|
dropOverlay.classList.add("hidden");
|
|
dropOverlay.classList.remove("flex");
|
|
}
|
|
}, false);
|
|
});
|
|
|
|
dropZone.addEventListener("drop", (e) => {
|
|
const dt = e.dataTransfer;
|
|
const files = dt.files;
|
|
|
|
if (files && files.length > 0) {
|
|
const file = files[0];
|
|
handleFileSelect({ target: { files: [file] } });
|
|
|
|
const fileInput = document.getElementById("file-input");
|
|
if (fileInput) {
|
|
const dataTransfer = new DataTransfer();
|
|
dataTransfer.items.add(file);
|
|
fileInput.files = dataTransfer.files;
|
|
}
|
|
}
|
|
}, false);
|
|
}
|
|
}); |