Files
llm_web/app.js
T

206 lines
5.8 KiB
JavaScript

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();