const form = document.querySelector("#chatForm"); const promptInput = document.querySelector("#prompt"); const messagesEl = document.querySelector("#messages"); const sendButton = document.querySelector("#sendButton"); const newChatButton = document.querySelector("#newChat"); const historyEl = document.querySelector("#history"); const chatTitleEl = document.querySelector("#chatTitle"); const modelSelect = document.querySelector("#modelSelect"); const ragToggle = document.querySelector("#ragToggle"); const ragStatus = document.querySelector("#ragStatus"); const reindexButton = document.querySelector("#reindexButton"); const metaLine = document.querySelector("#metaLine"); let activeChatId = null; let messages = []; function addMessage(role, content, meta = "") { const article = document.createElement("article"); article.className = `message ${role}`; const avatar = document.createElement("div"); avatar.className = "avatar"; avatar.textContent = role === "user" ? "U" : "L"; const bubble = document.createElement("div"); bubble.className = "bubble"; bubble.textContent = content; if (meta) { const metaEl = document.createElement("div"); metaEl.className = "message-meta"; metaEl.textContent = meta; bubble.append(document.createElement("br"), metaEl); } article.append(avatar, bubble); messagesEl.append(article); messagesEl.scrollTop = messagesEl.scrollHeight; return bubble; } function renderMessages() { messagesEl.innerHTML = ""; if (!messages.length) { addMessage("assistant", "Local LLM is ready. Ask in Korean or English."); return; } messages.forEach((message) => addMessage(message.role, message.content)); } function setTitle(title) { chatTitleEl.textContent = title || "Local LLM"; } function selectedModel() { return modelSelect.value || "qwen2.5:7b"; } async function loadModels() { const response = await fetch("/api/models"); const data = await response.json(); modelSelect.innerHTML = ""; data.models.forEach((model) => { const option = document.createElement("option"); option.value = model; option.textContent = model; if (model === data.defaultModel) { option.selected = true; } modelSelect.append(option); }); } async function loadRagStatus() { const response = await fetch("/api/rag/status"); const data = await response.json(); ragStatus.textContent = `RAG: ${data.files} files, ${data.chunks} chunks`; } async function loadHistory() { const response = await fetch("/api/chats"); const data = await response.json(); historyEl.innerHTML = ""; data.chats.forEach((chat) => { const item = document.createElement("button"); item.className = `history-item ${chat.id === activeChatId ? "active" : ""}`; item.type = "button"; item.textContent = chat.title; item.addEventListener("click", () => loadChat(chat.id)); historyEl.append(item); }); } async function loadChat(chatId) { const response = await fetch(`/api/chats/${chatId}`); const data = await response.json(); activeChatId = data.chat.id; messages = data.chat.messages || []; modelSelect.value = data.chat.model || selectedModel(); ragToggle.checked = data.chat.useRag !== false; setTitle(data.chat.title); renderMessages(); await loadHistory(); promptInput.focus(); } function resetChat() { activeChatId = null; messages = []; setTitle("Local LLM"); renderMessages(); loadHistory(); promptInput.focus(); } promptInput.addEventListener("input", () => { promptInput.style.height = "auto"; promptInput.style.height = `${promptInput.scrollHeight}px`; }); promptInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); form.requestSubmit(); } }); newChatButton.addEventListener("click", resetChat); reindexButton.addEventListener("click", async () => { reindexButton.disabled = true; ragStatus.textContent = "RAG: indexing"; try { const response = await fetch("/api/rag/reindex", { method: "POST" }); const data = await response.json(); ragStatus.textContent = `RAG: ${data.files} files, ${data.chunks} chunks`; } finally { reindexButton.disabled = false; } }); form.addEventListener("submit", async (event) => { event.preventDefault(); const content = promptInput.value.trim(); if (!content || sendButton.disabled) { return; } promptInput.value = ""; promptInput.style.height = "auto"; addMessage("user", content); messages.push({ role: "user", content }); sendButton.disabled = true; const pending = addMessage("assistant", "Thinking..."); try { const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chatId: activeChatId, messages, model: selectedModel(), useRag: ragToggle.checked, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "Response failed"); } const sourceText = data.sources?.length ? `sources: ${data.sources.map((item) => item.source).join(", ")}` : "no sources"; pending.textContent = data.reply; const meta = document.createElement("div"); meta.className = "message-meta"; meta.textContent = `${data.model} / ${(data.elapsedMs / 1000).toFixed(1)}s / ${sourceText}`; pending.append(document.createElement("br"), meta); activeChatId = data.chatId; messages = data.chat.messages || [...messages, { role: "assistant", content: data.reply }]; setTitle(data.chat.title); metaLine.textContent = `${data.model} with RAG ${ragToggle.checked ? "on" : "off"}`; await loadHistory(); } catch (error) { pending.textContent = `Error: ${error.message}`; } finally { sendButton.disabled = false; promptInput.focus(); } }); loadModels(); loadRagStatus(); renderMessages(); loadHistory();