| const MAX_FILE_SIZE = 50 * 1024 * 1024; |
| let threadId = sessionStorage.getItem("rag_thread_id") || crypto.randomUUID(); |
| sessionStorage.setItem("rag_thread_id", threadId); |
|
|
| |
| |
| |
| function initTheme() { |
| const savedTheme = localStorage.getItem("theme") || "light"; |
| document.documentElement.setAttribute("data-theme", savedTheme); |
| updateThemeIcon(savedTheme); |
| } |
|
|
| function toggleTheme() { |
| const current = document.documentElement.getAttribute("data-theme"); |
| const next = current === "light" ? "dark" : "light"; |
| document.documentElement.setAttribute("data-theme", next); |
| localStorage.setItem("theme", next); |
| updateThemeIcon(next); |
| } |
|
|
| function updateThemeIcon(theme) { |
| const btn = document.getElementById("themeToggleBtn"); |
| if (!btn) return; |
| |
| btn.innerHTML = theme === "light" |
| ? `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>` |
| : `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>`; |
| } |
|
|
| |
| |
| |
|
|
| |
| function handleKey(e) { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| ask(); |
| } |
| } |
|
|
| async function upload() { |
| const fileInput = document.getElementById("files"); |
| const files = fileInput.files; |
| const statusDiv = document.getElementById("uploadStatus"); |
| const progressContainer = document.querySelector(".progress-container"); |
| const progressBar = document.querySelector(".progress-bar"); |
|
|
| if (!files.length) { |
| alert("Please select at least one file."); |
| return; |
| } |
|
|
| |
| for (let f of files) { |
| if (f.size > MAX_FILE_SIZE) { |
| alert(`File "${f.name}" is too large. Max allowed is 50MB.`); |
| return; |
| } |
| } |
|
|
| |
| if (statusDiv) statusDiv.innerText = "Uploading..."; |
| if (progressContainer) { |
| progressContainer.style.display = "block"; |
| progressBar.style.width = "0%"; |
| } |
|
|
| const fd = new FormData(); |
| for (let f of files) fd.append("files", f); |
|
|
| const xhr = new XMLHttpRequest(); |
|
|
| xhr.upload.addEventListener("progress", (event) => { |
| if (event.lengthComputable) { |
| const percent = Math.round((event.loaded / event.total) * 100); |
| if (progressBar) progressBar.style.width = percent + "%"; |
| } |
| }); |
|
|
| xhr.addEventListener("load", () => { |
| if (xhr.status >= 200 && xhr.status < 300) { |
| try { |
| const data = JSON.parse(xhr.responseText); |
| if (statusDiv) { |
| statusDiv.innerText = data.message || "Upload complete!"; |
| statusDiv.style.color = "var(--success, green)"; |
| } |
| if (progressBar) progressBar.style.width = "100%"; |
| fileInput.value = ""; |
|
|
| |
| addMessage("ai", `✅ **System:** ${data.message || "Files processed successfully."}`); |
| } catch (e) { |
| if (statusDiv) statusDiv.innerText = "Error parsing server response."; |
| } |
| } else { |
| if (statusDiv) statusDiv.innerText = "Upload failed."; |
| } |
| }); |
|
|
| xhr.addEventListener("error", () => { |
| if (statusDiv) statusDiv.innerText = "Network error."; |
| }); |
|
|
| xhr.open("POST", "/upload"); |
| xhr.send(fd); |
| } |
|
|
| async function ask() { |
| const input = document.getElementById("question"); |
| const q = input.value.trim(); |
| if (!q) return; |
|
|
| |
| input.value = ""; |
|
|
| |
| addMessage("user", q); |
|
|
| |
| const thinkingId = addMessage("ai", '<div class="typing-indicator">Thinking...</div>'); |
|
|
| try { |
| const res = await fetch("/ask", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ prompt: q, thread_id: threadId }) |
| }); |
|
|
| const data = await res.json(); |
|
|
| |
| removeMessage(thinkingId); |
|
|
| let html = formatAnswer(data); |
| addMessage("ai", html); |
|
|
| saveToHistory(q, html); |
|
|
| } catch (e) { |
| removeMessage(thinkingId); |
| addMessage("ai", "❌ **Error:** Could not connect to the server."); |
| } |
| } |
|
|
| function formatAnswer(data) { |
| let html = data.answer |
| .replace(/\n/g, "<br>") |
| .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>"); |
|
|
| if (data.confidence > 0) { |
| let label = "Low"; |
| if (data.confidence >= 0.7) label = "High"; |
| else if (data.confidence >= 0.5) label = "Medium"; |
| html += `<br><div class="confidence-badge">Confidence: ${label} (${Math.round(data.confidence * 100)}%)</div>`; |
| } |
|
|
| if (data.citations?.length) { |
| html += "<div class='citation-box'><strong>Sources:</strong><ul>"; |
| data.citations.forEach(c => { |
| html += `<li>${c.source} (Page ${c.page})</li>`; |
| }); |
| html += "</ul></div>"; |
| } |
| return html; |
| } |
|
|
| function summarize() { |
| const input = document.getElementById("question"); |
| input.value = "Summarize the uploaded documents"; |
| ask(); |
| } |
|
|
| |
| |
| |
| function addMessage(type, html) { |
| const chatContainer = document.getElementById("chatContainer"); |
| const div = document.createElement("div"); |
| div.className = `message ${type}-message`; |
| div.id = "msg-" + Date.now(); |
|
|
| div.innerHTML = ` |
| <div class="bubble ${type}-bubble glass"> |
| ${html} |
| </div> |
| `; |
|
|
| chatContainer.appendChild(div); |
| scrollToBottom(); |
| return div.id; |
| } |
|
|
| function removeMessage(id) { |
| const el = document.getElementById(id); |
| if (el) el.remove(); |
| } |
|
|
| function scrollToBottom() { |
| const container = document.getElementById("mainContent"); |
| container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' }); |
| } |
|
|
| |
| |
| |
| function loadHistory() { |
| const list = document.getElementById("historyList"); |
| if (!list) return; |
| list.innerHTML = ""; |
| const history = JSON.parse(localStorage.getItem("rag_history") || "[]"); |
|
|
| history.forEach((item, index) => { |
| const div = document.createElement("div"); |
| div.className = "history-item"; |
| div.innerText = item.query; |
| div.onclick = () => loadSession(item); |
| list.appendChild(div); |
| }); |
| } |
|
|
| function saveToHistory(query, answerHtml) { |
| const history = JSON.parse(localStorage.getItem("rag_history") || "[]"); |
| history.unshift({ query, answerHtml, timestamp: Date.now() }); |
| if (history.length > 50) history.pop(); |
| localStorage.setItem("rag_history", JSON.stringify(history)); |
| loadHistory(); |
| } |
|
|
| function loadSession(item) { |
| |
| |
| document.getElementById("chatContainer").innerHTML = ""; |
| addMessage("user", item.query); |
| addMessage("ai", item.answerHtml); |
| } |
|
|
| function newChat() { |
| document.getElementById("chatContainer").innerHTML = ""; |
| addMessage("ai", "Hello! I am NexusGraph AI. Upload a document or ask me anything."); |
| threadId = crypto.randomUUID(); |
| sessionStorage.setItem("rag_thread_id", threadId); |
| } |
|
|
| function clearHistory() { |
| if (confirm("Delete all history?")) { |
| localStorage.removeItem("rag_history"); |
| loadHistory(); |
| newChat(); |
| } |
| } |
|
|
| |
| window.addEventListener("DOMContentLoaded", () => { |
| initTheme(); |
| loadHistory(); |
| newChat(); |
| }); |
|
|