blackopsrepl/quickstart-template
2
1/**2 * =============================================================================3 * SOLVERFORGE QUICKSTART TEMPLATE - APPLICATION JAVASCRIPT4 * =============================================================================5 *6 * This file contains all the client-side logic for the SolverForge quickstart7 * template. It implements a "code-link" educational UI that teaches users8 * how to build the very interface they're looking at.9 *10 * FILE STRUCTURE:11 * ---------------12 * 1. GLOBAL STATE - Variables tracking UI state, loaded data, solving jobs13 * 2. INITIALIZATION - Document ready handler and app setup14 * 3. AJAX CONFIGURATION - jQuery AJAX setup and HTTP method extensions15 * 4. DEMO DATA LOADING - Fetching and selecting sample datasets16 * 5. SCHEDULE/SOLUTION LOADING - Getting solution data from backend17 * 6. RENDERING - Card-based visualization of tasks and resources18 * 7. KPI UPDATES - Key Performance Indicator card updates19 * 8. SOLVING OPERATIONS - Start, stop, and poll the solver20 * 9. SCORE ANALYSIS - Constraint breakdown modal21 * 10. TAB NAVIGATION HELPERS - Programmatic tab switching22 * 11. BUILD TAB - Source code viewer with syntax highlighting23 * 12. INTERACTIVE CODE FEATURES - Click-to-code navigation24 * 13. NOTIFICATIONS - Toast messages for errors and info25 * 14. UTILITY FUNCTIONS - Helpers and formatters26 * 15. RESOURCE & TASK CRUD - Adding and removing entities dynamically27 * 16. CONSTRAINT WEIGHT CONTROLS - Adjusting optimization weights28 *29 * CUSTOMIZATION GUIDE:30 * --------------------31 * When adapting this template for your domain:32 *33 * 1. renderSolution() - Change task card layout for your entities34 * 2. renderResources() - Change resource card layout for your facts35 * 3. updateKPIs() - Update metrics shown in KPI cards36 * 4. countViolations() - Implement violation detection for your constraints37 *38 * API ENDPOINTS USED:39 * -------------------40 * - GET /demo-data - List available demo datasets41 * - GET /demo-data/{id} - Get a specific demo dataset42 * - POST /schedules - Start solving (returns job ID)43 * - GET /schedules/{jobId} - Get current solution44 * - DELETE /schedules/{jobId}- Stop solving45 * - PUT /schedules/analyze - Analyze score breakdown46 */47 48 49// =============================================================================50// 1. GLOBAL STATE51// =============================================================================52// These variables track the application state throughout the session.53// They are modified by various functions and checked to determine UI behavior.54 55/**56 * Interval ID for auto-refreshing the solution while solving.57 * Set by setInterval() when solving starts, cleared when solving stops.58 * Used to poll the backend for updates every 2 seconds.59 *60 * @type {number|null}61 */62let autoRefreshIntervalId = null;63 64/**65 * Currently selected demo data ID (e.g., "SMALL", "MEDIUM", "LARGE").66 * Set when user selects from the Data dropdown.67 * Used to fetch the initial dataset before solving.68 *69 * @type {string|null}70 */71let demoDataId = null;72 73/**74 * Current solving job ID (UUID string from the backend).75 * Set when solve() successfully starts a job.76 * Used to poll for updates and stop solving.77 *78 * @type {string|null}79 */80let scheduleId = null;81 82/**83 * The currently loaded schedule/solution data.84 * Contains the full problem definition and current solution:85 * - resources: Array of resource objects (problem facts)86 * - tasks: Array of task objects (planning entities)87 * - score: HardSoftScore string (e.g., "0hard/-50soft")88 * - solverStatus: "NOT_SOLVING" or "SOLVING"89 *90 * @type {Object|null}91 */92let loadedSchedule = null;93 94/**95 * Currently displayed file in the Build tab code viewer.96 * Used to track which file is being shown and for copy functionality.97 *98 * @type {string}99 */100let currentFile = 'domain.py';101 102/**103 * Cached source code content for the current file.104 * Populated by loadSourceFile() when fetching from API.105 *106 * @type {string}107 */108let currentFileContent = '';109 110 111// =============================================================================112// 2. INITIALIZATION113// =============================================================================114// Application startup code. Sets up event handlers and loads initial data.115 116/**117 * Document ready handler with safe initialization.118 *119 * PATTERN: Double-initialization120 * We use both $(window).on('load') and setTimeout() to ensure initialization121 * happens even if some external resources load slowly or fail to fire the122 * load event.123 *124 * This pattern is common in SolverForge quickstarts to handle:125 * - Slow CDN responses126 * - Browser caching issues127 * - Race conditions with external scripts128 */129$(document).ready(function () {130 let initialized = false;131 132 /**133 * Safe initialization wrapper.134 * Ensures initializeApp() is only called once.135 */136 function safeInitialize() {137 if (!initialized) {138 initialized = true;139 initializeApp();140 }141 }142 143 // Primary: Initialize when all resources (images, scripts) are loaded144 $(window).on('load', safeInitialize);145 146 // Fallback: Initialize after short delay if load event doesn't fire147 setTimeout(safeInitialize, 100);148});149 150/**151 * Main initialization function.152 *153 * Called once when the page is ready. This function:154 * 1. Sets up button click handlers155 * 2. Configures AJAX defaults156 * 3. Loads the demo data list157 * 4. Initializes the Build tab code viewer158 * 5. Sets up code-link click handlers159 *160 * CUSTOMIZATION: Add your own initialization code here.161 */162function initializeApp() {163 console.log('SolverForge Quickstart Template initializing...');164 165 // =========================================================================166 // BUTTON CLICK HANDLERS167 // =========================================================================168 169 // Solve button - starts the optimization170 // Connected to solve() function which POSTs to /schedules171 $("#solveButton").click(function () {172 solve();173 });174 175 // Stop button - terminates solving early176 // Connected to stopSolving() which DELETEs /schedules/{id}177 $("#stopSolvingButton").click(function () {178 stopSolving();179 });180 181 // Analyze button - shows score breakdown modal182 // Connected to analyze() which PUTs to /schedules/analyze183 $("#analyzeButton").click(function () {184 analyze();185 });186 187 // =========================================================================188 // AJAX SETUP & DATA LOADING189 // =========================================================================190 191 // Configure jQuery AJAX defaults (headers, methods)192 setupAjax();193 194 // Load the list of available demo datasets195 fetchDemoData();196 197 // =========================================================================198 // BUILD TAB INITIALIZATION199 // =========================================================================200 201 // Set up file navigator click handlers202 setupBuildTab();203 204 // Load the default file (domain.py)205 loadSourceFile('domain.py');206 207 // =========================================================================208 // INTERACTIVE CODE FEATURE INITIALIZATION209 // =========================================================================210 211 // Set up click handlers for code-link elements212 setupCodeLinkHandlers();213 214 console.log('Initialization complete');215}216 217 218// =============================================================================219// 3. AJAX CONFIGURATION220// =============================================================================221// jQuery AJAX setup for communicating with the backend REST API.222 223/**224 * Configures jQuery AJAX with proper headers and HTTP method extensions.225 *226 * WHAT THIS DOES:227 * 1. Sets default Content-Type and Accept headers for JSON228 * 2. Adds $.put() and $.delete() methods to jQuery229 * (jQuery only has $.get() and $.post() by default)230 *231 * WHY WE NEED THIS:232 * RESTful APIs use all HTTP methods (GET, POST, PUT, DELETE).233 * The Accept header includes text/plain because job IDs are returned as text.234 */235function setupAjax() {236 // Set default headers for all AJAX requests237 $.ajaxSetup({238 headers: {239 'Content-Type': 'application/json',240 'Accept': 'application/json,text/plain', // text/plain for job ID241 }242 });243 244 // Extend jQuery with PUT and DELETE methods245 // These mirror the signature of $.get() and $.post()246 jQuery.each(["put", "delete"], function (i, method) {247 jQuery[method] = function (url, data, callback, type) {248 // Handle optional parameters (data can be omitted)249 if (jQuery.isFunction(data)) {250 type = type || callback;251 callback = data;252 data = undefined;253 }254 return jQuery.ajax({255 url: url,256 type: method,257 dataType: type,258 data: data,259 success: callback260 });261 };262 });263}264 265 266// =============================================================================267// 4. DEMO DATA LOADING268// =============================================================================269// Functions for loading sample datasets from the backend.270 271/**272 * Fetches the list of available demo datasets and populates the dropdown.273 *274 * FLOW:275 * 1. GET /demo-data returns ["SMALL", "MEDIUM", "LARGE"] (or similar)276 * 2. For each dataset, create a dropdown menu item277 * 3. Auto-select and load the first dataset278 *279 * CUSTOMIZATION:280 * The backend demo_data.py defines what datasets are available.281 * Each dataset is a complete Schedule object with resources and tasks.282 */283function fetchDemoData() {284 $.get("/demo-data", function (data) {285 const dropdown = $("#dataDropdown");286 dropdown.empty();287 288 // Create a dropdown item for each available dataset289 data.forEach(item => {290 const menuItem = $(`291 <li>292 <a class="dropdown-item" href="#" data-dataset="${item}">293 ${item}294 </a>295 </li>296 `);297 298 // Click handler for this dataset299 menuItem.find('a').click(function (e) {300 e.preventDefault();301 302 // Update visual selection303 dropdown.find('.dropdown-item').removeClass('active');304 $(this).addClass('active');305 306 // Reset solving state and load new data307 scheduleId = null;308 demoDataId = item;309 310 // Load and display the selected dataset311 refreshSchedule();312 });313 314 dropdown.append(menuItem);315 });316 317 // Auto-select the first dataset318 if (data.length > 0) {319 demoDataId = data[0];320 dropdown.find('.dropdown-item').first().addClass('active');321 refreshSchedule();322 }323 }).fail(function (xhr, ajaxOptions, thrownError) {324 // Handle case where backend is not running or has no data325 showNotification("Failed to load demo data. Is the server running?", "danger");326 console.error('Failed to fetch demo data:', thrownError);327 });328}329 330 331// =============================================================================332// 5. SCHEDULE/SOLUTION LOADING333// =============================================================================334// Functions for fetching and displaying solution data.335 336/**337 * Fetches and displays the current schedule/solution.338 *339 * LOGIC:340 * - If scheduleId is set: GET /schedules/{scheduleId} for solving progress341 * - If scheduleId is null: GET /demo-data/{demoDataId} for initial data342 *343 * WHEN CALLED:344 * - When a dataset is selected from the dropdown345 * - Every 2 seconds while solving (via setInterval)346 * - After stopping solving347 */348function refreshSchedule() {349 // Determine which endpoint to call350 let path = "/schedules/" + scheduleId;351 if (scheduleId === null) {352 // No active job - load demo data instead353 if (demoDataId === null) {354 showNotification("Please select a dataset from the Data dropdown.", "warning");355 return;356 }357 path = "/demo-data/" + demoDataId;358 }359 360 // Fetch the schedule data361 $.getJSON(path, function (schedule) {362 loadedSchedule = schedule;363 renderSchedule(schedule);364 }).fail(function (xhr, ajaxOptions, thrownError) {365 showNotification("Failed to load schedule data.", "danger");366 console.error('Failed to fetch schedule:', thrownError);367 refreshSolvingButtons(false);368 });369}370 371/**372 * Renders the complete schedule/solution to the UI.373 *374 * UPDATES:375 * - Solve/Stop button visibility376 * - Spinner animation377 * - KPI cards378 * - Task cards in the tasks panel379 * - Resource cards in the resources panel380 *381 * @param {Object} schedule - The schedule data from the backend382 */383function renderSchedule(schedule) {384 if (!schedule) {385 console.error('No schedule data provided to renderSchedule');386 return;387 }388 389 console.log('Rendering schedule:', schedule);390 391 // Update solving buttons based on solver status392 const isSolving = schedule.solverStatus != null &&393 schedule.solverStatus !== "NOT_SOLVING";394 refreshSolvingButtons(isSolving);395 396 // Update KPI cards with current metrics397 updateKPIs(schedule);398 399 // Render the solution visualization (task cards)400 renderSolution(schedule);401 402 // Render the resources panel403 renderResources(schedule);404}405 406 407// =============================================================================408// 6. RENDERING - Card-Based Visualization409// =============================================================================410// Functions that create the visual representation of tasks and resources.411 412/**413 * Renders the tasks panel with card-based layout.414 *415 * CARD STATES:416 * - Default (green border): Task is assigned to a resource417 * - .unassigned (orange border): Task has no resource assigned418 * - .violation (red border): Task has a constraint violation419 *420 * CUSTOMIZATION:421 * Modify this function to match your domain model:422 * - Change what fields are displayed423 * - Add domain-specific badges or indicators424 * - Implement custom violation detection425 *426 * @param {Object} schedule - Schedule containing tasks array427 */428function renderSolution(schedule) {429 const panel = $("#tasksPanel");430 panel.empty();431 432 // Update task count badge433 const taskCount = schedule.tasks ? schedule.tasks.length : 0;434 $("#taskCount").text(taskCount);435 436 // Handle empty state437 if (!schedule.tasks || schedule.tasks.length === 0) {438 panel.html('<p class="text-muted text-center">No tasks in this dataset</p>');439 return;440 }441 442 // Create the task grid container443 const grid = $('<div class="task-grid"></div>');444 445 // Render each task as a card446 schedule.tasks.forEach(task => {447 const card = createTaskCard(task, schedule);448 grid.append(card);449 });450 451 panel.append(grid);452}453 454/**455 * Creates a single task card element.456 *457 * STRUCTURE:458 * <div class="task-card [unassigned|violation] code-link">459 * <div class="task-name">Task Name <duration></div>460 * <div class="task-detail">Skill: skill_name</div>461 * <div class="task-detail">Assigned: resource_name</div>462 * </div>463 *464 * CUSTOMIZATION:465 * Modify this to show your domain-specific fields.466 *467 * @param {Object} task - The task object468 * @param {Object} schedule - The full schedule (for violation checking)469 * @returns {jQuery} The task card jQuery element470 */471function createTaskCard(task, schedule) {472 // Determine card state473 const isAssigned = task.resource != null;474 const hasViolation = checkTaskViolation(task, schedule);475 476 // Build CSS classes477 let cardClass = 'task-card code-link';478 if (hasViolation) {479 cardClass += ' violation';480 } else if (!isAssigned) {481 cardClass += ' unassigned';482 }483 484 // Create the card (escaping id for onclick)485 const escapedId = task.id.replace(/'/g, "\\'");486 const card = $(`<div class="${cardClass}" data-target="app.js:createTaskCard"></div>`);487 488 // Task name, duration, and remove button489 const nameRow = $('<div class="task-name"></div>');490 nameRow.append($('<span></span>').text(task.name));491 const rightSide = $('<div class="d-flex align-items-center gap-2"></div>');492 rightSide.append($('<span class="task-duration"></span>').text(`${task.duration}m`));493 rightSide.append($(`<button class="btn btn-sm btn-outline-danger" onclick="removeTask('${escapedId}', event)" title="Remove Task"><i class="fas fa-minus"></i></button>`));494 nameRow.append(rightSide);495 card.append(nameRow);496 497 // Required skill (if any)498 if (task.requiredSkill) {499 const skillRow = $('<div class="task-detail"></div>');500 skillRow.append($('<span class="skill-tag"></span>').text(task.requiredSkill));501 card.append(skillRow);502 }503 504 // Assignment status505 const assignmentRow = $('<div class="task-detail"></div>');506 if (isAssigned) {507 assignmentRow.html(`<span class="assigned-badge"><i class="fas fa-check me-1"></i>${task.resource}</span>`);508 } else {509 assignmentRow.html('<span class="unassigned-badge">Unassigned</span>');510 }511 card.append(assignmentRow);512 513 return card;514}515 516/**517 * Checks if a task has any constraint violations.518 *519 * CUSTOMIZATION:520 * Implement your domain-specific violation detection here.521 * This example checks:522 * - Required skill: Is the task assigned to a resource with the required skill?523 *524 * @param {Object} task - The task to check525 * @param {Object} schedule - The schedule containing resources526 * @returns {boolean} True if task has a violation527 */528function checkTaskViolation(task, schedule) {529 // If not assigned, it's not a violation (just unassigned)530 if (!task.resource) {531 return false;532 }533 534 // Check required skill constraint535 if (task.requiredSkill) {536 const resource = schedule.resources.find(r => r.name === task.resource);537 if (resource) {538 // Check if resource has the required skill539 const hasSkill = resource.skills &&540 resource.skills.includes(task.requiredSkill);541 if (!hasSkill) {542 return true; // Skill violation!543 }544 }545 }546 547 return false;548}549 550/**551 * Renders the resources panel with card-based layout.552 *553 * CARD STRUCTURE:554 * - Resource name555 * - Capacity utilization bar (color-coded)556 * - Skills list557 *558 * CUSTOMIZATION:559 * Modify this function to match your problem facts.560 *561 * @param {Object} schedule - Schedule containing resources array562 */563function renderResources(schedule) {564 const panel = $("#resourcesPanel");565 panel.empty();566 567 // Update resource count badge568 const resourceCount = schedule.resources ? schedule.resources.length : 0;569 $("#resourceCount").text(resourceCount);570 571 // Handle empty state572 if (!schedule.resources || schedule.resources.length === 0) {573 panel.html('<p class="text-muted text-center">No resources in this dataset</p>');574 return;575 }576 577 // Render each resource as a card578 schedule.resources.forEach(resource => {579 const card = createResourceCard(resource, schedule);580 panel.append(card);581 });582}583 584/**585 * Creates a single resource card element.586 *587 * FEATURES:588 * - Capacity bar showing utilization589 * - Color-coded: green (<80%), orange (80-100%), red (>100%)590 * - Skills displayed as tags591 *592 * @param {Object} resource - The resource object593 * @param {Object} schedule - The schedule (for calculating utilization)594 * @returns {jQuery} The resource card jQuery element595 */596function createResourceCard(resource, schedule) {597 // Calculate utilization598 const totalDuration = schedule.tasks599 ? schedule.tasks600 .filter(t => t.resource === resource.name)601 .reduce((sum, t) => sum + t.duration, 0)602 : 0;603 const utilization = resource.capacity > 0604 ? (totalDuration / resource.capacity) * 100605 : 0;606 607 // Determine capacity bar color608 let fillClass = '';609 if (utilization > 100) {610 fillClass = 'danger';611 } else if (utilization > 80) {612 fillClass = 'warning';613 }614 615 // Create skills badges HTML616 const skillsHtml = resource.skills && resource.skills.length > 0617 ? resource.skills.map(s => `<span class="skill-tag me-1">${s}</span>`).join('')618 : '<span class="text-muted small">No skills</span>';619 620 // Build the card (escaping name for onclick)621 const escapedName = resource.name.replace(/'/g, "\\'");622 const card = $(`623 <div class="resource-card code-link" data-target="app.js:createResourceCard">624 <div class="resource-header">625 <span class="resource-name">${resource.name}</span>626 <div class="d-flex align-items-center gap-2">627 <span class="resource-stats">${totalDuration}/${resource.capacity} min</span>628 <button class="btn btn-sm btn-outline-danger" onclick="removeResource('${escapedName}', event)" title="Remove Resource">629 <i class="fas fa-minus"></i>630 </button>631 </div>632 </div>633 <div class="capacity-bar">634 <div class="capacity-fill ${fillClass}" style="width: ${Math.min(utilization, 100)}%"></div>635 </div>636 <div class="skills-list mt-2">637 ${skillsHtml}638 </div>639 </div>640 `);641 642 return card;643}644 645 646// =============================================================================647// 7. KPI UPDATES648// =============================================================================649// Functions for updating the Key Performance Indicator cards.650 651/**652 * Updates all KPI cards with current metrics.653 *654 * KPIs DISPLAYED:655 * - Total Tasks: Number of planning entities656 * - Assigned: Tasks with non-null planning variable657 * - Violations: Hard constraint violations658 * - Score: Current HardSoftScore659 *660 * ANIMATION:661 * KPI values pulse when they change (using .kpi-pulse class).662 *663 * CUSTOMIZATION:664 * Modify this to show metrics relevant to your domain.665 *666 * @param {Object} schedule - The schedule data667 */668function updateKPIs(schedule) {669 // Calculate metrics670 const totalTasks = schedule.tasks ? schedule.tasks.length : 0;671 const assignedTasks = schedule.tasks672 ? schedule.tasks.filter(t => t.resource != null).length673 : 0;674 const violations = countViolations(schedule);675 const score = schedule.score || '?';676 677 // Update KPI values with pulse animation678 updateKPIValue('#kpiTotalTasks', totalTasks);679 updateKPIValue('#kpiAssigned', assignedTasks);680 updateKPIValue('#kpiViolations', violations);681 updateKPIValue('#kpiScore', formatScore(score));682}683 684/**685 * Updates a single KPI value with optional pulse animation.686 *687 * @param {string} selector - jQuery selector for the KPI value element688 * @param {string|number} newValue - The new value to display689 */690function updateKPIValue(selector, newValue) {691 const el = $(selector);692 const oldValue = el.text();693 694 // Only animate if value changed695 if (oldValue !== String(newValue)) {696 el.text(newValue);697 el.addClass('kpi-pulse');698 setTimeout(() => el.removeClass('kpi-pulse'), 500);699 }700}701 702/**703 * Counts the number of hard constraint violations.704 *705 * CUSTOMIZATION:706 * Implement your domain-specific violation counting here.707 * This example counts:708 * - Required skill violations709 * - Capacity violations710 *711 * @param {Object} schedule - The schedule data712 * @returns {number} Number of violations713 */714function countViolations(schedule) {715 if (!schedule.tasks || !schedule.resources) {716 return 0;717 }718 719 let violations = 0;720 721 // Count required skill violations722 schedule.tasks.forEach(task => {723 if (task.resource && task.requiredSkill) {724 const resource = schedule.resources.find(r => r.name === task.resource);725 if (resource && resource.skills) {726 if (!resource.skills.includes(task.requiredSkill)) {727 violations++;728 }729 }730 }731 });732 733 // Count capacity violations734 schedule.resources.forEach(resource => {735 const totalDuration = schedule.tasks736 .filter(t => t.resource === resource.name)737 .reduce((sum, t) => sum + t.duration, 0);738 if (totalDuration > resource.capacity) {739 violations++;740 }741 });742 743 return violations;744}745 746/**747 * Formats a score string for display.748 *749 * EXAMPLES:750 * - "0hard/-50soft" -> "0/-50"751 * - "-2hard/-15soft" -> "-2/-15"752 * - null -> "?"753 *754 * @param {string|null} score - The score string755 * @returns {string} Formatted score756 */757function formatScore(score) {758 if (!score || score === '?') {759 return '?';760 }761 762 const components = getScoreComponents(score);763 764 // Format as hard/soft765 return `${components.hard}/${components.soft}`;766}767 768 769// =============================================================================770// 8. SOLVING OPERATIONS771// =============================================================================772// Functions for starting, stopping, and monitoring the solver.773 774/**775 * Starts the optimization solver.776 *777 * FLOW:778 * 1. Get current constraint weights from UI sliders779 * 2. POST schedule + weights to /schedules780 * 3. Backend returns a job ID (UUID)781 * 4. Store job ID and start polling for updates782 *783 * POLLING:784 * While solving, refreshSchedule() is called every 2 seconds785 * via setInterval(). This polls GET /schedules/{jobId}.786 */787function solve() {788 // Check that we have data to solve789 if (!loadedSchedule) {790 showNotification("No data loaded. Please select a dataset first.", "warning");791 return;792 }793 794 // Get constraint weights from UI sliders795 const constraintWeights = getConstraintWeights();796 console.log('Constraint weights:', constraintWeights);797 798 // Build the request payload with schedule and weights799 const payload = {800 ...loadedSchedule,801 constraintWeights: constraintWeights802 };803 804 console.log('Starting solver with payload:', payload);805 806 // Send the schedule to the solver807 $.post("/schedules", JSON.stringify(payload), function (data) {808 // Store the job ID for future requests809 scheduleId = data;810 console.log('Solving started, job ID:', scheduleId);811 812 // Update UI to show solving state813 refreshSolvingButtons(true);814 815 showNotification("Solver started!", "success");816 }).fail(function (xhr, ajaxOptions, thrownError) {817 showNotification("Failed to start solving: " + thrownError, "danger");818 console.error('Failed to start solving:', xhr.responseText);819 refreshSolvingButtons(false);820 }, "text");821}822 823/**824 * Stops the currently running solver.825 *826 * FLOW:827 * 1. DELETE /schedules/{jobId}828 * 2. Backend terminates the solver829 * 3. Update UI to idle state830 * 4. Refresh to show final solution831 */832function stopSolving() {833 if (!scheduleId) {834 console.warn('No active solving job to stop');835 return;836 }837 838 console.log('Stopping solver, job ID:', scheduleId);839 840 $.delete(`/schedules/${scheduleId}`, function () {841 // Update UI to show stopped state842 refreshSolvingButtons(false);843 844 // Refresh to get final solution845 refreshSchedule();846 847 showNotification("Solver stopped", "info");848 }).fail(function (xhr, ajaxOptions, thrownError) {849 showNotification("Failed to stop solving: " + thrownError, "danger");850 console.error('Failed to stop solving:', xhr.responseText);851 });852}853 854/**855 * Updates the UI to reflect solving/not-solving state.856 *857 * WHEN SOLVING:858 * - Hides Solve button, shows Stop button859 * - Shows spinner animation860 * - Starts polling for updates every 2 seconds861 *862 * WHEN NOT SOLVING:863 * - Shows Solve button, hides Stop button864 * - Hides spinner865 * - Stops polling866 *867 * @param {boolean} solving - Whether solving is currently in progress868 */869function refreshSolvingButtons(solving) {870 if (solving) {871 // Solving state872 $("#solveButton").hide();873 $("#stopSolvingButton").show();874 $("#solvingSpinner").addClass("active");875 876 // Start polling for updates if not already polling877 if (autoRefreshIntervalId == null) {878 autoRefreshIntervalId = setInterval(refreshSchedule, 2000);879 }880 } else {881 // Idle state882 $("#solveButton").show();883 $("#stopSolvingButton").hide();884 $("#solvingSpinner").removeClass("active");885 886 // Stop polling887 if (autoRefreshIntervalId != null) {888 clearInterval(autoRefreshIntervalId);889 autoRefreshIntervalId = null;890 }891 }892}893 894 895// =============================================================================896// 9. SCORE ANALYSIS897// =============================================================================898// Functions for displaying the score analysis modal.899 900/**901 * Shows the score analysis modal with constraint breakdown.902 *903 * FLOW:904 * 1. Show the modal905 * 2. PUT /schedules/analyze with current schedule906 * 3. Render constraint breakdown table907 *908 * DISPLAY:909 * - Warning icon for violated hard constraints910 * - Check icon for satisfied constraints911 * - Match count and score contribution912 */913function analyze() {914 // Show the modal915 const modal = new bootstrap.Modal("#scoreAnalysisModal");916 modal.show();917 918 const modalContent = $("#scoreAnalysisContent");919 modalContent.html('<p class="text-center"><i class="fas fa-spinner fa-spin me-2"></i>Analyzing...</p>');920 921 // Check if we have a score to analyze922 if (!loadedSchedule) {923 modalContent.html('<p class="text-muted text-center">No data loaded.</p>');924 return;925 }926 927 // Update the score label in the modal header928 $('#scoreAnalysisScore').text(loadedSchedule.score || '?');929 930 // Fetch the score analysis from the backend931 $.put("/schedules/analyze", JSON.stringify(loadedSchedule), function (scoreAnalysis) {932 renderScoreAnalysis(scoreAnalysis, modalContent);933 }).fail(function (xhr, ajaxOptions, thrownError) {934 modalContent.html('<p class="text-danger text-center">Failed to analyze score.</p>');935 console.error('Failed to analyze score:', xhr.responseText);936 }, "json");937}938 939/**940 * Renders the score analysis table in the modal.941 *942 * TABLE COLUMNS:943 * - Icon: Warning/check status944 * - Constraint: Name of the constraint945 * - Type: hard/soft946 * - Matches: Number of violations947 * - Weight: Constraint weight948 * - Score: Score contribution949 *950 * @param {Object} scoreAnalysis - The analysis data from the backend951 * @param {jQuery} container - The container element to render into952 */953function renderScoreAnalysis(scoreAnalysis, container) {954 container.empty();955 956 let constraints = scoreAnalysis.constraints || [];957 958 if (constraints.length === 0) {959 container.html('<p class="text-muted text-center">No constraint data available.</p>');960 return;961 }962 963 // Sort constraints: violated hard constraints first, then by impact964 constraints.sort((a, b) => {965 let aComponents = getScoreComponents(a.score);966 let bComponents = getScoreComponents(b.score);967 968 // Hard constraints with negative score first969 if (aComponents.hard < 0 && bComponents.hard >= 0) return -1;970 if (aComponents.hard >= 0 && bComponents.hard < 0) return 1;971 972 // Then by absolute hard score973 if (Math.abs(aComponents.hard) !== Math.abs(bComponents.hard)) {974 return Math.abs(bComponents.hard) - Math.abs(aComponents.hard);975 }976 977 // Then by soft score978 return Math.abs(bComponents.soft) - Math.abs(aComponents.soft);979 });980 981 // Build the analysis table982 let html = '<table class="table table-sm">';983 html += `984 <thead>985 <tr>986 <th></th>987 <th>Constraint</th>988 <th>Type</th>989 <th>Matches</th>990 <th>Score</th>991 </tr>992 </thead>993 <tbody>994 `;995 996 constraints.forEach(constraint => {997 const components = getScoreComponents(constraint.score || "0hard/0soft");998 const isHard = components.hard !== 0;999 const isViolated = components.hard < 0 || components.soft < 0;1000 const matchCount = constraint.matches ? constraint.matches.length : 0;1001 1002 // Status icon1003 let icon = '';1004 if (isHard && components.hard < 0) {1005 icon = '<i class="fas fa-exclamation-triangle text-danger"></i>';1006 } else if (matchCount === 0) {1007 icon = '<i class="fas fa-check-circle text-success"></i>';1008 } else {1009 icon = '<i class="fas fa-minus-circle text-warning"></i>';1010 }1011 1012 // Type badge1013 const typeBadge = isHard1014 ? '<span class="badge bg-danger">hard</span>'1015 : '<span class="badge bg-success">soft</span>';1016 1017 // Score display1018 const scoreDisplay = isHard ? components.hard : components.soft;1019 1020 html += `1021 <tr>1022 <td>${icon}</td>1023 <td>${constraint.name}</td>1024 <td>${typeBadge}</td>1025 <td><strong>${matchCount}</strong></td>1026 <td>${scoreDisplay}</td>1027 </tr>1028 `;1029 });1030 1031 html += '</tbody></table>';1032 container.html(html);1033}1034 1035/**1036 * Parses a score string into its component parts.1037 *1038 * EXAMPLES:1039 * - "0hard/0soft" -> {hard: 0, soft: 0}1040 * - "-2hard/-15soft" -> {hard: -2, soft: -15}1041 *1042 * @param {string} score - The score string to parse1043 * @returns {Object} Object with hard, medium, soft properties1044 */1045function getScoreComponents(score) {1046 let components = {hard: 0, medium: 0, soft: 0};1047 1048 if (!score || typeof score !== 'string') {1049 return components;1050 }1051 1052 // Match patterns like "-2hard", "0soft", "-5medium"1053 const matches = [...score.matchAll(/(-?\d*\.?\d+)(hard|medium|soft)/g)];1054 matches.forEach(match => {1055 components[match[2]] = parseFloat(match[1]);1056 });1057 1058 return components;1059}1060 1061 1062// =============================================================================1063// 10. TAB NAVIGATION HELPERS1064// =============================================================================1065// Functions for switching between tabs programmatically.1066 1067/**1068 * Navigates to Build tab and shows a specific file.1069 *1070 * Used by code-link elements to view source code.1071 *1072 * @param {string} filename - The file to show in the Build tab1073 */1074function showInBuild(filename) {1075 // Switch to Build tab using Bootstrap 5 API1076 const tabEl = document.querySelector('[data-bs-target="#build"]');1077 if (tabEl) {1078 const tab = new bootstrap.Tab(tabEl);1079 tab.show();1080 }1081 1082 // Load the requested file after a short delay to ensure tab is visible1083 setTimeout(() => {1084 loadSourceFile(filename);1085 }, 100);1086}1087 1088/**1089 * Navigates from Build tab to Demo tab.1090 *1091 * Used by "See in Demo" button in the code viewer.1092 */1093function showInDemo() {1094 // Switch to Demo tab using Bootstrap 5 API1095 const tabEl = document.querySelector('[data-bs-target="#demo"]');1096 if (tabEl) {1097 const tab = new bootstrap.Tab(tabEl);1098 tab.show();1099 }1100}1101 1102 1103// =============================================================================1104// 11. BUILD TAB - Source Code Viewer1105// =============================================================================1106// Functions for the source code viewer with syntax highlighting.1107 1108/**1109 * Sets up click handlers for the file navigator.1110 */1111function setupBuildTab() {1112 // File item click handlers1113 $('.file-item').click(function() {1114 const filename = $(this).data('file');1115 if (filename) {1116 // Update active state1117 $('.file-item').removeClass('active');1118 $(this).addClass('active');1119 1120 // Load the file1121 loadSourceFile(filename);1122 }1123 });1124}1125 1126/**1127 * Loads and displays a source file in the code viewer.1128 *1129 * FLOW:1130 * 1. Fetch source code from /source-code/{filename} API1131 * 2. Update the code viewer header with file path1132 * 3. Set the code content and language class1133 * 4. Trigger Prism.js highlighting1134 * 5. If section is provided, find its line number and scroll to it1135 *1136 * RUNTIME LINE DETECTION:1137 * When a section name is provided (e.g., "updateKPIs"), we search the loaded1138 * content for patterns that indicate where that section is defined:1139 * - Python: "def section_name" or "class SectionName"1140 * - JavaScript: "function sectionName" or "sectionName(" or "const sectionName"1141 *1142 * This approach is more robust than hardcoded line numbers because:1143 * - Line numbers change as code is edited1144 * - Different environments might have different line endings1145 * - The search adapts to the actual file content at runtime1146 *1147 * @param {string} filename - The file to load1148 * @param {string} [section] - Optional section/function name to scroll to1149 */1150function loadSourceFile(filename, section = null) {1151 console.log('Loading source file:', filename, section ? `(section: ${section})` : '');1152 currentFile = filename;1153 1154 // Determine language for syntax highlighting based on file extension1155 // Prism.js uses different language identifiers for different file types1156 let language = 'python';1157 if (filename.endsWith('.js')) {1158 language = 'javascript';1159 } else if (filename.endsWith('.html')) {1160 language = 'markup'; // Prism uses 'markup' for HTML1161 }1162 1163 // Update header with file path and appropriate icon1164 const icon = language === 'python' ? 'fab fa-python'1165 : language === 'javascript' ? 'fab fa-js'1166 : 'fab fa-html5';1167 const path = filename.endsWith('.py')1168 ? `src/my_quickstart/${filename}`1169 : `static/${filename}`;1170 1171 $('#currentFilePath').html(`<i class="${icon} me-2"></i>${path}`);1172 1173 // Show loading state while fetching1174 const codeEl = $('#codeContent');1175 codeEl.text('Loading...');1176 1177 // Fetch source code from API1178 // The /source-code/{filename} endpoint returns {filename, content}1179 $.getJSON(`/source-code/${filename}`, function(data) {1180 currentFileContent = data.content || '// File not found';1181 1182 // Update code content in the <code> element1183 codeEl.text(currentFileContent);1184 codeEl.attr('class', `language-${language}`);1185 1186 // Trigger Prism.js syntax highlighting1187 // This transforms plain text into highlighted HTML with line numbers1188 if (typeof Prism !== 'undefined') {1189 Prism.highlightElement(codeEl[0]);1190 }1191 1192 // RUNTIME LINE DETECTION: If a section was requested, find and scroll to it1193 // We do this AFTER Prism highlighting because:1194 // 1. The content needs to be rendered before we can scroll1195 // 2. Prism adds line-numbers-rows elements we use for accurate scrolling1196 if (section) {1197 // Small delay to ensure Prism.js has finished rendering line numbers1198 // Prism's highlightElement is synchronous, but DOM updates need a tick1199 setTimeout(() => {1200 const lineNumber = findSectionLineNumber(currentFileContent, section, language);