160 lines
3.8 KiB
JavaScript
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(); |