Files

160 lines
3.8 KiB
JavaScript

const messages = document.getElementById("messages");
const input = document.getElementById("user-input");
const sendBtn = document.getElementById("send-btn");
let userName = "Alex";
// текущий SSE поток (ВАЖНО: чтобы не плодить соединения)
let eventSource = null;
// ===============================
// 1. ЗАГРУЗКА ИСТОРИИ
// ===============================
async function loadHistory() {
const res = await fetch("/init");
const data = await res.json();
userName = data.user_name || "User";
data.messages.forEach(msg => {
renderMessage(msg.role, msg.content);
});
}
// ===============================
// 2. ОТПРАВКА СООБЩЕНИЯ
// ===============================
async function sendMessage() {
const text = input.value.trim();
if (!text) return;
renderMessage("user", text);
input.value = "";
// закрываем старый stream если есть
if (eventSource) {
eventSource.close();
eventSource = null;
}
// отправляем сообщение на backend (он сохраняет state)
await fetch("/chat", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message: text
})
});
// создаём ПУСТОЕ сообщение для стрима
const aiMessageDiv = createAIMessagePlaceholder();
// запускаем stream
startStream(aiMessageDiv);
}
// ===============================
// 3. SSE STREAM
// ===============================
function startStream(aiMessageDiv) {
eventSource = new EventSource("/stream");
let fullText = "";
eventSource.onmessage = function(event) {
const token = event.data;
// иногда Ollama может слать [DONE]
if (token === "[DONE]") {
eventSource.close();
eventSource = null;
return;
}
fullText += token;
aiMessageDiv.innerHTML = `<b>AI:</b> ${formatText(fullText)}`;
messages.scrollTop = messages.scrollHeight;
};
eventSource.onerror = function(err) {
console.error("Stream error:", err);
eventSource.close();
eventSource = null;
};
}
// ===============================
// 4. РЕНДЕР СООБЩЕНИЙ
// ===============================
function renderMessage(role, text) {
const div = document.createElement("div");
if (role === "user") {
div.className = "user-msg";
div.innerHTML = `<b>${userName}:</b> ${text}`;
}
if (role === "assistant") {
div.className = "ai-msg";
div.innerHTML = `<b>AI:</b> ${formatText(text)}`;
}
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
// ===============================
// 5. ПУСТОЙ КОНТЕЙНЕР ДЛЯ STREAM
// ===============================
function createAIMessagePlaceholder() {
const div = document.createElement("div");
div.className = "ai-msg";
div.innerHTML = `<b>AI:</b> `;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
return div;
}
// ===============================
// 6. ФОРМАТИРОВАНИЕ (*actions*)
// ===============================
function formatText(text) {
// действия *...*
return text.replace(/\*(.*?)\*/g, '<span class="action">*$1*</span>');
}
// ===============================
// 7. EVENTS
// ===============================
sendBtn.addEventListener("click", sendMessage);
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
sendMessage();
}
});
// ===============================
// 8. INIT
// ===============================
loadHistory();