// --- Элементы UI --- const chatList = document.getElementById('chat-list'); const newChatBtn = document.getElementById('new-chat-btn'); const chatWindow = document.querySelector('.chat-window'); const userInput = document.getElementById('global-input'); const sendBtn = document.getElementById('global-send-btn'); const pathInput = document.getElementById('global-model-input'); const suggestionsBox = document.getElementById('suggestions'); const loadBtn = document.getElementById('global-load-btn'); const unloadBtn = document.getElementById('global-unload-btn'); const statusText = document.getElementById('status-indicator'); const toggleBtn = document.getElementById('toggle-sidebar-btn'); const sidebar = document.getElementById('sidebar'); // --- Логика Сворачивания Сайдбара --- toggleBtn.addEventListener('click', () => { sidebar.classList.toggle('collapsed'); toggleBtn.textContent = sidebar.classList.contains('collapsed') ? '▶' : '◀'; }); // --- Состояние приложения --- let modelsList = []; let activeChatId = 'chat_1'; let currentHistory = []; // Текущая история сообщений // Инициализация document.addEventListener('DOMContentLoaded', () => { fetchModels(); setupGlobalControls(); // Загружаем первый чат loadHistoryFromServer(activeChatId); }); // --- Глобальные контролы (Модель) --- function setupGlobalControls() { // Автодополнение pathInput.addEventListener('input', () => { const inputVal = pathInput.value.toLowerCase(); suggestionsBox.innerHTML = ''; const filtered = modelsList.filter(model => model.toLowerCase().includes(inputVal)); if (filtered.length > 0) { filtered.forEach(modelName => { const div = document.createElement('div'); div.className = 'suggestion-item'; div.textContent = modelName; div.onclick = () => { let basePath = pathInput.value; let folderPath = basePath.substring(0, basePath.lastIndexOf('/') + 1); if (!folderPath.endsWith('/')) folderPath = './model/'; pathInput.value = folderPath + modelName; suggestionsBox.style.display = 'none'; }; suggestionsBox.appendChild(div); }); suggestionsBox.style.display = 'block'; } else { suggestionsBox.style.display = 'none'; } }); pathInput.addEventListener('focus', () => { if (pathInput.value === '' && modelsList.length > 0) { suggestionsBox.innerHTML = ''; modelsList.forEach(modelName => { const div = document.createElement('div'); div.className = 'suggestion-item'; div.textContent = modelName; div.onclick = () => { pathInput.value = `./model/${modelName}`; suggestionsBox.style.display = 'none'; }; suggestionsBox.appendChild(div); }); suggestionsBox.style.display = 'block'; } }); // Загрузка модели loadBtn.addEventListener('click', async () => { const path = pathInput.value.trim(); if (!path) return alert("Введите путь к модели"); loadBtn.disabled = true; loadBtn.textContent = "Загрузка..."; statusText.textContent = "● Загрузка модели..."; try { const res = await fetch('/api/model/load', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }) }); const data = await res.json(); if (data.success) { setUIState(true); statusText.textContent = "● Модель загружена"; statusText.style.color = "#4ec9b0"; } else { alert("Ошибка: " + data.message); loadBtn.disabled = false; loadBtn.textContent = "Загрузить"; } } catch (err) { alert("Ошибка сети: " + err.message); loadBtn.disabled = false; loadBtn.textContent = "Загрузить"; } }); // Выгрузка модели unloadBtn.addEventListener('click', async () => { if (!confirm("Выгрузить модель?")) return; try { const res = await fetch('/api/model/unload', { method: 'POST' }); const data = await res.json(); if (data.success) { setUIState(false); statusText.textContent = "● Модель выгружена"; statusText.style.color = "#cca511"; } } catch (err) { alert("Ошибка сети"); } }); // Отправка сообщения sendBtn.addEventListener('click', handleSend); userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }); } function setUIState(loaded) { pathInput.disabled = loaded; loadBtn.disabled = loaded; unloadBtn.disabled = !loaded; userInput.disabled = !loaded; sendBtn.disabled = !loaded; if (loaded) userInput.focus(); } // --- Логика чата и истории --- async function handleSend() { const text = userInput.value.trim(); if (!text) return; userInput.disabled = true; sendBtn.disabled = true; userInput.value = ''; // 1. Добавляем сообщение пользователя в историю и UI currentHistory.push({ role: 'user', content: text }); appendMessage(text, 'user'); // 2. Показываем лоадер const loaderHtml = 'Думаю...'; const loaderDiv = appendMessage(loaderHtml, 'bot', true); try { // 3. Отправляем запрос ВМЕСТЕ С ИСТОРИЕЙ const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, history: currentHistory.slice(0, -1) // Отправляем историю БЕЗ последнего сообщения юзера }) }); if (!response.ok) { const errText = await response.text(); throw new Error(errText); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let botResponseText = ""; let isFirstChunk = true; loaderDiv.classList.remove('loader'); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); botResponseText += chunk; if (isFirstChunk) { loaderDiv.textContent = botResponseText; isFirstChunk = false; } else { loaderDiv.textContent = botResponseText; } chatWindow.scrollTop = chatWindow.scrollHeight; } // 4. Добавляем ответ бота в историю и сохраняем на сервере currentHistory.push({ role: 'bot', content: botResponseText }); await saveHistoryToServer(activeChatId, currentHistory); playSound(); } catch (error) { loaderDiv.classList.remove('loader'); loaderDiv.textContent = "Ошибка: " + error.message; loaderDiv.style.color = "#ff6b6b"; } finally { userInput.disabled = false; sendBtn.disabled = false; userInput.focus(); } } // --- Функции работы с Историей --- async function loadHistoryFromServer(chatId) { try { const res = await fetch(`/api/chat/load?id=${chatId}`); const history = await res.json(); currentHistory = history; // Перерисовываем чат chatWindow.innerHTML = ''; history.forEach(msg => { appendMessage(msg.content, msg.role); }); if (history.length === 0) { chatWindow.innerHTML = '
Новая сессия. Загрузите модель.
'; } activeChatId = chatId; // Обновляем UI сайдбара document.querySelectorAll('.chat-item').forEach(el => el.classList.remove('active')); const activeItem = document.querySelector(`.chat-item[data-id="${chatId}"]`); if (activeItem) activeItem.classList.add('active'); } catch (err) { console.error("Не удалось загрузить историю:", err); } } async function saveHistoryToServer(chatId, history) { try { await fetch('/api/chat/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: chatId, history: history }) }); console.log("💾 История сохранена"); } catch (err) { console.error("Ошибка сохранения:", err); } } // --- Логика Сайдбара --- // Создание нового чата newChatBtn.addEventListener('click', async () => { // 1. ПРОВЕРКА: Если текущий чат пустой, не создаем новый if (currentHistory.length === 0) { alert("Сначала введите сообщение в текущем чате. Нельзя создавать несколько пустых чатов."); return; } // Генерируем ID const newId = `chat_${Date.now()}`; const item = document.createElement('div'); item.className = 'chat-item'; item.dataset.id = newId; // Создаем элементы чата с возможностью редактирования item.innerHTML = ` 💬 Новый чат `; // 2. НАВЕШИВАЕМ СОБЫТИЕ РЕДАКТИРОВАНИЯ const titleSpan = item.querySelector('.chat-title'); // Двойной клик - редактировать titleSpan.addEventListener('dblclick', () => { makeTitleEditable(item, titleSpan); }); // Клик по всему item - переключаться (если не редактируем) item.addEventListener('click', (e) => { // Если кликнули во время редактирования - ничего не делаем if (titleSpan.isContentEditable) return; loadHistoryFromServer(newId); }); chatList.appendChild(item); // Создаем пустой файл и переключаемся await saveHistoryToServer(newId, []); loadHistoryFromServer(newId); }); // --- Утилиты --- function appendMessage(text, sender, isLoader = false) { const msgDiv = document.createElement('div'); msgDiv.classList.add('message', sender); if (isLoader) { msgDiv.classList.add('loader'); msgDiv.innerHTML = text; } else { msgDiv.textContent = text; } chatWindow.appendChild(msgDiv); chatWindow.scrollTop = chatWindow.scrollHeight; return msgDiv; } function playSound() { const msgSound = document.getElementById('msg-sound'); if (msgSound) { msgSound.currentTime = 0; msgSound.play().catch(() => {}); } } async function fetchModels() { try { const res = await fetch('/api/models/list'); modelsList = await res.json(); console.log("📂 Модели:", modelsList); } catch (err) { console.error("Не удалось загрузить список моделей"); } } document.getElementById('stop-server-btn').addEventListener('click', async () => { if (confirm("Внимание! Это остановит сервер и закроет приложение.")) { await fetch('/api/server/stop', { method: 'POST' }); document.body.innerHTML = "
Приложение закрыто.
"; } }); // --- ФУНКЦИЯ РЕДАКТИРОВАНИЯ НАЗВАНИЯ --- function makeTitleEditable(itemElement, titleSpan) { const currentName = titleSpan.textContent; // Включаем редактирование titleSpan.contentEditable = "true"; titleSpan.classList.add('editing'); titleSpan.focus(); // Выделяем весь текст document.execCommand('selectAll', false, null); // Обработка нажатия клавиш const handleKeyDown = (e) => { if (e.key === 'Enter') { e.preventDefault(); saveNewTitle(); } else if (e.key === 'Escape') { cancelEdit(); } }; // Обработка потери фокуса (клик мимо - сохранить) const handleBlur = () => { // Небольшая задержка, чтобы успеть сработать клику по элементу setTimeout(() => { if (titleSpan.isContentEditable) saveNewTitle(); }, 200); }; function saveNewTitle() { const newName = titleSpan.textContent.trim(); // Если имя не изменилось - просто выключаем редактирование if (newName === currentName) { disableEdit(); return; } if (newName === '') { alert("Имя не может быть пустым"); titleSpan.textContent = currentName; // Восстанавливаем старое disableEdit(); return; } // Отправляем запрос на сервер для переименования файла renameChatFile(itemElement.dataset.id, newName).then((success) => { if (success) { // Если успех - обновляем ID элемента и загружаем историю под новым именем itemElement.dataset.id = newName; if (activeChatId === itemElement.dataset.id) { activeChatId = newName; // Если это активный чат, обновляем глобальный ID } console.log("✅ Чат переименован"); } else { alert("Не удалось переименовать чат"); titleSpan.textContent = currentName; } disableEdit(); }); } function disableEdit() { titleSpan.contentEditable = "false"; titleSpan.classList.remove('editing'); titleSpan.removeEventListener('keydown', handleKeyDown); titleSpan.removeEventListener('blur', handleBlur); } function cancelEdit() { titleSpan.textContent = currentName; disableEdit(); } // Добавляем временные слушатели titleSpan.addEventListener('keydown', handleKeyDown); titleSpan.addEventListener('blur', handleBlur); } // --- API ЗАПРОС РЕНЕЙМИНГА --- async function renameChatFile(oldId, newName) { try { const res = await fetch('/api/chat/rename', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ oldId, newName }) }); const data = await res.json(); if (data.success) return true; return false; } catch (err) { console.error("Ошибка переименования:", err); return false; } }