// CHANGED/ADDED: Declare global variable for modal let bootstrapFeedbackModal = null; const userId = "{{ user_id }}"; const effortId = "{{ effort_id }}"; const notebookId = "{{ notebook_id }}" const effortName = "{{ effort_name }}"; const notebookName = "{{ notebook_name}}" const preloadedFiles = {{ files | tojson }}; const preloadedZones = {{ zones | tojson }}; const globalZones = {{ global_ezones | tojson }}; const chatHistory = {{ chat_history | tojson }}; const chatBox = document.getElementById("chat-box"); const zoneDropdown = document.getElementById("zone-dropdown"); const fileList = document.getElementById("file-list"); const modal = document.getElementById("uploadModal"); const fileInput = document.getElementById("fileInput"); const backButton = document.getElementById("backBtn"); // Moved up for clarity fileInput.addEventListener("change", () => { const list = document.getElementById("selectedFilesList"); list.innerHTML = ""; const files = fileInput.files; if (!files.length) { list.textContent = "No files selected."; return; } for (let file of files) { const item = document.createElement("div"); item.className = "flex items-center gap-2"; item.innerHTML = `πŸ“„ ${file.name}`; list.appendChild(item); } }); let currentZoneId = null; let currentFileUri = ""; let selectedFileElement = null; function appendMessage(sender, message, color, alignRight = false) { const wrapper = document.createElement("div"); wrapper.className = alignRight ? "flex justify-end" : "flex justify-start"; const bubble = document.createElement("div"); bubble.className = `max-w-xl px-4 py-2 rounded-lg ${color} text-white`; bubble.innerHTML = `${sender}:
${message}
`; wrapper.appendChild(bubble); chatBox.appendChild(wrapper); chatBox.scrollTop = chatBox.scrollHeight; } function loadZones() { zoneDropdown.innerHTML = ""; const defaultOpt = document.createElement("option"); defaultOpt.disabled = true; defaultOpt.selected = true; defaultOpt.textContent = "Select a Configuration"; zoneDropdown.appendChild(defaultOpt); try { const addedZoneIds = new Set(); preloadedZones.forEach(zone => { if (!addedZoneIds.has(zone.id)) { const opt = document.createElement("option"); opt.value = zone.id; opt.textContent = zone.name; zoneDropdown.appendChild(opt); addedZoneIds.add(zone.id); } }); globalZones.forEach(zone => { if (!addedZoneIds.has(zone.id)) { const opt = document.createElement("option"); opt.value = zone.id; opt.textContent = zone.name + " (Global)"; zoneDropdown.appendChild(opt); addedZoneIds.add(zone.id); } }); const divider = document.createElement("option"); divider.disabled = true; divider.textContent = "──────────"; zoneDropdown.appendChild(divider); } catch (err) { console.error("Error loading zones:", err); } const manage = document.createElement("option"); manage.value = "manage"; manage.textContent = "βž• Manage Configurations"; zoneDropdown.appendChild(manage); zoneDropdown.addEventListener("change", (e) => { if (e.target.value === "manage") { const form = document.createElement("form"); form.method = "POST"; form.action = "/chat_feature/manage_zones"; const userInput = document.createElement("input"); userInput.type = "hidden"; userInput.name = "user_id"; userInput.value = userId; form.appendChild(userInput); const effortInput = document.createElement("input"); effortInput.type = "hidden"; effortInput.name = "effort_id"; effortInput.value = effortId; form.appendChild(effortInput); const notebookInput = document.createElement("input"); notebookInput.type = "hidden"; notebookInput.name = "notebook_id"; notebookInput.value = notebookId; form.appendChild(notebookInput); document.body.appendChild(form); form.submit(); } else { currentZoneId = parseInt(e.target.value); console.log("Zone selected, currentZoneID:", currentZoneId); } }); } // CHANGED/ADDED: Modal initialization moved inside window.onload window.onload = function () { loadFiles(); loadZones(); // CHANGED/ADDED: Modal is now initialized after DOM is loaded const feedbackModalEl = document.getElementById("feedbackModal"); bootstrapFeedbackModal = new bootstrap.Modal(feedbackModalEl); let historyFilename = "" if (chatHistory && chatHistory.length > 0) { chatHistory.forEach(entry => { if (entry.filename && (entry.filename !== historyFilename || historyFilename === "")) { historyFilename = entry.filename; appendMessage("System", `Selected file: ${historyFilename}`, "bg-purple-600"); } const userMessage = entry.question; appendMessage("You", userMessage, "bg-blue-500", true); const personaMessage = entry.persona; appendMessage("Persona", personaMessage, "bg-blue-500", true); const botMessage = entry.response; appendMessage("Assistant", botMessage, "bg-gray-700", false); }); } }; // Feedback Modal POP-UP on Back Button backButton.addEventListener("click", async function(event){ console.log("Back button clicked"); event.preventDefault(); if(!currentZoneId){ console.log("no configuration selected, going back directly") window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; return; } console.log("Debug BackBtn:", {userId, notebookId, currentZoneId, effortId}); try { const response = await fetch(`/chat_feature/user_feedback_eligibility?user_id=${userId}¬ebook_id=${notebookId}&zone_id=${currentZoneId}`); const data = await response.json(); if (data.eligible_for_feedback) { console.log("User is eligible for feedback:", data); if(data.has_feedback){ document.getElementById("feedbackLevel").value = data.rating ||""; document.getElementById("comment").value = data.comment ||""; document.getElementById("updateNotice").style.display = "block"; } else{ document.getElementById("feedbackLevel").value = data.rating ||""; document.getElementById("comment").value = data.comment ||""; document.getElementById("updateNotice").style.display = "none"; } // CHANGED/ADDED: Now modal will always be initialized and ready here if (bootstrapFeedbackModal) { bootstrapFeedbackModal.show(); } else { alert("Feedback modal not initialized!"); } } else { window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; } } catch (error) { console.error('Error checking eligibility:', error); window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; } }); // Feedback Submit document.getElementById("submitFeedback").addEventListener("click", async function () { const rating = document.getElementById("feedbackLevel").value; const comment = document.getElementById("comment").value; const payload = { user_id: userId, notebook_id: parseInt(notebookId), effort_id: parseInt(effortId), zone_id: parseInt(currentZoneId), rating: rating ? parseInt(rating) : null, comment: comment }; try { const response = await fetch("/chat_feature/submit_feedback", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (response.ok) { bootstrapFeedbackModal.hide(); window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; } else { alert("Error submitting feedback"); } } catch (error) { console.error("Error submitting feedback:", error); alert("Error submitting feedback"); } }); // Skip feedback document.getElementById("skipFeedback").addEventListener("click", function () { bootstrapFeedbackModal.hide(); window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; }); function loadFiles() { const files = preloadedFiles || []; fileList.innerHTML = ""; files.forEach(file => { const li = document.createElement("li"); li.className = "flex items-center gap-3 p-4 rounded-2xl border border-gray-200 bg-white hover:bg-blue-50 hover:shadow-lg transition shadow group"; const fileIcon = document.createElement("div"); fileIcon.className = "flex-shrink-0 text-blue-600 text-lg"; fileIcon.textContent = "πŸ“„"; const fileName = document.createElement("div"); fileName.className = "flex-1 min-w-0 cursor-pointer"; fileName.innerHTML = `

