Initial commit.
Basic docker deployment with Local LLM integration and simple game state.
This commit is contained in:
115
web/static/app.js
Normal file
115
web/static/app.js
Normal file
@@ -0,0 +1,115 @@
|
||||
(() => {
|
||||
const el = {
|
||||
statusDot: document.getElementById("statusDot"),
|
||||
messages: document.getElementById("messages"),
|
||||
form: document.getElementById("chatForm"),
|
||||
input: document.getElementById("messageInput"),
|
||||
sendBtn: document.getElementById("sendBtn"),
|
||||
tplUser: document.getElementById("msg-user"),
|
||||
tplAssistant: document.getElementById("msg-assistant"),
|
||||
};
|
||||
|
||||
const state = {
|
||||
sending: false,
|
||||
};
|
||||
|
||||
function setStatus(ok) {
|
||||
el.statusDot.classList.toggle("ok", !!ok);
|
||||
el.statusDot.classList.toggle("err", !ok);
|
||||
}
|
||||
|
||||
async function healthCheck() {
|
||||
try {
|
||||
const res = await fetch("/api/health", { cache: "no-store" });
|
||||
setStatus(res.ok);
|
||||
} catch {
|
||||
setStatus(false);
|
||||
}
|
||||
}
|
||||
|
||||
function appendMessage(role, text) {
|
||||
const tpl = role === "user" ? el.tplUser : el.tplAssistant;
|
||||
const node = tpl.content.cloneNode(true);
|
||||
const bubble = node.querySelector(".bubble");
|
||||
bubble.textContent = text;
|
||||
el.messages.appendChild(node);
|
||||
el.messages.scrollTop = el.messages.scrollHeight;
|
||||
}
|
||||
|
||||
function setSending(sending) {
|
||||
state.sending = sending;
|
||||
el.input.disabled = sending;
|
||||
el.sendBtn.disabled = sending;
|
||||
el.sendBtn.textContent = sending ? "Sending..." : "Send";
|
||||
}
|
||||
|
||||
async function sendMessage(text) {
|
||||
setSending(true);
|
||||
try {
|
||||
const res = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({ message: text }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let detail = "";
|
||||
try {
|
||||
const data = await res.json();
|
||||
detail = data.detail || res.statusText;
|
||||
} catch {
|
||||
detail = res.statusText;
|
||||
}
|
||||
throw new Error(detail || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
appendMessage("assistant", data.reply ?? "");
|
||||
setStatus(true);
|
||||
} catch (err) {
|
||||
appendMessage("assistant", `Error: ${err.message || err}`);
|
||||
setStatus(false);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}
|
||||
|
||||
el.form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const text = (el.input.value || "").trim();
|
||||
if (!text || state.sending) return;
|
||||
appendMessage("user", text);
|
||||
el.input.value = "";
|
||||
await sendMessage(text);
|
||||
});
|
||||
|
||||
// Submit on Enter, allow Shift+Enter for newline (if we switch to textarea later)
|
||||
el.input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
el.form.requestSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial status check
|
||||
healthCheck();
|
||||
|
||||
// Initialize session and show start message if configured
|
||||
(async function initSession() {
|
||||
try {
|
||||
const res = await fetch("/api/session", { credentials: "same-origin" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.start_message) {
|
||||
appendMessage("assistant", data.start_message);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// no-op
|
||||
}
|
||||
})();
|
||||
|
||||
// Periodic health check
|
||||
setInterval(healthCheck, 15000);
|
||||
})();
|
||||
Reference in New Issue
Block a user