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);