266 lines
10 KiB
JavaScript
266 lines
10 KiB
JavaScript
// Вспомогательная функция для автоматического изменения высоты текстового поля (1-3 строки)
|
|
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");
|
|
}
|
|
|
|
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;
|
|
|
|
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 || "Пустой ответ от нейросети";
|
|
|
|
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>
|
|
</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");
|
|
|
|
// --- 1. УМНАЯ НАВИГАЦИЯ СТРЕЛКАМИ В МНОГОСТРОЧНОМ ТЕКСТЕ ---
|
|
if (input) {
|
|
let historyIndex = -1;
|
|
let localHistory = JSON.parse(localStorage.getItem("scud_chat_input_history") || "[]");
|
|
|
|
input.addEventListener("keydown", (e) => {
|
|
// Отправка по Enter без Shift
|
|
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");
|
|
|
|
// Переключаем историю ТОЛЬКО когда курсор на 1-й строке И уперся в самое начало (позиция 0)
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- 2. ОБРАБОТКА DRAG-AND-DROP ФАЙЛОВ ---
|
|
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);
|
|
}
|
|
}); |