206 lines
5.8 KiB
JavaScript
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();
|