Replace lettered tabs with swap group navigation for solver results
This commit is contained in:
@@ -73,6 +73,9 @@
|
|||||||
.room-locked { --wa-color-surface-border: var(--wa-color-brand-50); }
|
.room-locked { --wa-color-surface-border: var(--wa-color-brand-50); }
|
||||||
.room-label { font-weight: bold; font-size: 0.8rem; margin-bottom: 0.2rem; }
|
.room-label { font-weight: bold; font-size: 0.8rem; margin-bottom: 0.2rem; }
|
||||||
.solver-score { font-size: 0.8rem; margin-top: 0.3rem; color: var(--wa-color-neutral-500); }
|
.solver-score { font-size: 0.8rem; margin-top: 0.3rem; color: var(--wa-color-neutral-500); }
|
||||||
|
.swap-group { border-left: 2px solid var(--wa-color-brand-50); padding-left: 0.5rem; margin-bottom: 0.5rem; }
|
||||||
|
.swap-group-nav { display: flex; align-items: center; gap: 0.3rem; margin-bottom: 0.3rem; }
|
||||||
|
.swap-group-label { font-size: 0.8rem; color: var(--wa-color-neutral-500); min-width: 3rem; text-align: center; }
|
||||||
.divider { border: none; border-top: 1px solid #909090; margin: 0.75rem 0; }
|
.divider { border: none; border-top: 1px solid #909090; margin: 0.75rem 0; }
|
||||||
.pref-rows { display: grid; grid-template-columns: auto 1fr; }
|
.pref-rows { display: grid; grid-template-columns: auto 1fr; }
|
||||||
.pref-row { display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; align-items: center; gap: 0.5rem; padding: 0.3rem 0.4rem; }
|
.pref-row { display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; align-items: center; gap: 0.5rem; padding: 0.3rem 0.4rem; }
|
||||||
|
|||||||
159
static/trip.js
159
static/trip.js
@@ -423,23 +423,20 @@ document.getElementById('solve-btn').addEventListener('click', async () => {
|
|||||||
const container = document.getElementById('solver-results');
|
const container = document.getElementById('solver-results');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
const renderSolution = (sol, parent, lockedRooms) => {
|
const renderRoomCard = (room, parent, roomNum, locked) => {
|
||||||
for (let i = 0; i < sol.rooms.length; i++) {
|
|
||||||
const key = sol.rooms[i].map(m => m.id).sort((a, b) => a - b).join(',');
|
|
||||||
const card = document.createElement('wa-card');
|
const card = document.createElement('wa-card');
|
||||||
const locked = lockedRooms.has(key);
|
|
||||||
card.className = 'room-card' + (locked ? ' room-locked' : '');
|
card.className = 'room-card' + (locked ? ' room-locked' : '');
|
||||||
if (locked) card.setAttribute('appearance', 'outlined');
|
if (locked) card.setAttribute('appearance', 'outlined');
|
||||||
const label = document.createElement('div');
|
const label = document.createElement('div');
|
||||||
label.className = 'room-label';
|
label.className = 'room-label';
|
||||||
label.textContent = 'Room ' + (i + 1);
|
label.textContent = 'Room ' + roomNum;
|
||||||
card.appendChild(label);
|
card.appendChild(label);
|
||||||
const tags = document.createElement('div');
|
const tags = document.createElement('div');
|
||||||
tags.className = 'tags';
|
tags.className = 'tags';
|
||||||
const roomIDs = sol.rooms[i].map(m => m.id);
|
const roomIDs = room.map(m => m.id);
|
||||||
const violations = [];
|
const violations = [];
|
||||||
for (const a of sol.rooms[i]) {
|
for (const a of room) {
|
||||||
for (const b of sol.rooms[i]) {
|
for (const b of room) {
|
||||||
if (a.id === b.id) continue;
|
if (a.id === b.id) continue;
|
||||||
const eff = lastOveralls[a.id]?.[b.id];
|
const eff = lastOveralls[a.id]?.[b.id];
|
||||||
if (eff && eff.kind === 'prefer_not') {
|
if (eff && eff.kind === 'prefer_not') {
|
||||||
@@ -447,7 +444,7 @@ document.getElementById('solve-btn').addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const member of sol.rooms[i]) {
|
for (const member of room) {
|
||||||
const tag = document.createElement('wa-tag');
|
const tag = document.createElement('wa-tag');
|
||||||
tag.size = 'small';
|
tag.size = 'small';
|
||||||
tag.style.cursor = 'pointer';
|
tag.style.cursor = 'pointer';
|
||||||
@@ -478,42 +475,138 @@ document.getElementById('solve-btn').addEventListener('click', async () => {
|
|||||||
card.appendChild(tags);
|
card.appendChild(tags);
|
||||||
}
|
}
|
||||||
parent.appendChild(card);
|
parent.appendChild(card);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const solutions = result.solutions;
|
const solutions = result.solutions;
|
||||||
let lockedRooms = new Set();
|
|
||||||
if (solutions.length > 1) {
|
|
||||||
const roomKey = (room) => room.map(m => m.id).sort((a, b) => a - b).join(',');
|
const roomKey = (room) => room.map(m => m.id).sort((a, b) => a - b).join(',');
|
||||||
const sets = solutions.map(sol => new Set(sol.rooms.map(roomKey)));
|
let swapGroups = [];
|
||||||
lockedRooms = new Set([...sets[0]].filter(k => sets.every(s => s.has(k))));
|
|
||||||
}
|
|
||||||
if (solutions.length === 1) {
|
if (solutions.length === 1) {
|
||||||
renderSolution(solutions[0], container, lockedRooms);
|
let roomNum = 1;
|
||||||
|
for (const room of solutions[0].rooms) {
|
||||||
|
renderRoomCard(room, container, roomNum++, false);
|
||||||
|
}
|
||||||
} else if (solutions.length > 1) {
|
} else if (solutions.length > 1) {
|
||||||
const optionLabel = (i) => {
|
const sets = solutions.map(sol => new Set(sol.rooms.map(roomKey)));
|
||||||
const a = 'A'.charCodeAt(0);
|
const lockedKeys = new Set([...sets[0]].filter(k => sets.every(s => s.has(k))));
|
||||||
return i < 26 ? String.fromCharCode(a + i) : String.fromCharCode(a + Math.floor(i / 26) - 1) + String.fromCharCode(a + (i % 26));
|
|
||||||
|
const lockedRoomsList = solutions[0].rooms.filter(r => lockedKeys.has(roomKey(r)));
|
||||||
|
|
||||||
|
const uf = {};
|
||||||
|
const ufFind = (x) => {
|
||||||
|
if (uf[x] === undefined) uf[x] = x;
|
||||||
|
if (uf[x] !== x) uf[x] = ufFind(uf[x]);
|
||||||
|
return uf[x];
|
||||||
};
|
};
|
||||||
const tabGroup = document.createElement('wa-tab-group');
|
const ufUnion = (a, b) => {
|
||||||
for (let si = 0; si < solutions.length; si++) {
|
const ra = ufFind(a), rb = ufFind(b);
|
||||||
const tab = document.createElement('wa-tab');
|
if (ra !== rb) uf[ra] = rb;
|
||||||
tab.slot = 'nav';
|
};
|
||||||
tab.panel = 'sol-' + si;
|
|
||||||
tab.textContent = optionLabel(si);
|
for (const sol of solutions) {
|
||||||
tabGroup.appendChild(tab);
|
for (const room of sol.rooms) {
|
||||||
|
if (lockedKeys.has(roomKey(room))) continue;
|
||||||
|
const ids = room.map(m => m.id);
|
||||||
|
for (let i = 1; i < ids.length; i++) {
|
||||||
|
ufUnion(ids[0], ids[i]);
|
||||||
}
|
}
|
||||||
for (let si = 0; si < solutions.length; si++) {
|
|
||||||
const panel = document.createElement('wa-tab-panel');
|
|
||||||
panel.name = 'sol-' + si;
|
|
||||||
renderSolution(solutions[si], panel, lockedRooms);
|
|
||||||
tabGroup.appendChild(panel);
|
|
||||||
}
|
}
|
||||||
container.appendChild(tabGroup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const components = {};
|
||||||
|
for (const id of Object.keys(uf)) {
|
||||||
|
const root = ufFind(parseInt(id));
|
||||||
|
if (!components[root]) components[root] = new Set();
|
||||||
|
components[root].add(parseInt(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const studentIDs of Object.values(components)) {
|
||||||
|
const configs = [];
|
||||||
|
const configKeySet = new Set();
|
||||||
|
for (const sol of solutions) {
|
||||||
|
const groupRooms = sol.rooms.filter(r => r.some(m => studentIDs.has(m.id)));
|
||||||
|
groupRooms.sort((a, b) => roomKey(a).localeCompare(roomKey(b)));
|
||||||
|
const ck = groupRooms.map(r => roomKey(r)).join('|');
|
||||||
|
if (!configKeySet.has(ck)) {
|
||||||
|
configKeySet.add(ck);
|
||||||
|
configs.push(groupRooms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
swapGroups.push({ studentIDs, configs });
|
||||||
|
}
|
||||||
|
swapGroups.sort((a, b) => Math.min(...a.studentIDs) - Math.min(...b.studentIDs));
|
||||||
|
|
||||||
|
let roomNum = 1;
|
||||||
|
for (const room of lockedRoomsList) {
|
||||||
|
renderRoomCard(room, container, roomNum++, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const group of swapGroups) {
|
||||||
|
const section = document.createElement('div');
|
||||||
|
section.className = 'swap-group';
|
||||||
|
|
||||||
|
let current = 0;
|
||||||
|
const roomsDiv = document.createElement('div');
|
||||||
|
const baseRoomNum = roomNum;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
roomsDiv.innerHTML = '';
|
||||||
|
let rn = baseRoomNum;
|
||||||
|
for (const room of group.configs[current]) {
|
||||||
|
renderRoomCard(room, roomsDiv, rn++, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nav = document.createElement('div');
|
||||||
|
nav.className = 'swap-group-nav';
|
||||||
|
const prevBtn = document.createElement('wa-button');
|
||||||
|
prevBtn.size = 'small';
|
||||||
|
prevBtn.variant = 'text';
|
||||||
|
prevBtn.textContent = '\u25c0';
|
||||||
|
const lbl = document.createElement('span');
|
||||||
|
lbl.className = 'swap-group-label';
|
||||||
|
lbl.textContent = '1 of ' + group.configs.length;
|
||||||
|
const nextBtn = document.createElement('wa-button');
|
||||||
|
nextBtn.size = 'small';
|
||||||
|
nextBtn.variant = 'text';
|
||||||
|
nextBtn.textContent = '\u25b6';
|
||||||
|
|
||||||
|
prevBtn.addEventListener('click', () => {
|
||||||
|
current = (current - 1 + group.configs.length) % group.configs.length;
|
||||||
|
lbl.textContent = (current + 1) + ' of ' + group.configs.length;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
nextBtn.addEventListener('click', () => {
|
||||||
|
current = (current + 1) % group.configs.length;
|
||||||
|
lbl.textContent = (current + 1) + ' of ' + group.configs.length;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
nav.appendChild(prevBtn);
|
||||||
|
nav.appendChild(lbl);
|
||||||
|
nav.appendChild(nextBtn);
|
||||||
|
section.appendChild(nav);
|
||||||
|
section.appendChild(roomsDiv);
|
||||||
|
container.appendChild(section);
|
||||||
|
|
||||||
|
render();
|
||||||
|
roomNum += group.configs[0].length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const scoreDiv = document.createElement('div');
|
const scoreDiv = document.createElement('div');
|
||||||
scoreDiv.className = 'solver-score';
|
scoreDiv.className = 'solver-score';
|
||||||
scoreDiv.textContent = 'Score: ' + (solutions[0]?.score ?? 0) + (solutions.length > 1 ? ' (' + solutions.length + ' options)' : '');
|
let scoreText = 'Score: ' + (solutions[0]?.score ?? 0);
|
||||||
|
if (swapGroups.length > 0) {
|
||||||
|
const counts = swapGroups.map(g => g.configs.length);
|
||||||
|
const total = counts.reduce((a, b) => a * b, 1);
|
||||||
|
if (swapGroups.length === 1) {
|
||||||
|
scoreText += ' (' + total + ' options)';
|
||||||
|
} else {
|
||||||
|
scoreText += ' (' + counts.join(' \u00d7 ') + ' = ' + total + ' combinations)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scoreDiv.textContent = scoreText;
|
||||||
container.appendChild(scoreDiv);
|
container.appendChild(scoreDiv);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const container = document.getElementById('solver-results');
|
const container = document.getElementById('solver-results');
|
||||||
|
|||||||
Reference in New Issue
Block a user