blackopsrepl's picture
.
e2dcb4d verified
// VM Placement Optimizer - Frontend Application with Real-Time Animations
let autoRefreshIntervalId = null;
let demoDataId = null;
let placementId = null;
let loadedPlacement = null;
// Animation state tracking
let vmPositionCache = {}; // { vmId: serverId | null }
let isFirstRender = true;
let previousScore = null;
let serverLookup = {}; // For quick server access during animations
$(document).ready(function () {
let initialized = false;
function safeInitialize() {
if (!initialized) {
initialized = true;
initializeApp();
}
}
$(window).on('load', safeInitialize);
setTimeout(safeInitialize, 100);
});
function initializeApp() {
replaceQuickstartSolverForgeAutoHeaderFooter();
$("#solveButton").click(function () {
solve();
});
$("#stopSolvingButton").click(function () {
stopSolving();
});
$("#analyzeButton").click(function () {
analyze();
});
// View toggle
$('input[name="viewToggle"]').change(function() {
if ($('#rackView').is(':checked')) {
$('#rackViewContainer').show();
$('#cardViewContainer').hide();
} else {
$('#rackViewContainer').hide();
$('#cardViewContainer').show();
}
});
setupAjax();
fetchDemoData();
}
function setupAjax() {
$.ajaxSetup({
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json,text/plain',
}
});
jQuery.each(["put", "delete"], function (i, method) {
jQuery[method] = function (url, data, callback, type) {
if (jQuery.isFunction(data)) {
type = type || callback;
callback = data;
data = undefined;
}
return jQuery.ajax({
url: url,
type: method,
dataType: type,
data: data,
success: callback
});
};
});
}
function fetchDemoData() {
$.get("/demo-data", function (data) {
data.forEach(item => {
$("#testDataButton").append($('<a id="' + item + 'TestData" class="dropdown-item" href="#">' + item + '</a>'));
$("#" + item + "TestData").click(function () {
switchDataDropDownItemActive(item);
placementId = null;
demoDataId = item;
// Reset animation state for new dataset
isFirstRender = true;
vmPositionCache = {};
previousScore = null;
refreshPlacement();
});
});
if (data.length > 0) {
demoDataId = data[0];
switchDataDropDownItemActive(demoDataId);
refreshPlacement();
}
}).fail(function (xhr, ajaxOptions, thrownError) {
let $demo = $("#demo");
$demo.empty();
$demo.html("<h1><p align=\"center\">No test data available</p></h1>");
});
}
function switchDataDropDownItemActive(newItem) {
const activeCssClass = "active";
$("#testDataButton > a." + activeCssClass).removeClass(activeCssClass);
$("#" + newItem + "TestData").addClass(activeCssClass);
}
function refreshPlacement() {
let path = "/placements/" + placementId;
if (placementId === null) {
if (demoDataId === null) {
showSimpleError("Please select a test data set.");
return;
}
path = "/demo-data/" + demoDataId;
}
$.getJSON(path, function (placement) {
loadedPlacement = placement;
renderPlacement(placement);
}).fail(function (xhr, ajaxOptions, thrownError) {
showError("Getting the placement has failed.", xhr);
refreshSolvingButtons(false);
});
}
function renderPlacement(placement) {
if (!placement) {
console.error('No placement data provided');
return;
}
const isSolving = placement.solverStatus != null && placement.solverStatus !== "NOT_SOLVING";
refreshSolvingButtons(isSolving);
// Update score with animation
const newScore = placement.score == null ? "?" : placement.score;
const scoreEl = $("#score");
if (previousScore !== newScore) {
scoreEl.text("Score: " + newScore);
if (previousScore !== null && newScore !== "?") {
scoreEl.addClass("updated");
setTimeout(() => scoreEl.removeClass("updated"), 300);
}
previousScore = newScore;
}
// Update summary cards with change detection
updateSummaryCard("#totalServers", placement.servers ? placement.servers.length : 0);
updateSummaryCard("#activeServers", placement.activeServers || 0);
updateSummaryCard("#totalVms", placement.vms ? placement.vms.length : 0);
updateSummaryCard("#unassignedVms", placement.unassignedVms || 0);
updateSummaryCard("#cpuUtil", Math.round((placement.totalCpuUtilization || 0) * 100) + "%");
updateSummaryCard("#memUtil", Math.round((placement.totalMemoryUtilization || 0) * 100) + "%");
// Update unassigned card styling
const unassignedCard = $("#unassignedCard");
const unassignedVms = placement.unassignedVms || 0;
unassignedCard.removeClass("warning danger");
if (unassignedVms > 0) {
unassignedCard.addClass(unassignedVms > 5 ? "danger" : "warning");
}
if (!placement.servers || !placement.vms) {
return;
}
// Create VM and server lookups
const vmById = {};
placement.vms.forEach(vm => vmById[vm.id] = vm);
serverLookup = {};
placement.servers.forEach(s => serverLookup[s.id] = s);
if (isFirstRender) {
// Full render on first load
renderRackView(placement, vmById);
renderCardView(placement, vmById);
renderUnassignedVMs(placement.vms);
vmPositionCache = buildPositionCache(placement);
isFirstRender = false;
return;
}
// Incremental animated update
const newPositions = buildPositionCache(placement);
const changes = diffPositions(vmPositionCache, newPositions);
if (changes.length > 0) {
console.log(`Detected ${changes.length} VM position changes:`, changes.slice(0, 5));
animateVmChanges(changes, placement, vmById);
}
// Always update utilization bars smoothly
updateUtilizationBars(placement);
// Update unassigned list
updateUnassignedList(placement.vms, vmById);
vmPositionCache = newPositions;
}
function updateSummaryCard(selector, newValue) {
const el = $(selector);
const oldValue = el.text();
if (oldValue !== String(newValue)) {
el.text(newValue);
el.addClass("changed");
setTimeout(() => el.removeClass("changed"), 300);
}
}
function buildPositionCache(placement) {
const cache = {};
placement.vms.forEach(vm => {
cache[vm.id] = vm.server || null;
});
// Debug: log sample of cache
const sample = Object.entries(cache).slice(0, 3);
console.log('Position cache sample:', sample);
return cache;
}
function diffPositions(oldCache, newCache) {
const changes = [];
for (const vmId in newCache) {
const oldServer = oldCache[vmId];
const newServer = newCache[vmId];
if (oldServer !== newServer) {
changes.push({ vmId, from: oldServer, to: newServer });
}
}
return changes;
}
function animateVmChanges(changes, placement, vmById) {
changes.forEach(({ vmId, from, to }) => {
const vm = vmById[vmId];
if (!vm) return;
// Get source and target elements
let sourceEl, targetEl;
let sourceChipEl = null; // The actual chip element to hide during animation
if (from) {
sourceEl = $(`#server-blade-${from} .vm-chips`);
sourceChipEl = $(`#server-blade-${from} #vm-chip-${vmId}`);
// Highlight sending server
$(`#server-blade-${from}`).addClass('sending');
setTimeout(() => $(`#server-blade-${from}`).removeClass('sending'), 200);
} else {
// VM is coming from unassigned list
sourceEl = $(`#unassigned-${vmId}`);
sourceChipEl = sourceEl;
}
if (to) {
targetEl = $(`#server-blade-${to} .vm-chips`);
// Highlight receiving server
$(`#server-blade-${to}`).addClass('receiving');
setTimeout(() => $(`#server-blade-${to}`).removeClass('receiving'), 300);
} else {
targetEl = $('#unassignedList');
}
// If elements not found, just update DOM directly
if (!sourceEl.length || !targetEl.length) {
console.log(`Animation fallback: sourceEl=${sourceEl.length}, targetEl=${targetEl.length} for VM ${vmId}`);
updateVmPosition(vmId, from, to, vm);
return;
}
// Create flying clone
const flyingChip = createVmChip(vm);
flyingChip.addClass('flying-vm');
// Get positions
const sourceRect = sourceEl[0].getBoundingClientRect();
const targetRect = targetEl[0].getBoundingClientRect();
// Position at source
flyingChip.css({
left: sourceRect.left + 'px',
top: sourceRect.top + 'px',
transform: 'translate(0, 0)'
});
$('body').append(flyingChip);
// Hide original element during animation
if (sourceChipEl && sourceChipEl.length) {
sourceChipEl.css('opacity', '0');
}
// Trigger fly animation (use double RAF for reliable animation start)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
flyingChip.addClass('animate');
flyingChip.css({
transform: `translate(${targetRect.left - sourceRect.left}px, ${targetRect.top - sourceRect.top}px)`
});
});
});
// Clean up and update DOM after animation
setTimeout(() => {
flyingChip.remove();
updateVmPosition(vmId, from, to, vm);
}, 220); // Slightly longer than animation duration to ensure completion
});
}
function updateVmPosition(vmId, from, to, vm) {
// Remove from source
if (from) {
$(`#server-blade-${from} #vm-chip-${vmId}`).remove();
} else {
$(`#unassigned-${vmId}`).remove();
}
// Add to target
if (to) {
let vmChipsContainer = $(`#server-blade-${to} .vm-chips`);
if (!vmChipsContainer.length) {
// Create vm-chips container if it doesn't exist
vmChipsContainer = $('<div class="vm-chips"></div>');
$(`#server-blade-${to}`).append(vmChipsContainer);
}
const newChip = createVmChip(vm);
newChip.attr('id', `vm-chip-${vm.id}`);
vmChipsContainer.append(newChip);
// Update server blade state (empty/not empty)
$(`#server-blade-${to}`).removeClass('empty');
}
// Update source server empty state
if (from) {
const sourceChips = $(`#server-blade-${from} .vm-chips`).children();
if (sourceChips.length === 0) {
$(`#server-blade-${from}`).addClass('empty');
}
}
}
function updateUtilizationBars(placement) {
placement.servers.forEach(server => {
const cpuBar = $(`#util-${server.id}-cpu`);
const memBar = $(`#util-${server.id}-mem`);
const stoBar = $(`#util-${server.id}-sto`);
const updates = [
{ bar: cpuBar, util: server.cpuUtilization || 0 },
{ bar: memBar, util: server.memoryUtilization || 0 },
{ bar: stoBar, util: server.storageUtilization || 0 }
];
updates.forEach(({ bar, util }) => {
if (bar.length) {
const newPct = Math.min(util * 100, 100);
const oldPct = parseFloat(bar[0].style.width) || 0;
bar.css('width', newPct + '%');
bar.removeClass('low medium high over').addClass(getUtilClass(util));
if (Math.abs(oldPct - newPct) > 1) {
bar.addClass('changed');
setTimeout(() => bar.removeClass('changed'), 300);
}
}
});
// Update server blade empty/overcommitted state
const blade = $(`#server-blade-${server.id}`);
const hasVms = server.vms && server.vms.length > 0;
const isOvercommitted = (server.cpuUtilization || 0) > 1 ||
(server.memoryUtilization || 0) > 1 ||
(server.storageUtilization || 0) > 1;
blade.toggleClass('empty', !hasVms);
blade.toggleClass('overcommitted', isOvercommitted);
});
}
function updateUnassignedList(vms, vmById) {
const container = $("#unassignedList");
const unassigned = vms.filter(vm => !vm.server);
if (unassigned.length === 0) {
if (!container.find('.all-assigned').length) {
container.empty();
container.append(`
<div class="all-assigned">
<i class="fas fa-check-circle"></i>
<strong>All VMs assigned!</strong>
</div>
`);
}
return;
}
// Remove "all assigned" message if present
container.find('.all-assigned').remove();
// Update existing or add new unassigned VMs
unassigned.sort((a, b) => (b.priority || 1) - (a.priority || 1));
unassigned.forEach(vm => {
if (!$(`#unassigned-${vm.id}`).length) {
const vmDiv = createUnassignedVmElement(vm);
container.append(vmDiv);
}
});
}
function createUnassignedVmElement(vm) {
return $(`
<div id="unassigned-${vm.id}" class="unassigned-vm">
<div class="name">
${vm.name}
${vm.affinityGroup ? `<span class="constraint-marker affinity">${vm.affinityGroup}</span>` : ''}
${vm.antiAffinityGroup ? `<span class="constraint-marker anti-affinity">${vm.antiAffinityGroup}</span>` : ''}
</div>
<div class="details">
<i class="fas fa-microchip"></i> ${vm.cpuCores}c
<i class="fas fa-memory ms-2"></i> ${vm.memoryGb}GB
<i class="fas fa-hdd ms-2"></i> ${vm.storageGb}GB
<span class="ms-2 badge bg-${getPriorityBadgeClass(vm.priority)}">P${vm.priority || 1}</span>
</div>
</div>
`);
}
function renderRackView(placement, vmById) {
const container = $("#rackViewContainer");
container.empty();
// Group servers by rack
const rackGroups = {};
placement.servers.forEach(server => {
const rack = server.rack || "Unracked";
if (!rackGroups[rack]) {
rackGroups[rack] = [];
}
rackGroups[rack].push(server);
});
// Sort racks alphabetically
const sortedRacks = Object.keys(rackGroups).sort();
sortedRacks.forEach(rackName => {
const servers = rackGroups[rackName];
servers.sort((a, b) => a.name.localeCompare(b.name));
const rackDiv = $('<div class="rack"></div>');
const rackHeader = $(`
<div class="rack-header">
<i class="fas fa-server"></i>
<span>${rackName}</span>
<span class="ms-auto badge bg-secondary">${servers.length} servers</span>
</div>
`);
rackDiv.append(rackHeader);
servers.forEach(server => {
const blade = createServerBlade(server, vmById);
rackDiv.append(blade);
});
container.append(rackDiv);
});
}
function createServerBlade(server, vmById) {
const cpuUtil = server.cpuUtilization || 0;
const memUtil = server.memoryUtilization || 0;
const storageUtil = server.storageUtilization || 0;
const isEmpty = !server.vms || server.vms.length === 0;
const isOvercommitted = cpuUtil > 1 || memUtil > 1 || storageUtil > 1;
let bladeClass = "server-blade";
if (isEmpty) bladeClass += " empty";
if (isOvercommitted) bladeClass += " overcommitted";
const blade = $(`<div id="server-blade-${server.id}" class="${bladeClass}"></div>`);
// Header
const header = $(`
<div class="server-blade-header">
<span class="server-name">${server.name}</span>
<span class="server-specs">${server.cpuCores}c / ${server.memoryGb}GB / ${server.storageGb}GB</span>
</div>
`);
blade.append(header);
// Utilization bars with IDs for animation
const utilBars = $('<div class="utilization-mini"></div>');
utilBars.append(createMiniUtilBar(cpuUtil, "CPU", `util-${server.id}-cpu`));
utilBars.append(createMiniUtilBar(memUtil, "MEM", `util-${server.id}-mem`));
utilBars.append(createMiniUtilBar(storageUtil, "STO", `util-${server.id}-sto`));
blade.append(utilBars);
// VM chips with IDs for animation
const vmChips = $('<div class="vm-chips"></div>');
if (server.vms && server.vms.length > 0) {
server.vms.forEach(vmId => {
const vm = vmById[vmId];
if (vm) {
const chip = createVmChip(vm);
chip.attr('id', `vm-chip-${vm.id}`);
vmChips.append(chip);
}
});
}
blade.append(vmChips);
// Click handler for details - pass server ID to get fresh data
blade.click(function(e) {
if (!$(e.target).hasClass('vm-chip')) {
showServerDetails(server.id);
}
});
return blade;
}
function createMiniUtilBar(value, label, id) {
const percentage = Math.min(value * 100, 100);
const utilClass = getUtilClass(value);
return $(`
<div class="util-mini-bar" title="${label}: ${Math.round(value * 100)}%">
<div id="${id}" class="util-mini-fill ${utilClass}" style="width: ${percentage}%"></div>
</div>
`);
}
function createVmChip(vm) {
const priority = vm.priority || 1;
let chipClass = `vm-chip priority-${priority}`;
if (vm.affinityGroup) chipClass += " affinity";
if (vm.antiAffinityGroup) chipClass += " anti-affinity";
let tooltip = vm.name;
tooltip += `\nCPU: ${vm.cpuCores} cores`;
tooltip += `\nMemory: ${vm.memoryGb} GB`;
tooltip += `\nStorage: ${vm.storageGb} GB`;
tooltip += `\nPriority: ${priority}`;
if (vm.affinityGroup) tooltip += `\nAffinity: ${vm.affinityGroup}`;
if (vm.antiAffinityGroup) tooltip += `\nAnti-Affinity: ${vm.antiAffinityGroup}`;
return $(`<span class="${chipClass}" title="${tooltip}">${vm.name}</span>`);
}
function renderCardView(placement, vmById) {
const container = $("#cardViewContainer");
container.empty();
const sortedServers = [...placement.servers].sort((a, b) => {
const aVms = a.vms ? a.vms.length : 0;
const bVms = b.vms ? b.vms.length : 0;
if (bVms !== aVms) return bVms - aVms;
return a.name.localeCompare(b.name);
});
sortedServers.forEach(server => {
const card = createServerCard(server, vmById);
container.append(card);
});
}
function createServerCard(server, vmById) {
const cpuUtil = server.cpuUtilization || 0;
const memUtil = server.memoryUtilization || 0;
const storageUtil = server.storageUtilization || 0;
const isEmpty = !server.vms || server.vms.length === 0;
const cardWrapper = $('<div class="col-md-6 col-lg-4"></div>');
const card = $(`
<div class="card h-100 ${isEmpty ? 'opacity-50' : ''}">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>${server.name}</strong>
${server.rack ? `<span class="badge bg-secondary">${server.rack}</span>` : ''}
</div>
<div class="card-body">
<div class="mb-3">
${createUtilRow("CPU", cpuUtil, server.cpuCores + " cores")}
${createUtilRow("Memory", memUtil, server.memoryGb + " GB")}
${createUtilRow("Storage", storageUtil, server.storageGb + " GB")}
</div>
<div class="vm-chips">
${(server.vms || []).map(vmId => {
const vm = vmById[vmId];
if (vm) {
const priority = vm.priority || 1;
let chipClass = `vm-chip priority-${priority}`;
if (vm.affinityGroup) chipClass += " affinity";
if (vm.antiAffinityGroup) chipClass += " anti-affinity";
return `<span class="${chipClass}" title="${vm.name}">${vm.name}</span>`;
}
return '';
}).join('')}
</div>
</div>
</div>
`);
cardWrapper.append(card);
return cardWrapper;
}
function createUtilRow(label, value, capacity) {
const percentage = Math.min(value * 100, 100);
const displayPercentage = Math.round(value * 100);
const utilClass = getUtilClass(value);
const bgClass = {
'low': 'bg-success',
'medium': 'bg-warning',
'high': 'bg-danger',
'over': 'bg-danger'
}[utilClass];
return `
<div class="mb-2">
<div class="d-flex justify-content-between small">
<span>${label}</span>
<span class="text-muted">${capacity} (${displayPercentage}%)</span>
</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar ${bgClass}" style="width: ${percentage}%"></div>
</div>
</div>
`;
}
function renderUnassignedVMs(vms) {
const container = $("#unassignedList");
container.empty();
const unassigned = vms.filter(vm => !vm.server);
if (unassigned.length === 0) {
container.append(`
<div class="all-assigned">
<i class="fas fa-check-circle"></i>
<strong>All VMs assigned!</strong>
</div>
`);
return;
}
unassigned.sort((a, b) => (b.priority || 1) - (a.priority || 1));
unassigned.forEach(vm => {
const vmDiv = createUnassignedVmElement(vm);
container.append(vmDiv);
});
}
function getPriorityBadgeClass(priority) {
switch(priority) {
case 5: return "danger";
case 4: return "primary";
case 3: return "success";
case 2: return "secondary";
default: return "light text-dark";
}
}
function showServerDetails(serverId) {
// Always get fresh data from loadedPlacement
if (!loadedPlacement || !loadedPlacement.servers) {
console.error('No placement data available');
return;
}
const server = loadedPlacement.servers.find(s => s.id === serverId);
if (!server) {
console.error('Server not found:', serverId);
return;
}
// Build VM lookup from current placement
const vmById = {};
if (loadedPlacement.vms) {
loadedPlacement.vms.forEach(vm => vmById[vm.id] = vm);
}
const modal = new bootstrap.Modal("#serverDetailModal");
const content = $("#serverDetailModalContent");
$("#serverDetailModalLabel").html(`<i class="fas fa-server me-2"></i>${server.name}`);
const cpuUtil = server.cpuUtilization || 0;
const memUtil = server.memoryUtilization || 0;
const storageUtil = server.storageUtilization || 0;
content.html(`
<div class="mb-3">
<h6>Specifications</h6>
<table class="table table-sm">
<tr><td>Rack</td><td>${server.rack || 'Unracked'}</td></tr>
<tr><td>CPU Cores</td><td>${server.cpuCores}</td></tr>
<tr><td>Memory</td><td>${server.memoryGb} GB</td></tr>
<tr><td>Storage</td><td>${server.storageGb} GB</td></tr>
</table>
</div>
<div class="mb-3">
<h6>Utilization</h6>
${createUtilRow("CPU", cpuUtil, `${server.usedCpu || 0}/${server.cpuCores} cores`)}
${createUtilRow("Memory", memUtil, `${server.usedMemory || 0}/${server.memoryGb} GB`)}
${createUtilRow("Storage", storageUtil, `${server.usedStorage || 0}/${server.storageGb} GB`)}
</div>
<div>
<h6>Assigned VMs (${server.vms ? server.vms.length : 0})</h6>
${server.vms && server.vms.length > 0 ? `
<table class="table table-sm">
<thead>
<tr>
<th>Name</th>
<th>CPU</th>
<th>Memory</th>
<th>Storage</th>
<th>Priority</th>
</tr>
</thead>
<tbody>
${server.vms.map(vmId => {
const vm = vmById[vmId];
if (vm) {
return `
<tr>
<td>${vm.name}</td>
<td>${vm.cpuCores}</td>
<td>${vm.memoryGb} GB</td>
<td>${vm.storageGb} GB</td>
<td><span class="badge bg-${getPriorityBadgeClass(vm.priority)}">P${vm.priority || 1}</span></td>
</tr>
`;
}
return '';
}).join('')}
</tbody>
</table>
` : '<p class="text-muted">No VMs assigned</p>'}
</div>
`);
modal.show();
}
function getUtilClass(value) {
if (value > 1) return 'over';
if (value > 0.8) return 'high';
if (value > 0.5) return 'medium';
return 'low';
}
function solve() {
if (!loadedPlacement) {
showSimpleError("No placement data loaded. Please wait for the data to load or refresh the page.");
return;
}
// Reset animation state for solving - capture current positions before solving modifies them
vmPositionCache = buildPositionCache(loadedPlacement);
console.log('Solve started. Initial position cache size:', Object.keys(vmPositionCache).length);
$.post("/placements", JSON.stringify(loadedPlacement), function (data) {
placementId = data;
refreshSolvingButtons(true);
}).fail(function (xhr, ajaxOptions, thrownError) {
showError("Start solving failed.", xhr);
refreshSolvingButtons(false);
}, "text");
}
function stopSolving() {
$.delete(`/placements/${placementId}`, function () {
refreshSolvingButtons(false);
// Do a final full render to ensure accuracy
isFirstRender = true;
refreshPlacement();
}).fail(function (xhr, ajaxOptions, thrownError) {
showError("Stop solving failed.", xhr);
});
}
function analyze() {
const modal = new bootstrap.Modal("#scoreAnalysisModal");
modal.show();
const scoreAnalysisModalContent = $("#scoreAnalysisModalContent");
scoreAnalysisModalContent.empty();
if (!loadedPlacement || loadedPlacement.score == null) {
scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'Solve' button.");
return;
}
$('#scoreAnalysisScoreLabel').text(`(${loadedPlacement.score})`);
$.put("/placements/analyze", JSON.stringify(loadedPlacement), function (scoreAnalysis) {
let constraints = scoreAnalysis.constraints;
constraints.sort((a, b) => {
let aComponents = getScoreComponents(a.score);
let bComponents = getScoreComponents(b.score);
if (aComponents.hard < 0 && bComponents.hard > 0) return -1;
if (aComponents.hard > 0 && bComponents.soft < 0) return 1;
if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) {
return -1;
} else {
if (Math.abs(aComponents.soft) > Math.abs(bComponents.soft)) {
return -1;
}
return Math.abs(bComponents.soft) - Math.abs(aComponents.soft);
}
});
constraints = constraints.map((e) => {
let components = getScoreComponents(e.weight);
e.type = components.hard !== 0 ? 'hard' : 'soft';
e.weight = components[e.type];
let scores = getScoreComponents(e.score);
e.implicitScore = scores.hard !== 0 ? scores.hard : scores.soft;
return e;
});
scoreAnalysisModalContent.empty();
const analysisTable = $(`<table class="table"/>`).css({textAlign: 'center'});
const analysisTHead = $(`<thead/>`).append($(`<tr/>`)
.append($(`<th></th>`))
.append($(`<th>Constraint</th>`).css({textAlign: 'left'}))
.append($(`<th>Type</th>`))
.append($(`<th># Matches</th>`))
.append($(`<th>Weight</th>`))
.append($(`<th>Score</th>`)));
analysisTable.append(analysisTHead);
const analysisTBody = $(`<tbody/>`);
$.each(constraints, (index, constraintAnalysis) => {
let icon = constraintAnalysis.type === "hard" && constraintAnalysis.implicitScore < 0
? '<span class="fas fa-exclamation-triangle text-danger"></span>'
: '';
if (!icon) {
icon = constraintAnalysis.matches.length === 0
? '<span class="fas fa-check-circle text-success"></span>'
: '';
}
let row = $(`<tr/>`);
row.append($(`<td/>`).html(icon))
.append($(`<td/>`).text(constraintAnalysis.name).css({textAlign: 'left'}))
.append($(`<td/>`).html(`<span class="badge bg-${constraintAnalysis.type === 'hard' ? 'danger' : 'warning'}">${constraintAnalysis.type}</span>`))
.append($(`<td/>`).html(`<b>${constraintAnalysis.matches.length}</b>`))
.append($(`<td/>`).text(constraintAnalysis.weight))
.append($(`<td/>`).text(constraintAnalysis.implicitScore));
analysisTBody.append(row);
});
analysisTable.append(analysisTBody);
scoreAnalysisModalContent.append(analysisTable);
}).fail(function (xhr, ajaxOptions, thrownError) {
showError("Analyze failed.", xhr);
}, "text");
}
function getScoreComponents(score) {
let components = {hard: 0, soft: 0};
if (!score) return components;
$.each([...score.matchAll(/(-?\d*\.?\d+)(hard|soft)/g)], (i, parts) => {
components[parts[2]] = parseFloat(parts[1]);
});
return components;
}
function refreshSolvingButtons(solving) {
if (solving) {
$("#solveButton").hide();
$("#stopSolvingButton").show();
$("#solvingSpinner").addClass("active");
// Add solving pulse animation to summary card values
$(".summary-card .value").addClass("solving-pulse");
if (autoRefreshIntervalId == null) {
// 250ms polling for smooth real-time animations
autoRefreshIntervalId = setInterval(refreshPlacement, 250);
}
} else {
$("#solveButton").show();
$("#stopSolvingButton").hide();
$("#solvingSpinner").removeClass("active");
// Remove solving pulse animation
$(".summary-card .value").removeClass("solving-pulse");
if (autoRefreshIntervalId != null) {
clearInterval(autoRefreshIntervalId);
autoRefreshIntervalId = null;
}
}
}
function replaceQuickstartSolverForgeAutoHeaderFooter() {
const solverforgeHeader = $("header#solverforge-auto-header");
if (solverforgeHeader != null) {
solverforgeHeader.css("background-color", "#ffffff");
solverforgeHeader.append(
$(`<div class="container-fluid">
<nav class="navbar sticky-top navbar-expand-lg shadow-sm mb-3" style="background-color: #ffffff;">
<a class="navbar-brand" href="https://www.solverforge.org">
<img src="/webjars/solverforge/img/solverforge-horizontal.svg" alt="SolverForge logo" width="400">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="nav nav-pills">
<li class="nav-item active" id="navUIItem">
<button class="nav-link active" id="navUI" data-bs-toggle="pill" data-bs-target="#demo" type="button" style="color: #1f2937;">Demo UI</button>
</li>
<li class="nav-item" id="navRestItem">
<button class="nav-link" id="navRest" data-bs-toggle="pill" data-bs-target="#rest" type="button" style="color: #1f2937;">Guide</button>
</li>
<li class="nav-item" id="navOpenApiItem">
<button class="nav-link" id="navOpenApi" data-bs-toggle="pill" data-bs-target="#openapi" type="button" style="color: #1f2937;">REST API</button>
</li>
</ul>
</div>
<div class="ms-auto">
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="background-color: #10b981; color: #ffffff; border-color: #10b981;">
Data
</button>
<div id="testDataButton" class="dropdown-menu" aria-labelledby="dropdownMenuButton"></div>
</div>
</div>
</nav>
</div>`));
}
const solverforgeFooter = $("footer#solverforge-auto-footer");
if (solverforgeFooter != null) {
solverforgeFooter.append(
$(`<footer class="bg-black text-white-50">
<div class="container">
<div class="hstack gap-3 p-4">
<div class="ms-auto"><a class="text-white" href="https://www.solverforge.org">SolverForge</a></div>
<div class="vr"></div>
<div><a class="text-white" href="https://www.solverforge.org/docs">Documentation</a></div>
<div class="vr"></div>
<div><a class="text-white" href="https://github.com/SolverForge/solverforge-legacy">Code</a></div>
<div class="vr"></div>
<div class="me-auto"><a class="text-white" href="mailto:info@solverforge.org">Support</a></div>
</div>
</div>
</footer>`));
}
}
function copyTextToClipboard(elementId) {
const element = document.getElementById(elementId);
if (element) {
navigator.clipboard.writeText(element.textContent).then(() => {
// Optional: show feedback
}).catch(err => {
console.error('Failed to copy text: ', err);
});
}
}