|
|
|
|
|
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"); |
|
|
|
|
|
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 = `π <span class="truncate">${file.name}</span>`; |
|
|
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 = `<strong>${sender}:</strong><br><pre class="whitespace-pre-wrap break-words">${message}</pre>`; |
|
|
|
|
|
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); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
window.onload = function () { |
|
|
loadFiles(); |
|
|
loadZones(); |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
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"; |
|
|
} |
|
|
|
|
|
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}`; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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"); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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 = ` |
|
|
<p class="font-medium truncate group-hover:whitespace-normal group-hover:break-all" title="${file.name}"> |
|
|
${file.name} |
|
|
</p> |
|
|
`; |
|
|
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 = `<i class="bi bi-trash"></i>`; |
|
|
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 = `<strong>Assistant:</strong> <span class="loading-dots"><span></span><span></span><span></span></span>`; |
|
|
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 = `<strong>Assistant:</strong> <span class="loading-dots"><span></span><span></span><span></span></span>`; |
|
|
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..."); |
|
|
|
|
|
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"; |
|
|
} |
|
|
|
|
|
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; } |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
--- |
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
}) |
|
|
.catch(err => { |
|
|
console.error("Error fetching previous feedback:", err); |
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
==================================== |
|
|
|
|
|
window.addEventListener('DOMContentLoaded', function () { |
|
|
loadFiles(); |
|
|
loadZones(); |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const feedbackBtn = document.getElementById("feedbackBtn"); |
|
|
if (feedbackBtn) { |
|
|
feedbackBtn.addEventListener("click", async function () { |
|
|
try { |
|
|
|
|
|
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 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 |