erowid-bot/app/static/app.js

158 lines
4.4 KiB
JavaScript

const chatContainer = document.getElementById("chat-container");
const messageInput = document.getElementById("message-input");
const sendBtn = document.getElementById("send-btn");
const welcomeEl = document.getElementById("welcome");
const statsEl = document.getElementById("stats");
let sessionId = localStorage.getItem("erowid_session") || "";
let isStreaming = false;
// Load stats
async function loadStats() {
try {
const resp = await fetch("/stats");
const data = await resp.json();
statsEl.textContent = `${data.experiences} reports | ${data.substances} substances | ${data.chunks} chunks`;
} catch {
statsEl.textContent = "connecting...";
}
}
loadStats();
// Auto-resize textarea
messageInput.addEventListener("input", () => {
messageInput.style.height = "auto";
messageInput.style.height = Math.min(messageInput.scrollHeight, 120) + "px";
});
// Send on Enter (Shift+Enter for newline)
messageInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
sendBtn.addEventListener("click", sendMessage);
// Suggestion clicks
document.querySelectorAll(".suggestion").forEach((el) => {
el.addEventListener("click", () => {
messageInput.value = el.textContent;
sendMessage();
});
});
function addMessage(role, content) {
if (welcomeEl) welcomeEl.style.display = "none";
const msg = document.createElement("div");
msg.className = `message ${role}`;
const avatar = document.createElement("div");
avatar.className = "message-avatar";
avatar.textContent = role === "user" ? "You" : "E";
const contentEl = document.createElement("div");
contentEl.className = "message-content";
contentEl.textContent = content;
msg.appendChild(avatar);
msg.appendChild(contentEl);
chatContainer.appendChild(msg);
chatContainer.scrollTop = chatContainer.scrollHeight;
return contentEl;
}
function addTypingIndicator() {
const msg = document.createElement("div");
msg.className = "message assistant";
msg.id = "typing-indicator";
const avatar = document.createElement("div");
avatar.className = "message-avatar";
avatar.textContent = "E";
const contentEl = document.createElement("div");
contentEl.className = "message-content";
contentEl.innerHTML = '<div class="typing"><span></span><span></span><span></span></div>';
msg.appendChild(avatar);
msg.appendChild(contentEl);
chatContainer.appendChild(msg);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function removeTypingIndicator() {
const el = document.getElementById("typing-indicator");
if (el) el.remove();
}
async function sendMessage() {
const text = messageInput.value.trim();
if (!text || isStreaming) return;
isStreaming = true;
sendBtn.disabled = true;
messageInput.value = "";
messageInput.style.height = "auto";
addMessage("user", text);
addTypingIndicator();
try {
const resp = await fetch("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text, session_id: sessionId }),
});
removeTypingIndicator();
const contentEl = addMessage("assistant", "");
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let fullResponse = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const jsonStr = line.slice(6).trim();
if (!jsonStr) continue;
try {
const data = JSON.parse(jsonStr);
if (data.token) {
fullResponse += data.token;
contentEl.textContent = fullResponse;
chatContainer.scrollTop = chatContainer.scrollHeight;
}
if (data.session_id) {
sessionId = data.session_id;
localStorage.setItem("erowid_session", sessionId);
}
if (data.error) {
contentEl.textContent = `Error: ${data.error}`;
}
} catch {}
}
}
} catch (err) {
removeTypingIndicator();
addMessage("assistant", `Connection error: ${err.message}`);
}
isStreaming = false;
sendBtn.disabled = false;
messageInput.focus();
}