284 lines
9.6 KiB
JavaScript
284 lines
9.6 KiB
JavaScript
const chatWindow = document.getElementById('chat-window');
|
||
const userInput = document.getElementById('user-input');
|
||
const sendBtn = document.getElementById('send-btn');
|
||
const pathInput = document.getElementById('model-path-input');
|
||
const loadBtn = document.getElementById('load-model-btn');
|
||
const unloadBtn = document.getElementById('unload-model-btn');
|
||
const stopBtn = document.getElementById('stop-server-btn');
|
||
const statusIndicator = document.getElementById('status-indicator');
|
||
// --- Логика Автодополнения ---
|
||
|
||
const suggestionsBox = document.getElementById('suggestions');
|
||
let availableModels = [];
|
||
|
||
// Загрузка списка моделей с сервера
|
||
async function fetchModels() {
|
||
try {
|
||
const res = await fetch('/api/models/list');
|
||
availableModels = await res.json();
|
||
console.log("📂 Найдены модели:", availableModels);
|
||
} catch (err) {
|
||
console.error("Не удалось получить список моделей:", err);
|
||
}
|
||
}
|
||
|
||
fetchModels();
|
||
|
||
// Обработка ввода и показ списка
|
||
pathInput.addEventListener('input', () => {
|
||
const inputVal = pathInput.value.toLowerCase();
|
||
suggestionsBox.innerHTML = '';
|
||
|
||
// Фильтруем: ищем по имени файла
|
||
const filtered = availableModels.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.addEventListener('click', () => {
|
||
// Собираем путь
|
||
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';
|
||
pathInput.focus();
|
||
});
|
||
|
||
suggestionsBox.appendChild(div);
|
||
});
|
||
suggestionsBox.style.display = 'block';
|
||
} else {
|
||
suggestionsBox.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// Скрывать список при клике вне его
|
||
document.addEventListener('click', (e) => {
|
||
if (!pathInput.contains(e.target) && !suggestionsBox.contains(e.target)) {
|
||
suggestionsBox.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// Показывать весь список при клике в инпут (если есть модели)
|
||
pathInput.addEventListener('focus', () => {
|
||
// Если инпут пуст, покажем все модели
|
||
if (pathInput.value === '' && availableModels.length > 0) {
|
||
suggestionsBox.innerHTML = '';
|
||
availableModels.forEach(modelName => {
|
||
const div = document.createElement('div');
|
||
div.className = 'suggestion-item';
|
||
div.textContent = modelName;
|
||
div.addEventListener('click', () => {
|
||
pathInput.value = `./model/${modelName}`;
|
||
suggestionsBox.style.display = 'none';
|
||
});
|
||
suggestionsBox.appendChild(div);
|
||
});
|
||
suggestionsBox.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
let isModelLoaded = false;
|
||
|
||
// --- Функции Управления Интерфейсом ---
|
||
|
||
function setUIState(loaded) {
|
||
isModelLoaded = loaded;
|
||
|
||
if (loaded) {
|
||
// Модель загружена
|
||
pathInput.disabled = true;
|
||
loadBtn.disabled = true;
|
||
unloadBtn.disabled = false;
|
||
|
||
userInput.disabled = false;
|
||
sendBtn.disabled = false;
|
||
|
||
statusIndicator.textContent = "● Сервер: Активен | Модель: Загружена";
|
||
statusIndicator.style.color = "#4ec9b0";
|
||
addSystemMessage("Модель успешно загружена и готова к работе.");
|
||
userInput.focus();
|
||
} else {
|
||
// Модель выгружена
|
||
pathInput.disabled = false;
|
||
loadBtn.disabled = false;
|
||
unloadBtn.disabled = true;
|
||
|
||
userInput.disabled = true;
|
||
sendBtn.disabled = true;
|
||
|
||
statusIndicator.textContent = "● Сервер: Активен | Модель: Не загружена";
|
||
statusIndicator.style.color = "#cca511"; // Желтый
|
||
addSystemMessage("Модель выгружена.");
|
||
}
|
||
}
|
||
|
||
function addSystemMessage(text) {
|
||
const div = document.createElement('div');
|
||
div.className = 'message system';
|
||
div.textContent = text;
|
||
chatWindow.appendChild(div);
|
||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||
}
|
||
|
||
// --- API Запросы Управления ---
|
||
|
||
// 1. Загрузка модели
|
||
loadBtn.addEventListener('click', async () => {
|
||
const path = pathInput.value.trim();
|
||
if (!path) {
|
||
alert("Введите путь к модели");
|
||
return;
|
||
}
|
||
|
||
loadBtn.disabled = true;
|
||
loadBtn.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);
|
||
} else {
|
||
alert("Ошибка загрузки: " + data.message);
|
||
loadBtn.disabled = false;
|
||
loadBtn.textContent = "Загрузить";
|
||
}
|
||
} catch (err) {
|
||
alert("Ошибка сети при загрузке: " + err.message);
|
||
loadBtn.disabled = false;
|
||
loadBtn.textContent = "Загрузить";
|
||
}
|
||
});
|
||
|
||
// 2. Выгрузка модели
|
||
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);
|
||
} else {
|
||
alert("Ошибка выгрузки: " + data.message);
|
||
}
|
||
} catch (err) {
|
||
alert("Ошибка сети: " + err.message);
|
||
}
|
||
});
|
||
|
||
// 3. Остановка сервера
|
||
stopBtn.addEventListener('click', async () => {
|
||
if (!confirm("Внимание! Это остановит веб-сервер. Придется перезапустить его через консоль (npm start).")) return;
|
||
|
||
try {
|
||
await fetch('/api/server/stop', { method: 'POST' });
|
||
document.body.innerHTML = "<div style='color:white; text-align:center; margin-top:50px;'>Сервер остановлен. Закройте эту вкладку.</div>";
|
||
} catch (err) {
|
||
// Ошибка может возникнуть, потому что сервер умирает
|
||
console.log("Сервер остановлен (разрыв соединения ожидается)");
|
||
}
|
||
});
|
||
|
||
// --- Логика Чата ---
|
||
|
||
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;
|
||
}
|
||
|
||
async function handleSend() {
|
||
const text = userInput.value.trim();
|
||
if (!text) return;
|
||
|
||
userInput.disabled = true;
|
||
sendBtn.disabled = true;
|
||
userInput.value = '';
|
||
|
||
appendMessage(text, 'user');
|
||
|
||
const loaderHtml = 'Думаю<span>.</span><span>.</span><span>.</span>';
|
||
const loaderDiv = appendMessage(loaderHtml, 'bot', true);
|
||
|
||
try {
|
||
const response = await fetch('/api/chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ message: text })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errText = await response.text();
|
||
throw new Error(errText);
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let fullResponse = "";
|
||
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 });
|
||
fullResponse += chunk;
|
||
|
||
if (isFirstChunk) {
|
||
loaderDiv.textContent = fullResponse;
|
||
isFirstChunk = false;
|
||
} else {
|
||
loaderDiv.textContent = fullResponse;
|
||
}
|
||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||
}
|
||
|
||
} catch (error) {
|
||
loaderDiv.classList.remove('loader');
|
||
loaderDiv.textContent = "Ошибка: " + error.message;
|
||
loaderDiv.style.color = "#ff6b6b";
|
||
} finally {
|
||
// Возвращаем доступ только если модель все еще загружена
|
||
if (isModelLoaded) {
|
||
userInput.disabled = false;
|
||
sendBtn.disabled = false;
|
||
}
|
||
userInput.focus();
|
||
}
|
||
}
|
||
|
||
sendBtn.addEventListener('click', handleSend);
|
||
userInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
handleSend();
|
||
}
|
||
}); |