Spaces:
Sleeping
Sleeping
| 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 = '<div class="empty-state">Your step‑by‑step plan will appear here after you click <strong>Plan My Day</strong>.</div>'; | |
| 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); | |