document.addEventListener("DOMContentLoaded", function () { // Cache DOM Elements const ui = { openBtn: document.getElementById("open-rag-frame"), closeBtn: document.getElementById("close-rag-frame"), ragContainer: document.getElementById("rag-container"), ragHeader: document.getElementById("rag-header"), submitQuestionBtn: document.getElementById("rag-submit-btn"), summarizeBtn: document.getElementById("rag-summarize-btn"), questionInput: document.getElementById("rag-question"), ragMessagesArea: document.getElementById("rag-messages-area"), historyArea: document.getElementById("rag-chat-history-area") }; const uploadUi = { openBtn: document.getElementById("open-pdf-upload"), modal: document.getElementById("pdf-upload-modal"), closeBtn: document.getElementById("pdf-upload-close"), cancelBtn: document.getElementById("pdf-upload-cancel"), submitBtn: document.getElementById("pdf-upload-submit"), fileInput: document.getElementById("pdf-upload-file"), numberInput: document.getElementById("pdf-upload-number"), titleInput: document.getElementById("pdf-upload-title"), backgroundInput: document.getElementById("pdf-upload-background"), createMarkdown: document.getElementById("pdf-upload-create-md"), status: document.getElementById("pdf-upload-status") }; const defaultBackgroundValue = uploadUi.backgroundInput ? uploadUi.backgroundInput.value : 'Blue'; const RAG_SESSION_ID = "user_" + Date.now(); // --- Upload UI Helpers --- function setUploadStatus(message, tone = "info") { if (!uploadUi.status) return; uploadUi.status.textContent = message || ""; if (tone === "error") { uploadUi.status.style.color = "#c0392b"; } else if (tone === "success") { uploadUi.status.style.color = "#1b7e39"; } else { uploadUi.status.style.color = "#222"; } } async function handleQuestionSubmit() { console.log("DEBUG: handleQuestionSubmit() called"); const userQuestion = ui.questionInput.value.trim(); if (!userQuestion) return; appendUserMessage(userQuestion); ui.questionInput.value = ""; appendBotMessage("Thinking...", true); try { let ocrText = ""; // --- OPTIMIZATION START --- // 1. Check if we already have a session ID if (currentSessionDocId) { console.log(`DEBUG: Session ID exists (${currentSessionDocId}). SKIPPING text extraction.`); // We leave ocrText as empty string "". // The 'callPhpBackend' function will handle this and NOT send the text field. } else { // 2. Only extract text if this is the FIRST interaction console.log("DEBUG: No Session ID found. Extracting full text..."); ocrText = await getDocumentHocrTextAsync(); if (ocrText === null) { updateLastBotMessage("Could not retrieve document text."); return; } if (ocrText.trim() === "") { updateLastBotMessage("Document is empty. Cannot answer."); return; } } // --- OPTIMIZATION END --- // 3. Call Backend const responseData = await callPhpBackend(ocrText, userQuestion, "answer_question", currentSessionDocId); processBackendResponse(responseData, responseData.task_performed || "answer_question"); chatHistory.push({ question: userQuestion, answer: responseData.answer || "No answer provided", task_performed: "question_answering", timestamp: new Date().toISOString() }); } catch (error) { console.error("Error during Q&A:", error); updateLastBotMessage(`Sorry, an error occurred: ${error.message}`); } } async function handleSummarizeRequest() { console.log("DEBUG: handleSummarizeRequest() called"); appendUserMessage("Please summarize the document."); const startTs = (window.performance && performance.now) ? performance.now() : Date.now(); const tempMsg = appendBotMessage("Summarizing... 0.0s", true); // Timer for visual feedback const tick = setInterval(() => { try { const now = (window.performance && performance.now) ? performance.now() : Date.now(); const secs = ((now - startTs) / 1000).toFixed(1); if (tempMsg) tempMsg.innerText = `Summarizing... ${secs}s`; } catch {} }, 250); try { let ocrText = ""; // OPTIMIZATION: If ID exists, skip text extraction if (currentSessionDocId) { console.log(`DEBUG: ID exists (${currentSessionDocId}). Using cache for summary.`); } else { ocrText = await getDocumentHocrTextAsync(); if (!ocrText || ocrText.trim() === "") { updateLastBotMessage("Document is empty."); clearInterval(tick); return; } } const responseData = await callPhpBackend(ocrText, null, "summarize_document", currentSessionDocId); clearInterval(tick); // Stop timer // Calculate processing time try { const end = (window.performance && performance.now) ? performance.now() : Date.now(); responseData.client_processing_time = parseFloat(((end - startTs) / 1000).toFixed(2)); } catch {} processBackendResponse(responseData, responseData.task_performed || "summarize_document"); chatHistory.push({ question: "Summarize the document", answer: responseData.answer || responseData.summary || "No summary provided", task_performed: "summarization", timestamp: new Date().toISOString() }); } catch (error) { clearInterval(tick); console.error("Error during Summarization:", error); updateLastBotMessage(`Sorry, an error occurred: ${error.message}`); } } function resetUploadForm() { if (uploadUi.fileInput) uploadUi.fileInput.value = ""; if (uploadUi.numberInput) uploadUi.numberInput.value = ""; if (uploadUi.titleInput) uploadUi.titleInput.value = ""; if (uploadUi.backgroundInput) uploadUi.backgroundInput.value = defaultBackgroundValue; if (uploadUi.createMarkdown) uploadUi.createMarkdown.checked = false; setUploadStatus(""); } function hideUploadModal() { if (uploadUi.modal) { uploadUi.modal.style.display = "none"; } } async function submitPdfUpload() { if (!uploadUi.fileInput || !uploadUi.fileInput.files || uploadUi.fileInput.files.length === 0) { setUploadStatus("Please choose a PDF file.", "error"); return; } const file = uploadUi.fileInput.files[0]; if (!file.name.toLowerCase().endsWith('.pdf')) { setUploadStatus("Only PDF files are supported.", "error"); return; } const number = (uploadUi.numberInput && uploadUi.numberInput.value ? uploadUi.numberInput.value : "").trim(); if (!/^\d{6,}$/.test(number)) { setUploadStatus("Enter a numeric Dings number (at least 6 digits).", "error"); return; } const formData = new FormData(); formData.append("pdf_file", file); formData.append("dings_number", number); const title = (uploadUi.titleInput && uploadUi.titleInput.value ? uploadUi.titleInput.value : "").trim(); if (title) { formData.append("title", title); } const background = (uploadUi.backgroundInput && uploadUi.backgroundInput.value ? uploadUi.backgroundInput.value : "").trim() || "Blue"; formData.append("background_color", background); if (uploadUi.createMarkdown && uploadUi.createMarkdown.checked) { formData.append("create_markdown", "1"); } setUploadStatus("Uploading and processing…", "info"); if (uploadUi.submitBtn) uploadUi.submitBtn.disabled = true; try { const response = await fetch('/api/pdf-upload', { method: 'POST', body: formData }); let payload; try { payload = await response.json(); } catch (jsonErr) { payload = { success: false, error: 'Invalid response from server.' }; } if (!response.ok || !payload.success) { const errorMsg = payload && payload.error ? payload.error : `Upload failed (${response.status})`; setUploadStatus(errorMsg, "error"); console.error("PDF upload failed", payload); return; } const extras = []; if (payload.generated && Array.isArray(payload.generated.hocr_files)) { extras.push(`HOCR pages: ${payload.generated.hocr_files.length}`); } if (payload.generated && payload.generated.thumbnail) { extras.push(`Thumbnail: ${payload.generated.thumbnail}`); } const extraInfo = extras.length ? ` (${extras.join(', ')})` : ''; setUploadStatus((payload.message || 'Upload complete.') + extraInfo, "success"); setTimeout(() => { hideUploadModal(); resetUploadForm(); }, 1200); } catch (err) { console.error("Upload error", err); setUploadStatus(`Upload failed: ${err.message}`, "error"); } finally { if (uploadUi.submitBtn) uploadUi.submitBtn.disabled = false; } } // --- Event Listeners (Upload) --- if (uploadUi.openBtn && uploadUi.modal) { uploadUi.openBtn.addEventListener("click", () => { uploadUi.modal.style.display = "block"; setUploadStatus(""); }); } if (uploadUi.closeBtn) { uploadUi.closeBtn.addEventListener("click", () => { hideUploadModal(); resetUploadForm(); }); } if (uploadUi.cancelBtn) { uploadUi.cancelBtn.addEventListener("click", () => { hideUploadModal(); resetUploadForm(); }); } if (uploadUi.submitBtn) { uploadUi.submitBtn.addEventListener("click", submitPdfUpload); } // --- RAG Chat Logic --- let chatHistory = []; let currentSessionDocId = null; // To track the current document session // Initial welcome message if (ui.ragMessagesArea) { appendBotMessage("Welcome! Ask a question or request a summary of the document."); } // Toggle RAG Window if (ui.openBtn && ui.ragContainer) { ui.openBtn.addEventListener("click", function () { ui.ragContainer.style.display = "block"; }); } if (ui.closeBtn && ui.ragContainer) { ui.closeBtn.addEventListener("click", function () { ui.ragContainer.style.display = "none"; triggerSessionCleanup(); // Clean up session on close }); } if (ui.ragHeader && ui.ragContainer) { enableDragging(ui.ragHeader, ui.ragContainer); } if (ui.submitQuestionBtn && ui.questionInput) { ui.submitQuestionBtn.addEventListener("click", handleQuestionSubmit); ui.questionInput.addEventListener("keypress", function(event) { if (event.key === "Enter") { event.preventDefault(); handleQuestionSubmit(); } }); } if (ui.summarizeBtn) { ui.summarizeBtn.addEventListener("click", handleSummarizeRequest); } // --- NEW CLEANUP LOGIC --- function triggerSessionCleanup() { if (!currentSessionDocId) return; // CRITICAL: Make sure this matches your actual PHP filename! // const phpRagUrl = "./rag_local.php"; const pythonRagUrl = "/api/rag"; // Same origin to avoid CORS const payload = JSON.stringify({ task: "cleanup", question: "CLEANUP_SIGNAL", // Explicit signal for Python document_id: currentSessionDocId }); const blob = new Blob([payload], { type: 'application/json' }); // sendBeacon is best for closing windows if (navigator.sendBeacon) { navigator.sendBeacon(pythonRagUrl, blob); console.log("RAG Cleanup: Beacon sent for ID", currentSessionDocId); } else { fetch(pythonRagUrl, { method: "POST", body: payload, headers: { "Content-Type": "application/json" }, keepalive: true }); } currentSessionDocId = null; } // Trigger cleanup if user closes the browser tab or refreshes window.addEventListener("beforeunload", function() { triggerSessionCleanup(); }); // --- Core Logic Functions --- async function handleQuestionSubmit() { console.log("DEBUG: handleQuestionSubmit() called"); const userQuestion = ui.questionInput.value.trim(); if (!userQuestion) return; console.log("DEBUG: User question:", userQuestion); appendUserMessage(userQuestion); ui.questionInput.value = ""; appendBotMessage("Thinking...", true); try { // Get ALL text (Unlimited) const ocrText = await getDocumentHocrTextAsync(); console.log("DEBUG: Retrieved OCR text length:", ocrText ? ocrText.length : "null"); if (ocrText === null) { updateLastBotMessage("Could not retrieve document text."); return; } if (ocrText.trim() === "") { updateLastBotMessage("Document is empty. Cannot answer."); return; } console.log("DEBUG: About to call callPhpBackend for question"); const responseData = await callPhpBackend(ocrText, userQuestion, "answer_question"); console.log("DEBUG: Got response from callPhpBackend:", responseData); processBackendResponse(responseData, responseData.task_performed || "answer_question"); chatHistory.push({ question: userQuestion, answer: responseData.answer || "No answer provided", task_performed: "question_answering", timestamp: new Date().toISOString() }); } catch (error) { console.error("Error during Q&A:", error); updateLastBotMessage(`Sorry, an error occurred: ${error.message}`); } } async function handleSummarizeRequest() { console.log("DEBUG: handleSummarizeRequest() called"); appendUserMessage("Please summarize the document."); const startTs = (window.performance && performance.now) ? performance.now() : Date.now(); const tempMsg = appendBotMessage("Summarizing... 0.0s", true); const tick = setInterval(() => { try { const now = (window.performance && performance.now) ? performance.now() : Date.now(); const secs = ((now - startTs) / 1000).toFixed(1); if (tempMsg) tempMsg.innerText = `Summarizing... ${secs}s`; } catch {} }, 250); try { // Get ALL text (Unlimited) const ocrText = await getDocumentHocrTextAsync(); console.log("DEBUG: Retrieved OCR text for summary, length:", ocrText ? ocrText.length : "null"); if (ocrText === null) { updateLastBotMessage("Could not retrieve document text to summarize."); return; } if (ocrText.trim() === "") { updateLastBotMessage("Document is empty. Nothing to summarize."); return; } console.log("DEBUG: About to call callPhpBackend for summary"); const responseData = await callPhpBackend(ocrText, null, "summarize_document"); console.log("DEBUG: Got response from callPhpBackend for summary:"); console.log(" - Type:", typeof responseData); console.log(" - Is null:", responseData === null); console.log(" - Is undefined:", responseData === undefined); console.log(" - Full response:", responseData); console.log(" - Response keys:", responseData ? Object.keys(responseData) : "No keys (null/undefined)"); if (responseData) { console.log(" - Has answer:", !!responseData.answer); console.log(" - Has text:", !!responseData.text); console.log(" - Has summary:", !!responseData.summary); console.log(" - Has error:", !!responseData.error); } clearInterval(tick); try { const end = (window.performance && performance.now) ? performance.now() : Date.now(); responseData.client_processing_time = parseFloat(((end - startTs) / 1000).toFixed(2)); } catch {} processBackendResponse(responseData, responseData.task_performed || "summarize_document"); chatHistory.push({ question: "Summarize the document", answer: responseData.answer || responseData.summary || "No summary provided", task_performed: "summarization", timestamp: new Date().toISOString() }); } catch (error) { clearInterval(tick); console.error("Error during Summarization:", error); updateLastBotMessage(`Sorry, an error occurred: ${error.message}`); } } // =========================================================== // TEXT EXTRACTION - UNLIMITED VERSION // =========================================================== // 1. Get text from CURRENT frame (Unlimited) async function getHocrTextAsync() { return new Promise((resolve) => { const iframe = document.querySelector('.editor'); if (!iframe) { console.error("Iframe (editor) not found."); resolve(null); return; } const attemptExtraction = () => { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (!iframeDoc) { resolve(null); return; } const ocrPage = iframeDoc.querySelector('.ocr_page'); if (ocrPage) { let selected = ""; try { const sel = iframeDoc.getSelection && iframeDoc.getSelection(); selected = sel && sel.toString ? sel.toString().trim() : ""; } catch {} let text = selected && selected.length > 20 ? selected : ocrPage.textContent.trim(); text = text.replace(/\s+/g, ' '); // REMOVED: Character Limit Check console.log(`Extracted current page text: ${text.length} chars`); resolve(text); return; } const words = iframeDoc.querySelectorAll('.ocrx_word'); if (words.length === 0) { resolve(""); return; } let allText = ''; words.forEach(word => { allText += word.textContent.trim() + ' '; }); resolve(allText.trim()); }; if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') { attemptExtraction(); } else { iframe.onload = attemptExtraction; setTimeout(() => { if (iframe.contentDocument && iframe.contentDocument.readyState !== 'complete' && !iframe.hasAlreadyLoadedForRag) { attemptExtraction(); iframe.hasAlreadyLoadedForRag = true; } else if (!iframe.hasAlreadyLoadedForRag && (!iframe.contentDocument || iframe.contentDocument.readyState !== 'complete') ){ resolve(null); } }, 1500); } }); } // 2. Get text from ALL loaded frames (Unlimited) function getAllPagesTextFromIframe() { try { const iframe = document.querySelector('.editor'); const doc = iframe && (iframe.contentDocument || iframe.contentWindow?.document); if (!doc) return ''; const pages = doc.querySelectorAll('.ocr_page'); if (!pages || pages.length === 0) return ''; let combined = ''; pages.forEach(p => { const t = (p.textContent || '').replace(/\s+/g, ' ').trim(); if (t) combined += (combined ? ' ' : '') + t; }); return combined; } catch (e) { return ''; } } // 3. Fetch ALL hOCR files from server (Unlimited loop) async function getAllPagesHocrTextAsync() { const baseUrl = './'; let combined = ''; return new Promise((resolve) => { const tempViewer = { detectTotalPages: function(callback) { let page = 1; let pages = []; function checkNextPage() { // Check for edited version first, then original let editedUrl = `${baseUrl}${buildHocrFileName(page, '_edited')}`; let originalUrl = `${baseUrl}${buildHocrFileName(page)}`; fetch(editedUrl) .then(response => { if (response.ok) { // Edited version exists, use it pages.push(editedUrl); console.log(`RAG: Found edited page ${page}: ${editedUrl}`); page++; checkNextPage(); } else { // Try original version return fetch(originalUrl); } }) .then(response => { if (response && response.ok) { // Original version exists pages.push(originalUrl); console.log(`RAG: Found original page ${page}: ${originalUrl}`); page++; checkNextPage(); } else if (response) { // Neither edited nor original exists, we're done return callback(pages); } }) .catch(err => { return callback(pages); }); } checkNextPage(); } }; tempViewer.detectTotalPages(async (hocrFiles) => { console.log(`RAG: Downloading ${hocrFiles.length} total pages...`); for (let i = 0; i < hocrFiles.length; i++) { const hocrUrl = hocrFiles[i]; try { const res = await fetch(hocrUrl, { method: 'GET' }); if (res.ok) { const hocr = await res.text(); const text = hocr.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); combined += (combined ? ' ' : '') + text; // REMOVED: The break condition that stopped at 45000 chars } } catch (e) { console.log(`Error loading page ${i + 1}: ${e.message}`); } } console.log(`Full document extraction complete: ${combined.length} total chars`); resolve(combined); }); }); } // 4. Master function to get text (Unlimited) async function getDocumentHocrTextAsync() { try { const iframe = document.querySelector('.editor'); const iframeDoc = iframe && (iframe.contentDocument || iframe.contentWindow?.document); // 1) USER SELECTION (Priority) if (iframeDoc) { try { const sel = iframeDoc.getSelection && iframeDoc.getSelection(); const selected = sel && sel.toString ? sel.toString().trim() : ''; if (selected && selected.length > 20) { const s = selected.replace(/\s+/g, ' '); console.log('RAG: Using User Selection:', s.length); // REMOVED: .slice() return s; } } catch {} } // 2) ALL LOADED IFRAME TEXT const iframeAll = getAllPagesTextFromIframe(); if (iframeAll && iframeAll.length > 0) { console.log("RAG: Using iframe combined text:", iframeAll.length); // REMOVED: .slice() return iframeAll; } // 3) FETCH ALL PAGES FROM SERVER const combined = await getAllPagesHocrTextAsync(); if (combined && combined.length) { console.log("RAG: Using fetched combined text:", combined.length); // REMOVED: .slice() return combined; } // 4) FALLBACK (Current Page) const cur = await getHocrTextAsync(); console.log("RAG: Using current page fallback:", cur ? cur.length : 0); // REMOVED: .slice() return cur; } catch (e) { console.warn("getDocumentHocrTextAsync() failed, fallback to current page:", e); const fallback = await getHocrTextAsync(); return fallback; } } // =========================================================== // BACKEND COMMUNICATION // =========================================================== async function callRagBackend(payload) { // Use Python RAG backend (300020001.py) - same origin to avoid CORS const pythonRagUrl = "/api/rag"; // Relative URL - same port as HTML server // const pythonRagUrl = "http://localhost:8888/api/rag"; // Cross-origin (causes CORS) console.log("XXX callRagBackend"); // Transform payload format for Python RAG backend const pythonPayload = {}; // Map frontend format to Python format if (payload.hocr_text) { pythonPayload.text = payload.hocr_text; } if (payload.text) { pythonPayload.text = payload.text; } if (payload.task) { pythonPayload.task = payload.task; } if (payload.question) { pythonPayload.question = payload.question; } if (payload.document_id) { pythonPayload.document_id = payload.document_id; } if (payload.user) { pythonPayload.user = payload.user; } if (payload.chat_history) { pythonPayload.chat_history = payload.chat_history; } try { console.log("DEBUG: Connecting to Python RAG backend at", pythonRagUrl); console.log("DEBUG: Python payload:", pythonPayload); const res = await fetch(pythonRagUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(pythonPayload) }); const raw = await res.text(); console.log("DEBUG: Raw Python response:", raw); const responseData = JSON.parse(raw || "{}"); if (!res.ok) { const msg = responseData.error || `HTTP ${res.status} ${res.statusText}`; throw new Error(msg || "bad_gateway"); } // Transform PHP response to frontend format if (responseData.summary) { responseData.text = responseData.summary; // for summarize responseData.answer = responseData.summary; // for Q&A fallback } if (responseData.answer) { responseData.text = responseData.answer; // ensure text field exists } console.log("DEBUG: Got successful response from PHP RAG backend:", responseData); console.log("DEBUG: RAG Response Details:"); console.log("- Response type:", responseData.response_type || "php_backend"); console.log("- Processing time:", responseData.processing_time || "N/A"); if (responseData.answer) { console.log("- Answer length:", responseData.answer.length); console.log("- Answer content:", responseData.answer); } if (responseData.summary) { console.log("- Summary length:", responseData.summary.length); console.log("- Summary content:", responseData.summary); } return responseData; } catch (e) { console.error("PHP RAG backend connection failed:", e.message); throw new Error(`PHP RAG backend connection failed: ${e.message}`); } } async function callPhpBackend(text, question, task, specificDocId = null) { console.log("DEBUG: callPhpBackend() task:", task); const activeDocId = specificDocId || currentSessionDocId || "default_doc"; const payload = { task: task, document_id: activeDocId, user: RAG_SESSION_ID, // <--- Send unique ID here chat_history: chatHistory }; if (text && text.length > 5 && (!specificDocId && !currentSessionDocId)) { payload.text = text; } if (task === "answer_question") { payload.question = question; } // Debugging to prove we aren't sending massive text const textStatus = payload.text ? `SENDING (${payload.text.length} chars)` : "SKIPPING (Cached)"; console.log(`DEBUG: Payload -> DocID: ${activeDocId} | Text: ${textStatus}`); const responseData = await callRagBackend(payload); return responseData; } // Ensure RAG cache is cleared on close (function registerRagCleanupOnClose() { function sendCleanup() { try { const url = "/api/rag"; // Same origin cleanup endpoint const blob = new Blob([JSON.stringify({reason: "page_close"})], {type: "application/json"}); if (navigator.sendBeacon) { navigator.sendBeacon(url, blob); } else { fetch(url, {method: "POST", body: blob, headers: {"Content-Type": "application/json"}, keepalive: true}).catch(()=>{}); } } catch(e) {} } window.addEventListener("pagehide", sendCleanup); window.addEventListener("beforeunload", sendCleanup); })(); function processBackendResponse(data, taskPerformed) { if (!data || typeof data !== 'object') { updateLastBotMessage("Invalid response."); return; } if (data.error) { updateLastBotMessage(`Error: ${data.error}`); return; } // --- FIX: Correctly save the Document ID --- if (data.document_id) { // Only update if it's different/new if (currentSessionDocId !== data.document_id) { console.log(`DEBUG: Session ID updated: ${currentSessionDocId} -> ${data.document_id}`); currentSessionDocId = data.document_id; } } // ------------------------------------------- // Handle summarization if (taskPerformed === "summarize_document" || taskPerformed === "summarization") { let summaryText = data.summary || data.answer || data.result; if (summaryText) { const t = (typeof data.processing_time === 'number' ? data.processing_time : data.client_processing_time); if (t) summaryText = ` ${t}s\n\n` + summaryText; updateLastBotMessage(`Summary:\n${summaryText}`); } } // Handle QA else if (taskPerformed === "answer_question" || taskPerformed === "question_answering") { let answerText = data.answer || data.result; if (answerText) { updateLastBotMessage(answerText); } } // Fallback else { updateLastBotMessage(data.answer || "Action completed."); } } // --- UI Utility Functions --- function appendMessage(text, type) { if (!ui.ragMessagesArea) return; const messageDiv = document.createElement("div"); messageDiv.className = `rag-message ${type}`; messageDiv.innerText = text; ui.ragMessagesArea.appendChild(messageDiv); ui.ragMessagesArea.scrollTop = ui.ragMessagesArea.scrollHeight; return messageDiv; } function appendUserMessage(text) { return appendMessage(text, "rag-user"); } function appendBotMessage(text, isTemporary = false) { const botMessageDiv = appendMessage(text, "rag-bot"); if (isTemporary && botMessageDiv) botMessageDiv.classList.add("rag-temporary"); return botMessageDiv; } function updateLastBotMessage(newText) { if (!ui.ragMessagesArea) return; const lastBotMessage = ui.ragMessagesArea.querySelector('.rag-message.rag-bot.rag-temporary'); if (lastBotMessage) { lastBotMessage.innerText = newText; lastBotMessage.classList.remove("rag-temporary"); } else { appendBotMessage(newText); } ui.ragMessagesArea.scrollTop = ui.ragMessagesArea.scrollHeight; } function enableDragging(headerElem, containerElem) { let offsetX, offsetY; headerElem.addEventListener('mousedown', function (e) { if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return; offsetX = e.clientX - containerElem.getBoundingClientRect().left; offsetY = e.clientY - containerElem.getBoundingClientRect().top; const originalCursor = headerElem.style.cursor; headerElem.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; function dragMouseMove(ev) { containerElem.style.left = `${ev.clientX - offsetX}px`; containerElem.style.top = `${ev.clientY - offsetY}px`; } function stopDragging() { document.removeEventListener('mousemove', dragMouseMove); document.removeEventListener('mouseup', stopDragging); headerElem.style.cursor = originalCursor || 'grab'; document.body.style.userSelect = ''; } document.addEventListener('mousemove', dragMouseMove); document.addEventListener('mouseup', stopDragging); }); } function displayChatHistory(history) { if (!ui.historyArea) return; let html = "