const ATTRACTIONS = [ "Family River Cruise", "Sky Carousel", "Falcon Coaster", "Desert Drop", "Oasis Rapids", "Wave Lagoon", "Qiddiya Dining Court", "Sunset Plaza Show" ]; const NODE_POSITIONS = { "HUB": { x: 150, y: 110 }, "ZONE_FAMILY_N": { x: 70, y: 55 }, "ZONE_FAMILY_S": { x: 70, y: 165 }, "ZONE_THRILL": { x: 230, y: 65 }, "ZONE_WATER": { x: 235, y: 165 }, "ZONE_DINING": { x: 150, y: 195 } }; const mustDoSelect = document.getElementById("must-do-select"); const mustDoChips = document.getElementById("must-do-chips"); const intensityRange = document.getElementById("intensity-range"); const walkingRange = document.getElementById("walking-range"); const intensityLabel = document.getElementById("intensity-label"); const walkingLabel = document.getElementById("walking-label"); const planBtn = document.getElementById("plan-btn"); const planSpinner = document.getElementById("plan-spinner"); const errorBanner = document.getElementById("error-banner"); const metricWait = document.getElementById("metric-wait"); const metricWalk = document.getElementById("metric-walk"); const metricEnjoy = document.getElementById("metric-enjoyment"); const barWait = document.getElementById("bar-wait"); const barWalk = document.getElementById("bar-walk"); const barEnjoy = document.getElementById("bar-enjoy"); const timeline = document.getElementById("timeline"); // Populate multi-select if not already in HTML (e.g. when JS runs first) if (mustDoSelect && mustDoSelect.options.length <= 1) { ATTRACTIONS.forEach(function(name) { const opt = document.createElement("option"); opt.value = name; opt.textContent = name; mustDoSelect.appendChild(opt); }); } const selectedMustDo = new Set(); mustDoSelect.addEventListener("change", () => { const value = mustDoSelect.value; if (value && !selectedMustDo.has(value)) { selectedMustDo.add(value); renderChips(); } mustDoSelect.value = ""; }); function renderChips() { mustDoChips.innerHTML = ""; if (selectedMustDo.size === 0) { return; } Array.from(selectedMustDo).forEach(name => { const chip = document.createElement("div"); chip.className = "chip"; chip.textContent = name; const btn = document.createElement("button"); btn.type = "button"; btn.textContent = "×"; btn.onclick = () => { selectedMustDo.delete(name); renderChips(); }; chip.appendChild(btn); mustDoChips.appendChild(chip); }); } intensityRange.addEventListener("input", () => { intensityLabel.textContent = intensityRange.value; }); walkingRange.addEventListener("input", () => { walkingLabel.textContent = walkingRange.value; }); function setLoading(isLoading) { planBtn.disabled = isLoading; planSpinner.style.display = isLoading ? "inline-block" : "none"; } function clearResults() { metricWait.textContent = "–"; metricWalk.textContent = "–"; metricEnjoy.textContent = "–"; barWait.style.width = "0%"; barWalk.style.width = "0%"; barEnjoy.style.width = "0%"; timeline.innerHTML = '
Your step‑by‑step plan will appear here after you click Plan My Day.
'; const layer = document.getElementById("route-layer"); if (layer) layer.innerHTML = ""; const wrap = document.getElementById("system-response-wrap"); if (wrap) wrap.style.display = "none"; } function setError(message) { if (message) { errorBanner.textContent = message; errorBanner.style.display = "block"; } else { errorBanner.style.display = "none"; } } function renderMetrics(plan) { if (!plan) return; metricWait.textContent = plan.total_wait_minutes + " min"; metricWalk.textContent = plan.total_walking_m + " m"; metricEnjoy.textContent = plan.enjoyment_score.toFixed(1) + " / 10"; const waitPct = Math.min(100, (plan.total_wait_minutes / 240) * 100); const walkPct = Math.min(100, (plan.total_walking_m / 6000) * 100); const enjoyPct = Math.min(100, (plan.enjoyment_score / 10) * 100); barWait.style.width = waitPct + "%"; barWalk.style.width = walkPct + "%"; barEnjoy.style.width = enjoyPct + "%"; } function renderTimeline(stops) { timeline.innerHTML = ""; if (!stops || stops.length === 0) { const empty = document.createElement("div"); empty.className = "empty-state"; empty.textContent = "No stops were generated for this window. Try expanding your time range or relaxing constraints."; timeline.appendChild(empty); return; } const hasSuggested = stops.some(function(s) { return s.is_suggested; }); if (hasSuggested) { const note = document.createElement("div"); note.className = "suggested-note"; note.textContent = "Attractions marked \u201cSuggestion\u201d were added by the system to improve your day (e.g. to boost enjoyment). You only requested the others."; timeline.appendChild(note); } stops.forEach((stop, idx) => { const card = document.createElement("div"); card.className = "stop-card"; const step = document.createElement("div"); step.className = "stop-step"; const circle = document.createElement("div"); circle.className = "stop-step-circle"; circle.textContent = idx + 1; step.appendChild(circle); if (idx < stops.length - 1) { const line = document.createElement("div"); line.className = "stop-step-line"; step.appendChild(line); } const main = document.createElement("div"); main.className = "stop-main"; const title = document.createElement("div"); title.className = "stop-title"; title.appendChild(document.createTextNode(stop.attraction_name)); if (stop.is_suggested) { const badge = document.createElement("span"); badge.className = "suggested-badge"; badge.textContent = "Suggestion"; title.appendChild(badge); } main.appendChild(title); const time = document.createElement("div"); time.className = "stop-time"; time.textContent = stop.start_time + " – " + stop.end_time; main.appendChild(time); const meta = document.createElement("div"); meta.className = "stop-meta"; const waitPill = document.createElement("span"); waitPill.className = "pill"; waitPill.textContent = "Wait ~ " + stop.estimated_wait_minutes + " min"; const walkPill = document.createElement("span"); walkPill.className = "pill"; walkPill.textContent = "Walk ~ " + stop.walking_distance_m + " m"; meta.appendChild(waitPill); meta.appendChild(walkPill); main.appendChild(meta); card.appendChild(step); card.appendChild(main); timeline.appendChild(card); }); } function renderRoute(stops) { const layer = document.getElementById("route-layer"); if (!layer) return; layer.innerHTML = ""; if (!stops || stops.length === 0) return; const points = [{ x: 150, y: 110 }]; stops.forEach(s => { const n = NODE_POSITIONS[s.node_id]; if (n) points.push(n); }); if (points.length >= 2) { const d = points.map((p, i) => (i ? "L" : "M") + " " + p.x + " " + p.y).join(" "); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", d); path.setAttribute("fill", "none"); path.setAttribute("stroke", "#66a0ff"); path.setAttribute("stroke-width", "3"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-linejoin", "round"); path.setAttribute("stroke-dasharray", "5 6"); layer.appendChild(path); } stops.forEach((stop, idx) => { const n = NODE_POSITIONS[stop.node_id]; if (!n) return; const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); const c = document.createElementNS("http://www.w3.org/2000/svg", "circle"); c.setAttribute("cx", n.x); c.setAttribute("cy", n.y); c.setAttribute("r", "8"); c.setAttribute("fill", "#ffb347"); c.setAttribute("stroke", "#1b1630"); c.setAttribute("stroke-width", "2"); const t = document.createElementNS("http://www.w3.org/2000/svg", "text"); t.setAttribute("x", n.x); t.setAttribute("y", n.y - 11); t.setAttribute("fill", "#fff5e6"); t.setAttribute("font-size", "9"); t.setAttribute("text-anchor", "middle"); t.setAttribute("font-weight", "600"); t.textContent = idx + 1; g.appendChild(c); g.appendChild(t); layer.appendChild(g); }); } async function handlePlanClick() { setError(""); clearResults(); setLoading(true); try { const body = { visit_date: document.getElementById("visit-date").value.trim(), start_time: document.getElementById("start-time").value.trim(), end_time: document.getElementById("end-time").value.trim(), must_do_attractions: Array.from(selectedMustDo), intensity_preference: parseInt(intensityRange.value, 10), walking_tolerance: parseInt(walkingRange.value, 10), }; const resp = await fetch("/api/v1/plan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!resp.ok) { const data = await resp.json().catch(() => ({})); throw new Error(data.detail || "Planning failed"); } const data = await resp.json(); const plan = data.plan || data; const system = data.system || null; renderMetrics(plan); renderTimeline(plan.stops || []); renderRoute(plan.stops || []); const wrap = document.getElementById("system-response-wrap"); const bodyEl = document.getElementById("system-response-body"); const jsonEl = document.getElementById("system-response-json"); const toggle = document.getElementById("system-response-toggle"); if (wrap && jsonEl) { wrap.style.display = "block"; jsonEl.textContent = JSON.stringify(system || { plan: plan }, null, 2); if (bodyEl) bodyEl.classList.remove("open"); if (toggle) { toggle.onclick = function() { if (bodyEl) bodyEl.classList.toggle("open"); toggle.setAttribute("aria-expanded", bodyEl && bodyEl.classList.contains("open")); }; } } } catch (err) { setError(err.message || "Something went wrong while planning your day."); } finally { setLoading(false); } } planBtn.addEventListener("click", handlePlanClick);