Files
airllm-fork-nodejs/public/script.js
2026-02-05 15:27:49 +08:00

284 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
});