Spaces:
Running
Running
| // 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); | |
| }; | |
| }); | |