PaperMate / frontend /js /feedback.js
ntphuc149's picture
feat: AI disclaimer modal, admin cost/status fixes, quota-aware submission detail
3050229
Raw
History Blame
6.87 kB
// Feedback modal β€” triggered by scroll-to-bottom after review loads.
// Step-by-step: each question appears after the previous is answered.
// Shares API_BASE and currentKey exposed by review.js on window.
document.addEventListener("DOMContentLoaded", function () {
const TOTAL_STEPS = 4; // step 5 is success screen
const SHOWN_KEY = "pm_feedback_shown";
let currentStep = 1;
const answers = {};
const overlay = document.getElementById("feedback-modal");
const closeBtn = document.getElementById("fb-modal-close");
const progressBar = document.getElementById("fb-progress-bar");
const stepNumEl = document.getElementById("fb-step-num");
const fbComments = document.getElementById("fb-comments");
const fbCharNum = document.getElementById("fb-char-num");
const fbError = document.getElementById("fb-error");
const skipBtn = document.getElementById("fb-skip-btn");
const submitBtn = document.getElementById("fb-submit-btn");
// ── Open / Close ───────────────────────────────────────────────────────────
function openModal() {
overlay.classList.add("open");
showStep(1);
}
function closeModal() {
overlay.classList.remove("open");
}
closeBtn.addEventListener("click", closeModal);
overlay.addEventListener("click", (e) => {
if (e.target === overlay) closeModal();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeModal();
});
// ── Progress bar ───────────────────────────────────────────────────────────
function updateProgress(step) {
const pct = ((step - 1) / TOTAL_STEPS) * 100;
progressBar.style.width = pct + "%";
stepNumEl.textContent = Math.min(step, TOTAL_STEPS);
}
// ── Step navigation ────────────────────────────────────────────────────────
const modalBox = overlay.querySelector(".fb-modal-box");
function showStep(n) {
const steps = document.querySelectorAll(".fb-step");
steps.forEach((s) => s.classList.remove("active"));
const next = document.querySelector(`.fb-step[data-step="${n}"]`);
if (!next) return;
next.classList.add("active");
currentStep = n;
updateProgress(n);
}
function advanceStep() {
showStep(currentStep + 1);
}
// ── Chip selection (steps 1-3) ─────────────────────────────────────────────
document.querySelectorAll(".fb-chip").forEach((chip) => {
chip.addEventListener("click", () => {
const name = chip.dataset.name;
const value = chip.dataset.value;
// Highlight selected chip in group
const group = chip.closest(".fb-chip-group");
group.querySelectorAll(".fb-chip").forEach((c) => c.classList.remove("selected"));
chip.classList.add("selected");
answers[name] = value;
// Auto-advance after short delay so user sees the selection
setTimeout(() => advanceStep(), 340);
});
});
// ── Step 4: comment ────────────────────────────────────────────────────────
if (fbComments) {
fbComments.addEventListener("input", () => {
fbCharNum.textContent = fbComments.value.length;
});
}
skipBtn?.addEventListener("click", () => submitFeedback(""));
submitBtn?.addEventListener("click", () => submitFeedback(fbComments?.value.trim() || ""));
// ── Submit ─────────────────────────────────────────────────────────────────
async function submitFeedback(comment) {
fbError.style.display = "none";
const key = window.currentKey;
if (!key) {
fbError.textContent = "Cannot identify the review. Please reload and try again.";
fbError.style.display = "block";
return;
}
submitBtn.disabled = true;
skipBtn.disabled = true;
submitBtn.innerHTML = '<i class="ri-loader-4-line spin"></i> Submitting…';
try {
const resp = await fetch(`${window.API_BASE}/api/feedback/${encodeURIComponent(key)}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
helpfulness: answers.helpfulness || null,
has_critical_error: answers.has_critical_error === "true",
has_suggestions: answers.has_suggestions === "true",
comments: comment,
}),
});
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
fbError.textContent = data.detail || "Failed to submit. Please try again.";
fbError.style.display = "block";
submitBtn.disabled = false;
skipBtn.disabled = false;
submitBtn.innerHTML = '<i class="ri-send-plane-line"></i> Submit';
return;
}
// Success step
progressBar.style.width = "100%";
showStep(5);
setTimeout(() => closeModal(), 2800);
} catch {
fbError.textContent = "Network error. Make sure the server is running.";
fbError.style.display = "block";
submitBtn.disabled = false;
skipBtn.disabled = false;
submitBtn.innerHTML = '<i class="ri-send-plane-line"></i> Submit';
}
}
// ── Scroll trigger ─────────────────────────────────────────────────────────
// Fire once when user reaches bottom, but only after review is rendered.
// review.js sets reviewContent.style.display = "block" when done.
let triggered = false;
let reviewReady = false; // set to true by review.js after renderReview()
function checkScrollTrigger() {
if (triggered || !reviewReady) return;
const scrolled = window.scrollY + window.innerHeight;
const docHeight = document.documentElement.scrollHeight;
if (scrolled >= docHeight - 200) {
triggered = true;
setTimeout(openModal, 2000);
}
}
window.addEventListener("scroll", checkScrollTrigger, { passive: true });
// Called by review.js after renderReview() finishes β€” arms the trigger
// and immediately checks in case user is already near the bottom
window.onReviewReady = () => {
reviewReady = true;
triggered = false;
// Short delay to let DOM settle after render
setTimeout(checkScrollTrigger, 300);
};
});