${file.name}

`; fileName.onclick = () => { currentFileUri = file.uri; appendMessage("System", `Selected file: ${file.name}`, "bg-purple-600"); document.getElementById("notebook-path").textContent = notebookName; document.getElementById("effort-path").textContent = effortName; document.getElementById("file-path").textContent = file.name; if (selectedFileElement) { selectedFileElement.classList.remove("border-blue-500", "bg-blue-100", "ring", "ring-blue-300", "shadow-lg", "transform", "scale-105"); } li.classList.add("border-blue-500", "bg-blue-100", "ring", "ring-blue-300", "shadow-lg", "transform", "scale-105"); selectedFileElement = li; }; const deleteButton = document.createElement("button"); deleteButton.className = "btn btn-sm btn-outline-danger"; deleteButton.setAttribute("data-bs-toggle", "modal"); deleteButton.setAttribute("data-bs-target", "#deleteFileModal"); deleteButton.innerHTML = ``; deleteButton.onclick = (e) => { e.stopPropagation(); setDeleteFile(file.uri, file.name); }; li.appendChild(fileIcon); li.appendChild(fileName); li.appendChild(deleteButton); fileList.appendChild(li); }); } async function sendMessage() { const message = document.getElementById("message").value; const persona = document.getElementById("persona").value; if (!message.trim() || !currentFileUri) { alert("Please select a file and type a message."); return; } if (!currentZoneId) { alert("Please select a zone before sending a message."); return; } appendMessage("You", message, "bg-blue-500", true); const loadingId = "loading-msg"; const loadingWrapper = document.createElement("div"); loadingWrapper.id = loadingId; loadingWrapper.className = "flex justify-start"; const loadingBubble = document.createElement("div"); loadingBubble.className = "max-w-xl px-4 py-2 rounded-lg bg-gray-300 text-gray-800"; loadingBubble.innerHTML = `Assistant: `; loadingWrapper.appendChild(loadingBubble); chatBox.appendChild(loadingWrapper); chatBox.scrollTop = chatBox.scrollHeight; const res = await fetch("/chat_feature/send_message", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message, persona, file_uri: currentFileUri, experiment_zone_id: currentZoneId, effortId: effortId, notebookId: notebookId }) }); const data = await res.json(); document.getElementById(loadingId)?.remove(); appendMessage("Assistant", data.message || "No response", "bg-gray-700"); document.getElementById("message").value = ""; } async function summarize() { const summaryType = document.getElementById("summary-type").value; if (!currentFileUri) { alert("Please select a file and summarize."); return; } if (!currentZoneId) { alert("Please select a zone before summarizing."); return; } appendMessage("You", `Summarize (${summaryType})`, "bg-green-500", true); const loadingId = "loading-msg"; const loadingWrapper = document.createElement("div"); loadingWrapper.id = loadingId; loadingWrapper.className = "flex justify-start"; const loadingBubble = document.createElement("div"); loadingBubble.className = "max-w-xl px-4 py-2 rounded-lg bg-gray-300 text-gray-800"; loadingBubble.innerHTML = `Assistant: `; loadingWrapper.appendChild(loadingBubble); chatBox.appendChild(loadingWrapper); chatBox.scrollTop = chatBox.scrollHeight; const res = await fetch("/chat_feature/summarize", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ summary_type: summaryType, file_uri: currentFileUri, experiment_zone_id: currentZoneId, effortId: effortId, notebookId: notebookId }) }); const data = await res.json(); document.getElementById(loadingId)?.remove(); appendMessage("Summary", data.message || "No summary", "bg-gray-700"); } function openModal() { modal.classList.remove("hidden"); } function closeModal() { modal.classList.add("hidden"); fileInput.value = ""; } function triggerFileInput() { fileInput.click(); } async function submitFile() { const files = fileInput.files; if (!files.length) return alert("Choose at least one file."); const formData = new FormData(); for (let file of files) { if (file.type !== "application/pdf") { alert(`Invalid file type: ${file.name}`); return; } formData.append("files", file); } formData.append("effort_id", effortId); try { const res = await fetch("/chat_feature/upload_file", { method: "POST", body: formData }); const contentType = res.headers.get("content-type") || ""; let responseData = contentType.includes("application/json") ? await res.json() : await res.text(); if (!res.ok) { const errorMessage = typeof responseData === "string" ? responseData : (responseData.error || JSON.stringify(responseData)); alert(`Upload failed: ${errorMessage}`); return; } alert("Upload successful!"); location.reload(); } catch (err) { alert("Error: " + err.message); } } window.onclick = function (event) { if (event.target == modal) closeModal(); } let deleteFileUriToConfirm = ""; let deleteFileName = "" function setDeleteFile(fileUri, fileName) { deleteFileUriToConfirm = fileUri; deleteFileName = fileName document.getElementById("deleteFileName").textContent = fileName; } async function confirmDeleteFile() { try { const res = await fetch("/chat_feature/delete_file", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_uri: deleteFileUriToConfirm, effort_id: effortId, filename: deleteFileName }) }); if (!res.ok) { const text = await res.text(); alert("Failed to delete: " + text); } else { const modal = bootstrap.Modal.getInstance(document.getElementById("deleteFileModal")); modal.hide(); alert("File deleted."); location.reload(); } } catch (err) { alert("Error deleting file: " + err.message); } } const dropZone = document.querySelector(".drop-zone"); dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("border-blue-500", "text-blue-500"); }); dropZone.addEventListener("dragleave", () => { dropZone.classList.remove("border-blue-500", "text-blue-500"); }); dropZone.addEventListener("drop", (e) => { e.preventDefault(); dropZone.classList.remove("border-blue-500", "text-blue-500"); const files = e.dataTransfer.files; fileInput.files = files; const list = document.getElementById("selectedFilesList"); list.innerHTML = ""; for (const file of files) { const p = document.createElement("p"); p.textContent = file.name; list.appendChild(p); } }); ======================================================================== backButton.addEventListener("click", async function(event){ console.log("Back button clicked"); event.preventDefault(); if(!currentZoneId){ console.log("no configuration selected, going back directly") window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; return; } console.log("Debug BackBtn:", {userId, notebookId, currentZoneId, effortId}); try { console.log("Checking user feedback eligibility..."); // CHANGED/ADDED: Fetch eligibility data from the server const response = await fetch(`/chat_feature/user_feedback_eligibility?user_id=${userId}¬ebook_id=${notebookId}&zone_id=${currentZoneId}`); const data = await response.json(); if (data.eligible_for_feedback) { console.log("User is eligible for feedback:", data); // show the modal popup //document.getElementById("feedbackModal").showModal = true; // CHANGED/ADDED: Initialize the modal here if(data.has_feedback){ document.getElementById("feedbackLevel").value = data.rating ||""; document.getElementById("comment").value = data.comment ||""; document.getElementById("updateNotice").style.display = "block"; } else{ document.getElementById("feedbackLevel").value = data.rating ||""; document.getElementById("comment").value = data.comment ||""; document.getElementById("updateNotice").style.display = "none"; } // CHANGED/ADDED: Now modal will always be initialized and ready here console.log(bootstrapFeedbackModal); if (bootstrapFeedbackModal) { console.log("Showing feedback modal"); bootstrapFeedbackModal.show(); } else { alert("Feedback modal not initialized!"); } } else { console.log("User is not eligible for feedback:", data); window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; } } catch (error) { console.error('Error checking eligibility:', error); window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; } }); ---> in this why my 'bootstrapFeedbackModal.show();' --------------------------- Feedback Modal Feature – Technical Design --- 1. Overview This feature enables contextual user feedback collection in the β€œDocument Chat & Summarization” UI. Feedback (rating/comment) is requested only if the user selects a configuration (zone), engages in chat or summarization, and then attempts to leave the page via the back button. --- 2. Technical Architecture Frontend Stack: JavaScript (Vanilla), Bootstrap 5 (modal) Session State: Tracks currentZoneId (active configuration) and hasChatted (true if user has sent a message or summarized) Eligibility Check: On back button click, modal is triggered only if both currentZoneId is set and hasChatted is true. API Integration: AJAX/fetch to /user_feedback_eligibility endpoint for final eligibility confirmation and feedback prefill. POST to /submit_feedback endpoint for feedback persistence. Backend Stack: Flask (UI Gateway), FastAPI (microservices), PostgreSQL Security: All feedback endpoints protected via Flask’s @login_required decorator. Eligibility Endpoint: /user_feedback_eligibility returns: eligible_for_feedback (True if chat exists for notebook/zone) has_feedback (True if prior feedback exists) rating, comment (prefill values if present) Submission Endpoint: /submit_feedback creates or updates feedback in DB. Data Model feedback ( id SERIAL PRIMARY KEY, user_id VARCHAR, notebook_id INT, effort_id INT, zone_id INT, rating INT, comment TEXT, created_at TIMESTAMP DEFAULT NOW(), UNIQUE(user_id, notebook_id, effort_id, zone_id) ) --- 3. Flow Sequence (with Tech Detail) 1. Zone selection: User selects a configuration from the dropdown; currentZoneId is set. 2. Chat/Summarize interaction: On any message or summary, hasChatted = true; in JS. 3. Back button event: On click, JS checks: If no zone or hasChatted == false, navigates away with no modal. If both present, calls /user_feedback_eligibility. 4. Eligibility check (backend): Flask endpoint verifies chat history via FastAPI; returns feedback eligibility and prefill values. 5. Modal Display: If eligible, Bootstrap modal is shown. Prefilled if feedback exists. 6. Feedback submission: On modal submit, JS POSTs feedback to /submit_feedback. FastAPI microservice creates or updates DB row. --- 4. API Reference Eligibility Request (GET): /user_feedback_eligibility?user_id=...¬ebook_id=...&zone_id=... Returns: { "eligible_for_feedback": true, "has_feedback": true, "rating": 4, "comment": "Nice" } Feedback Submission (POST): /submit_feedback Payload: { "user_id": "PI0001", "notebook_id": 2, "effort_id": 1, "zone_id": 36, "rating": 4, "comment": "Nice" } --- 5. Edge Cases & Error Handling Session loss: If the user refreshes, hasChatted resets. Modal will not pop up unless the user interacts again. API failure: Eligibility API failure: User is redirected back. Submission API failure: User sees an error alert. Feedback updates: Existing feedback is prefilled and updatable via the modal. Security: All endpoints require authentication (@login_required). --- 6. Pseudocode (Frontend Eligibility Logic) let hasChatted = false; function sendMessage() { ... hasChatted = true; } function summarize() { ... hasChatted = true; } backButton.addEventListener("click", async function(event) { if (!currentZoneId) { goBack(); return; } if (!hasChatted) { goBack(); return; } // Call backend for eligibility check and show modal if eligible }); --- 7. Sequence Diagram (Textual) User ↓ [Select Zone] --(sets currentZoneId)--> ↓ [Chat/Summarize] --(sets hasChatted)--> ↓ [Click Back] ↓ [JS: If both set, call API] ↓ [Flask: /user_feedback_eligibility] ↓ [FastAPI: Checks chat history] ↓ [Return eligible? Prefill values?] ↓ [If eligible: Show modal] ↓ [User submits feedback] ↓ [JS: POST to /submit_feedback] ↓ [FastAPI: Store/update feedback in DB] --- 8. Security & Robustness Notes State variables (hasChatted, currentZoneId) are session-only and do not persist on page reload. Server-side check guarantees modal is shown only after real chat/summarize event. No feedback collection for passive/idle sessions. --- Summary This design guarantees that feedback is solicited only after meaningful user interaction in the correct context, prevents duplicate feedback, and provides seamless update flows. All actions are secured, API-driven, and fail-safe against common errors. ======================= window.addEventListener('DOMContentLoaded', function () { loadFiles(); loadZones(); // Modal initialization after DOM is loaded const feedbackModalEl = document.getElementById("feedbackModal"); bootstrapFeedbackModal = new bootstrap.Modal(feedbackModalEl); let historyFilename = ""; if (chatHistory && chatHistory.length > 0) { chatHistory.forEach(entry => { if (entry.filename && (entry.filename !== historyFilename || historyFilename === "")) { historyFilename = entry.filename; appendMessage("System", `Selected file: ${historyFilename}`, "bg-purple-600"); } const userMessage = entry.question; appendMessage("You", userMessage, "bg-blue-500", true); const personaMessage = entry.persona; appendMessage("Persona", personaMessage, "bg-blue-500", true); const botMessage = entry.response; appendMessage("Assistant", botMessage, "bg-gray-700", false); }); } // ---- FEEDBACK BUTTON LOGIC ---- const feedbackBtn = document.getElementById("feedbackBtn"); if (feedbackBtn) { feedbackBtn.addEventListener("click", async function () { try { const response = await fetch(`/chat_feature/get_latest_feedback?user_id=${userId}¬ebook_id=${notebookId}`); const data = await response.json(); console.log("Feedback data:", data); if (data && data.exists) { const msg = `Your Previous Experience on Last Used Configuration:\nConfiguration: ${data.zone_name}\nRating: ${data.rating}\nComment: ${data.comment}`; alert(msg); } else { alert("No feedback exists for your previous configuration."); } } catch (err) { alert("Error fetching feedback."); console.error(err); } }); } // ---- AUTO POPUP ALERT LOGIC ---- fetch(`/chat_feature/get_latest_feedback?user_id=${userId}¬ebook_id=${notebookId}`) .then(response => response.json()) .then(data => { if (data && data.exists) { setTimeout(function () { const msg = `Your Previous Experience on Last Used Configuration:\nConfiguration: ${data.zone_name}\nRating: ${data.rating}\nComment: ${data.comment}`; alert(msg); }, 5000); // 5 seconds } }) .catch(err => { console.error("Error fetching previous feedback:", err); }); }); ==================================== window.addEventListener('DOMContentLoaded', function () { loadFiles(); loadZones(); // Modal initialization after DOM is loaded const feedbackModalEl = document.getElementById("feedbackModal"); bootstrapFeedbackModal = new bootstrap.Modal(feedbackModalEl); let historyFilename = ""; if (chatHistory && chatHistory.length > 0) { chatHistory.forEach(entry => { if (entry.filename && (entry.filename !== historyFilename || historyFilename === "")) { historyFilename = entry.filename; appendMessage("System", `Selected file: ${historyFilename}`, "bg-purple-600"); } const userMessage = entry.question; appendMessage("You", userMessage, "bg-blue-500", true); const personaMessage = entry.persona; appendMessage("Persona", personaMessage, "bg-blue-500", true); const botMessage = entry.response; appendMessage("Assistant", botMessage, "bg-gray-700", false); }); } // ---- FEEDBACK BUTTON LOGIC ---- const feedbackBtn = document.getElementById("feedbackBtn"); if (feedbackBtn) { feedbackBtn.addEventListener("click", async function () { try { //new change let zoneParam = ""; if (currentZoneId) { zoneParam = `&zone_id=${currentZoneId}`; } const userId = "{{ user_id }}"; const notebookId = "{{ notebook_id }}"; const response = await fetch(`/chat_feature/get_latest_feedback?user_id=${userId}¬ebook_id=${notebookId}${zoneParam}`); //const response = await fetch(`/chat_feature/get_latest_feedback?user_id=${userId}¬ebook_id=${notebookId}`); const data = await response.json(); console.log("Feedback data:", data); if (data && data.exists) { const msg = `Your Previous Experience on Last Used Configuration:\nConfiguration: ${data.zone_name}\nRating: ${data.rating}\nComment: ${data.comment}`; alert(msg); } else { alert("No feedback exists for your previous configuration."); } } catch (err) { alert("Error fetching feedback."); console.error(err); } }); } }); --> make me understand this in 3-4 lines