// 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