| const CHAT_URL = "/api/chat"; |
| const MODEL = "llama3:latest"; |
|
|
| const BASE_SYSTEM = "You are helpful."; |
|
|
| const chatEl = document.getElementById("chat"); |
| const formEl = document.getElementById("form"); |
| const promptEl = document.getElementById("prompt"); |
| const searchBtn = document.getElementById("searchBtn"); |
| const sendBtn = document.getElementById("sendBtn"); |
|
|
| const statusDot = document.getElementById("statusDot"); |
| const statusText = document.getElementById("statusText"); |
| const heroEl = document.getElementById("hero"); |
|
|
| let searchMode = false; |
|
|
| function addMessage(role, text, { highlight = false } = {}) { |
| const msg = document.createElement("div"); |
| msg.className = `msg ${role}${highlight ? " highlight" : ""}`; |
|
|
| const bubble = document.createElement("div"); |
| bubble.className = "bubble"; |
| bubble.textContent = text; |
|
|
| msg.appendChild(bubble); |
| chatEl.appendChild(msg); |
| chatEl.scrollTop = chatEl.scrollHeight; |
|
|
| if (heroEl) heroEl.style.display = "none"; |
| return msg; |
| } |
|
|
| function setConnected(ok) { |
| statusDot.classList.toggle("on", ok); |
| statusText.textContent = ok ? "Connected" : "Disconnected"; |
| } |
|
|
| async function pingProxy() { |
| try { |
| const r = await fetch("/health"); |
| setConnected(r.ok); |
| } catch { |
| setConnected(false); |
| } |
| } |
|
|
| function autosizeTextarea(el) { |
| el.style.height = "auto"; |
| el.style.height = Math.min(el.scrollHeight, 180) + "px"; |
| } |
|
|
| promptEl.addEventListener("input", () => autosizeTextarea(promptEl)); |
|
|
| promptEl.addEventListener("keydown", (e) => { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| formEl.requestSubmit(); |
| } |
| }); |
|
|
| searchBtn.addEventListener("click", () => { |
| searchMode = !searchMode; |
| searchBtn.setAttribute("aria-pressed", String(searchMode)); |
| }); |
|
|
| formEl.addEventListener("submit", async (e) => { |
| e.preventDefault(); |
|
|
| const userText = promptEl.value.trim(); |
| if (!userText) return; |
|
|
| addMessage("user", userText); |
|
|
| promptEl.value = ""; |
| autosizeTextarea(promptEl); |
|
|
| sendBtn.disabled = true; |
| const placeholder = addMessage("ai", "Thinking…"); |
|
|
| try { |
| const body = { |
| model: MODEL, |
| messages: [ |
| { role: "system", content: BASE_SYSTEM }, |
| { role: "user", content: userText } |
| ], |
| stream: false |
| }; |
|
|
| const resp = await fetch(CHAT_URL, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(body) |
| }); |
|
|
| if (!resp.ok) { |
| const err = await resp.text(); |
| placeholder.querySelector(".bubble").textContent = `Error (${resp.status}): ${err}`; |
| placeholder.classList.add("highlight"); |
| return; |
| } |
|
|
| const data = await resp.json(); |
| const answer = data?.message?.content ?? "(No content)"; |
| placeholder.querySelector(".bubble").textContent = answer; |
|
|
| placeholder.classList.add("highlight"); |
| setTimeout(() => placeholder.classList.remove("highlight"), 1200); |
| } catch (err) { |
| placeholder.querySelector(".bubble").textContent = `Request failed: ${err?.message ?? String(err)}`; |
| placeholder.classList.add("highlight"); |
| } finally { |
| sendBtn.disabled = false; |
| promptEl.focus(); |
| await pingProxy(); |
| } |
| }); |
|
|
| pingProxy(); |
| setInterval(pingProxy, 5000); |
|
|