CoolFace
Apppublic

blackopsrepl/quickstart-template

sourceHugging Faceapache-2.0updated 9mo agoView on Hugging Face
2likes
app.js1837 linesDownload Raw Back to static
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);

Showing the first 1,200 of 1837 lines. Download the file for the rest.