Spaces:
Sleeping
Sleeping
| /** | |
| * ============================================================================= | |
| * SOLVERFORGE QUICKSTART TEMPLATE - APPLICATION JAVASCRIPT | |
| * ============================================================================= | |
| * | |
| * This file contains all the client-side logic for the SolverForge quickstart | |
| * template. It implements a "code-link" educational UI that teaches users | |
| * how to build the very interface they're looking at. | |
| * | |
| * FILE STRUCTURE: | |
| * --------------- | |
| * 1. GLOBAL STATE - Variables tracking UI state, loaded data, solving jobs | |
| * 2. INITIALIZATION - Document ready handler and app setup | |
| * 3. AJAX CONFIGURATION - jQuery AJAX setup and HTTP method extensions | |
| * 4. DEMO DATA LOADING - Fetching and selecting sample datasets | |
| * 5. SCHEDULE/SOLUTION LOADING - Getting solution data from backend | |
| * 6. RENDERING - Card-based visualization of tasks and resources | |
| * 7. KPI UPDATES - Key Performance Indicator card updates | |
| * 8. SOLVING OPERATIONS - Start, stop, and poll the solver | |
| * 9. SCORE ANALYSIS - Constraint breakdown modal | |
| * 10. TAB NAVIGATION HELPERS - Programmatic tab switching | |
| * 11. BUILD TAB - Source code viewer with syntax highlighting | |
| * 12. INTERACTIVE CODE FEATURES - Click-to-code navigation | |
| * 13. NOTIFICATIONS - Toast messages for errors and info | |
| * 14. UTILITY FUNCTIONS - Helpers and formatters | |
| * 15. RESOURCE & TASK CRUD - Adding and removing entities dynamically | |
| * 16. CONSTRAINT WEIGHT CONTROLS - Adjusting optimization weights | |
| * | |
| * CUSTOMIZATION GUIDE: | |
| * -------------------- | |
| * When adapting this template for your domain: | |
| * | |
| * 1. renderSolution() - Change task card layout for your entities | |
| * 2. renderResources() - Change resource card layout for your facts | |
| * 3. updateKPIs() - Update metrics shown in KPI cards | |
| * 4. countViolations() - Implement violation detection for your constraints | |
| * | |
| * API ENDPOINTS USED: | |
| * ------------------- | |
| * - GET /demo-data - List available demo datasets | |
| * - GET /demo-data/{id} - Get a specific demo dataset | |
| * - POST /schedules - Start solving (returns job ID) | |
| * - GET /schedules/{jobId} - Get current solution | |
| * - DELETE /schedules/{jobId}- Stop solving | |
| * - PUT /schedules/analyze - Analyze score breakdown | |
| */ | |
| // ============================================================================= | |
| // 1. GLOBAL STATE | |
| // ============================================================================= | |
| // These variables track the application state throughout the session. | |
| // They are modified by various functions and checked to determine UI behavior. | |
| /** | |
| * Interval ID for auto-refreshing the solution while solving. | |
| * Set by setInterval() when solving starts, cleared when solving stops. | |
| * Used to poll the backend for updates every 2 seconds. | |
| * | |
| * @type {number|null} | |
| */ | |
| let autoRefreshIntervalId = null; | |
| /** | |
| * Currently selected demo data ID (e.g., "SMALL", "MEDIUM", "LARGE"). | |
| * Set when user selects from the Data dropdown. | |
| * Used to fetch the initial dataset before solving. | |
| * | |
| * @type {string|null} | |
| */ | |
| let demoDataId = null; | |
| /** | |
| * Current solving job ID (UUID string from the backend). | |
| * Set when solve() successfully starts a job. | |
| * Used to poll for updates and stop solving. | |
| * | |
| * @type {string|null} | |
| */ | |
| let scheduleId = null; | |
| /** | |
| * The currently loaded schedule/solution data. | |
| * Contains the full problem definition and current solution: | |
| * - resources: Array of resource objects (problem facts) | |
| * - tasks: Array of task objects (planning entities) | |
| * - score: HardSoftScore string (e.g., "0hard/-50soft") | |
| * - solverStatus: "NOT_SOLVING" or "SOLVING" | |
| * | |
| * @type {Object|null} | |
| */ | |
| let loadedSchedule = null; | |
| /** | |
| * Currently displayed file in the Build tab code viewer. | |
| * Used to track which file is being shown and for copy functionality. | |
| * | |
| * @type {string} | |
| */ | |
| let currentFile = 'domain.py'; | |
| /** | |
| * Cached source code content for the current file. | |
| * Populated by loadSourceFile() when fetching from API. | |
| * | |
| * @type {string} | |
| */ | |
| let currentFileContent = ''; | |
| // ============================================================================= | |
| // 2. INITIALIZATION | |
| // ============================================================================= | |
| // Application startup code. Sets up event handlers and loads initial data. | |
| /** | |
| * Document ready handler with safe initialization. | |
| * | |
| * PATTERN: Double-initialization | |
| * We use both $(window).on('load') and setTimeout() to ensure initialization | |
| * happens even if some external resources load slowly or fail to fire the | |
| * load event. | |
| * | |
| * This pattern is common in SolverForge quickstarts to handle: | |
| * - Slow CDN responses | |
| * - Browser caching issues | |
| * - Race conditions with external scripts | |
| */ | |
| $(document).ready(function () { | |
| let initialized = false; | |
| /** | |
| * Safe initialization wrapper. | |
| * Ensures initializeApp() is only called once. | |
| */ | |
| function safeInitialize() { | |
| if (!initialized) { | |
| initialized = true; | |
| initializeApp(); | |
| } | |
| } | |
| // Primary: Initialize when all resources (images, scripts) are loaded | |
| $(window).on('load', safeInitialize); | |
| // Fallback: Initialize after short delay if load event doesn't fire | |
| setTimeout(safeInitialize, 100); | |
| }); | |
| /** | |
| * Main initialization function. | |
| * | |
| * Called once when the page is ready. This function: | |
| * 1. Sets up button click handlers | |
| * 2. Configures AJAX defaults | |
| * 3. Loads the demo data list | |
| * 4. Initializes the Build tab code viewer | |
| * 5. Sets up code-link click handlers | |
| * | |
| * CUSTOMIZATION: Add your own initialization code here. | |
| */ | |
| function initializeApp() { | |
| console.log('SolverForge Quickstart Template initializing...'); | |
| // ========================================================================= | |
| // BUTTON CLICK HANDLERS | |
| // ========================================================================= | |
| // Solve button - starts the optimization | |
| // Connected to solve() function which POSTs to /schedules | |
| $("#solveButton").click(function () { | |
| solve(); | |
| }); | |
| // Stop button - terminates solving early | |
| // Connected to stopSolving() which DELETEs /schedules/{id} | |
| $("#stopSolvingButton").click(function () { | |
| stopSolving(); | |
| }); | |
| // Analyze button - shows score breakdown modal | |
| // Connected to analyze() which PUTs to /schedules/analyze | |
| $("#analyzeButton").click(function () { | |
| analyze(); | |
| }); | |
| // ========================================================================= | |
| // AJAX SETUP & DATA LOADING | |
| // ========================================================================= | |
| // Configure jQuery AJAX defaults (headers, methods) | |
| setupAjax(); | |
| // Load the list of available demo datasets | |
| fetchDemoData(); | |
| // ========================================================================= | |
| // BUILD TAB INITIALIZATION | |
| // ========================================================================= | |
| // Set up file navigator click handlers | |
| setupBuildTab(); | |
| // Load the default file (domain.py) | |
| loadSourceFile('domain.py'); | |
| // ========================================================================= | |
| // INTERACTIVE CODE FEATURE INITIALIZATION | |
| // ========================================================================= | |
| // Set up click handlers for code-link elements | |
| setupCodeLinkHandlers(); | |
| console.log('Initialization complete'); | |
| } | |
| // ============================================================================= | |
| // 3. AJAX CONFIGURATION | |
| // ============================================================================= | |
| // jQuery AJAX setup for communicating with the backend REST API. | |
| /** | |
| * Configures jQuery AJAX with proper headers and HTTP method extensions. | |
| * | |
| * WHAT THIS DOES: | |
| * 1. Sets default Content-Type and Accept headers for JSON | |
| * 2. Adds $.put() and $.delete() methods to jQuery | |
| * (jQuery only has $.get() and $.post() by default) | |
| * | |
| * WHY WE NEED THIS: | |
| * RESTful APIs use all HTTP methods (GET, POST, PUT, DELETE). | |
| * The Accept header includes text/plain because job IDs are returned as text. | |
| */ | |
| function setupAjax() { | |
| // Set default headers for all AJAX requests | |
| $.ajaxSetup({ | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Accept': 'application/json,text/plain', // text/plain for job ID | |
| } | |
| }); | |
| // Extend jQuery with PUT and DELETE methods | |
| // These mirror the signature of $.get() and $.post() | |
| jQuery.each(["put", "delete"], function (i, method) { | |
| jQuery[method] = function (url, data, callback, type) { | |
| // Handle optional parameters (data can be omitted) | |
| if (jQuery.isFunction(data)) { | |
| type = type || callback; | |
| callback = data; | |
| data = undefined; | |
| } | |
| return jQuery.ajax({ | |
| url: url, | |
| type: method, | |
| dataType: type, | |
| data: data, | |
| success: callback | |
| }); | |
| }; | |
| }); | |
| } | |
| // ============================================================================= | |
| // 4. DEMO DATA LOADING | |
| // ============================================================================= | |
| // Functions for loading sample datasets from the backend. | |
| /** | |
| * Fetches the list of available demo datasets and populates the dropdown. | |
| * | |
| * FLOW: | |
| * 1. GET /demo-data returns ["SMALL", "MEDIUM", "LARGE"] (or similar) | |
| * 2. For each dataset, create a dropdown menu item | |
| * 3. Auto-select and load the first dataset | |
| * | |
| * CUSTOMIZATION: | |
| * The backend demo_data.py defines what datasets are available. | |
| * Each dataset is a complete Schedule object with resources and tasks. | |
| */ | |
| function fetchDemoData() { | |
| $.get("/demo-data", function (data) { | |
| const dropdown = $("#dataDropdown"); | |
| dropdown.empty(); | |
| // Create a dropdown item for each available dataset | |
| data.forEach(item => { | |
| const menuItem = $(` | |
| <li> | |
| <a class="dropdown-item" href="#" data-dataset="${item}"> | |
| ${item} | |
| </a> | |
| </li> | |
| `); | |
| // Click handler for this dataset | |
| menuItem.find('a').click(function (e) { | |
| e.preventDefault(); | |
| // Update visual selection | |
| dropdown.find('.dropdown-item').removeClass('active'); | |
| $(this).addClass('active'); | |
| // Reset solving state and load new data | |
| scheduleId = null; | |
| demoDataId = item; | |
| // Load and display the selected dataset | |
| refreshSchedule(); | |
| }); | |
| dropdown.append(menuItem); | |
| }); | |
| // Auto-select the first dataset | |
| if (data.length > 0) { | |
| demoDataId = data[0]; | |
| dropdown.find('.dropdown-item').first().addClass('active'); | |
| refreshSchedule(); | |
| } | |
| }).fail(function (xhr, ajaxOptions, thrownError) { | |
| // Handle case where backend is not running or has no data | |
| showNotification("Failed to load demo data. Is the server running?", "danger"); | |
| console.error('Failed to fetch demo data:', thrownError); | |
| }); | |
| } | |
| // ============================================================================= | |
| // 5. SCHEDULE/SOLUTION LOADING | |
| // ============================================================================= | |
| // Functions for fetching and displaying solution data. | |
| /** | |
| * Fetches and displays the current schedule/solution. | |
| * | |
| * LOGIC: | |
| * - If scheduleId is set: GET /schedules/{scheduleId} for solving progress | |
| * - If scheduleId is null: GET /demo-data/{demoDataId} for initial data | |
| * | |
| * WHEN CALLED: | |
| * - When a dataset is selected from the dropdown | |
| * - Every 2 seconds while solving (via setInterval) | |
| * - After stopping solving | |
| */ | |
| function refreshSchedule() { | |
| // Determine which endpoint to call | |
| let path = "/schedules/" + scheduleId; | |
| if (scheduleId === null) { | |
| // No active job - load demo data instead | |
| if (demoDataId === null) { | |
| showNotification("Please select a dataset from the Data dropdown.", "warning"); | |
| return; | |
| } | |
| path = "/demo-data/" + demoDataId; | |
| } | |
| // Fetch the schedule data | |
| $.getJSON(path, function (schedule) { | |
| loadedSchedule = schedule; | |
| renderSchedule(schedule); | |
| }).fail(function (xhr, ajaxOptions, thrownError) { | |
| showNotification("Failed to load schedule data.", "danger"); | |
| console.error('Failed to fetch schedule:', thrownError); | |
| refreshSolvingButtons(false); | |
| }); | |
| } | |
| /** | |
| * Renders the complete schedule/solution to the UI. | |
| * | |
| * UPDATES: | |
| * - Solve/Stop button visibility | |
| * - Spinner animation | |
| * - KPI cards | |
| * - Task cards in the tasks panel | |
| * - Resource cards in the resources panel | |
| * | |
| * @param {Object} schedule - The schedule data from the backend | |
| */ | |
| function renderSchedule(schedule) { | |
| if (!schedule) { | |
| console.error('No schedule data provided to renderSchedule'); | |
| return; | |
| } | |
| console.log('Rendering schedule:', schedule); | |
| // Update solving buttons based on solver status | |
| const isSolving = schedule.solverStatus != null && | |
| schedule.solverStatus !== "NOT_SOLVING"; | |
| refreshSolvingButtons(isSolving); | |
| // Update KPI cards with current metrics | |
| updateKPIs(schedule); | |
| // Render the solution visualization (task cards) | |
| renderSolution(schedule); | |
| // Render the resources panel | |
| renderResources(schedule); | |
| } | |
| // ============================================================================= | |
| // 6. RENDERING - Card-Based Visualization | |
| // ============================================================================= | |
| // Functions that create the visual representation of tasks and resources. | |
| /** | |
| * Renders the tasks panel with card-based layout. | |
| * | |
| * CARD STATES: | |
| * - Default (green border): Task is assigned to a resource | |
| * - .unassigned (orange border): Task has no resource assigned | |
| * - .violation (red border): Task has a constraint violation | |
| * | |
| * CUSTOMIZATION: | |
| * Modify this function to match your domain model: | |
| * - Change what fields are displayed | |
| * - Add domain-specific badges or indicators | |
| * - Implement custom violation detection | |
| * | |
| * @param {Object} schedule - Schedule containing tasks array | |
| */ | |
| function renderSolution(schedule) { | |
| const panel = $("#tasksPanel"); | |
| panel.empty(); | |
| // Update task count badge | |
| const taskCount = schedule.tasks ? schedule.tasks.length : 0; | |
| $("#taskCount").text(taskCount); | |
| // Handle empty state | |
| if (!schedule.tasks || schedule.tasks.length === 0) { | |
| panel.html('<p class="text-muted text-center">No tasks in this dataset</p>'); | |
| return; | |
| } | |
| // Create the task grid container | |
| const grid = $('<div class="task-grid"></div>'); | |
| // Render each task as a card | |
| schedule.tasks.forEach(task => { | |
| const card = createTaskCard(task, schedule); | |
| grid.append(card); | |
| }); | |
| panel.append(grid); | |
| } | |
| /** | |
| * Creates a single task card element. | |
| * | |
| * STRUCTURE: | |
| * <div class="task-card [unassigned|violation] code-link"> | |
| * <div class="task-name">Task Name <duration></div> | |
| * <div class="task-detail">Skill: skill_name</div> | |
| * <div class="task-detail">Assigned: resource_name</div> | |
| * </div> | |
| * | |
| * CUSTOMIZATION: | |
| * Modify this to show your domain-specific fields. | |
| * | |
| * @param {Object} task - The task object | |
| * @param {Object} schedule - The full schedule (for violation checking) | |
| * @returns {jQuery} The task card jQuery element | |
| */ | |
| function createTaskCard(task, schedule) { | |
| // Determine card state | |
| const isAssigned = task.resource != null; | |
| const hasViolation = checkTaskViolation(task, schedule); | |
| // Build CSS classes | |
| let cardClass = 'task-card code-link'; | |
| if (hasViolation) { | |
| cardClass += ' violation'; | |
| } else if (!isAssigned) { | |
| cardClass += ' unassigned'; | |
| } | |
| // Create the card (escaping id for onclick) | |
| const escapedId = task.id.replace(/'/g, "\\'"); | |
| const card = $(`<div class="${cardClass}" data-target="app.js:createTaskCard"></div>`); | |
| // Task name, duration, and remove button | |
| const nameRow = $('<div class="task-name"></div>'); | |
| nameRow.append($('<span></span>').text(task.name)); | |
| const rightSide = $('<div class="d-flex align-items-center gap-2"></div>'); | |
| rightSide.append($('<span class="task-duration"></span>').text(`${task.duration}m`)); | |
| rightSide.append($(`<button class="btn btn-sm btn-outline-danger" onclick="removeTask('${escapedId}', event)" title="Remove Task"><i class="fas fa-minus"></i></button>`)); | |
| nameRow.append(rightSide); | |
| card.append(nameRow); | |
| // Required skill (if any) | |
| if (task.requiredSkill) { | |
| const skillRow = $('<div class="task-detail"></div>'); | |
| skillRow.append($('<span class="skill-tag"></span>').text(task.requiredSkill)); | |
| card.append(skillRow); | |
| } | |
| // Assignment status | |
| const assignmentRow = $('<div class="task-detail"></div>'); | |
| if (isAssigned) { | |
| assignmentRow.html(`<span class="assigned-badge"><i class="fas fa-check me-1"></i>${task.resource}</span>`); | |
| } else { | |
| assignmentRow.html('<span class="unassigned-badge">Unassigned</span>'); | |
| } | |
| card.append(assignmentRow); | |
| return card; | |
| } | |
| /** | |
| * Checks if a task has any constraint violations. | |
| * | |
| * CUSTOMIZATION: | |
| * Implement your domain-specific violation detection here. | |
| * This example checks: | |
| * - Required skill: Is the task assigned to a resource with the required skill? | |
| * | |
| * @param {Object} task - The task to check | |
| * @param {Object} schedule - The schedule containing resources | |
| * @returns {boolean} True if task has a violation | |
| */ | |
| function checkTaskViolation(task, schedule) { | |
| // If not assigned, it's not a violation (just unassigned) | |
| if (!task.resource) { | |
| return false; | |
| } | |
| // Check required skill constraint | |
| if (task.requiredSkill) { | |
| const resource = schedule.resources.find(r => r.name === task.resource); | |
| if (resource) { | |
| // Check if resource has the required skill | |
| const hasSkill = resource.skills && | |
| resource.skills.includes(task.requiredSkill); | |
| if (!hasSkill) { | |
| return true; // Skill violation! | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Renders the resources panel with card-based layout. | |
| * | |
| * CARD STRUCTURE: | |
| * - Resource name | |
| * - Capacity utilization bar (color-coded) | |
| * - Skills list | |
| * | |
| * CUSTOMIZATION: | |
| * Modify this function to match your problem facts. | |
| * | |
| * @param {Object} schedule - Schedule containing resources array | |
| */ | |
| function renderResources(schedule) { | |
| const panel = $("#resourcesPanel"); | |
| panel.empty(); | |
| // Update resource count badge | |
| const resourceCount = schedule.resources ? schedule.resources.length : 0; | |
| $("#resourceCount").text(resourceCount); | |
| // Handle empty state | |
| if (!schedule.resources || schedule.resources.length === 0) { | |
| panel.html('<p class="text-muted text-center">No resources in this dataset</p>'); | |
| return; | |
| } | |
| // Render each resource as a card | |
| schedule.resources.forEach(resource => { | |
| const card = createResourceCard(resource, schedule); | |
| panel.append(card); | |
| }); | |
| } | |
| /** | |
| * Creates a single resource card element. | |
| * | |
| * FEATURES: | |
| * - Capacity bar showing utilization | |
| * - Color-coded: green (<80%), orange (80-100%), red (>100%) | |
| * - Skills displayed as tags | |
| * | |
| * @param {Object} resource - The resource object | |
| * @param {Object} schedule - The schedule (for calculating utilization) | |
| * @returns {jQuery} The resource card jQuery element | |
| */ | |
| function createResourceCard(resource, schedule) { | |
| // Calculate utilization | |
| const totalDuration = schedule.tasks | |
| ? schedule.tasks | |
| .filter(t => t.resource === resource.name) | |
| .reduce((sum, t) => sum + t.duration, 0) | |
| : 0; | |
| const utilization = resource.capacity > 0 | |
| ? (totalDuration / resource.capacity) * 100 | |
| : 0; | |
| // Determine capacity bar color | |
| let fillClass = ''; | |
| if (utilization > 100) { | |
| fillClass = 'danger'; | |
| } else if (utilization > 80) { | |
| fillClass = 'warning'; | |
| } | |
| // Create skills badges HTML | |
| const skillsHtml = resource.skills && resource.skills.length > 0 | |
| ? resource.skills.map(s => `<span class="skill-tag me-1">${s}</span>`).join('') | |
| : '<span class="text-muted small">No skills</span>'; | |
| // Build the card (escaping name for onclick) | |
| const escapedName = resource.name.replace(/'/g, "\\'"); | |
| const card = $(` | |
| <div class="resource-card code-link" data-target="app.js:createResourceCard"> | |
| <div class="resource-header"> | |
| <span class="resource-name">${resource.name}</span> | |
| <div class="d-flex align-items-center gap-2"> | |
| <span class="resource-stats">${totalDuration}/${resource.capacity} min</span> | |
| <button class="btn btn-sm btn-outline-danger" onclick="removeResource('${escapedName}', event)" title="Remove Resource"> | |
| <i class="fas fa-minus"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="capacity-bar"> | |
| <div class="capacity-fill ${fillClass}" style="width: ${Math.min(utilization, 100)}%"></div> | |
| </div> | |
| <div class="skills-list mt-2"> | |
| ${skillsHtml} | |
| </div> | |
| </div> | |
| `); | |
| return card; | |
| } | |
| // ============================================================================= | |
| // 7. KPI UPDATES | |
| // ============================================================================= | |
| // Functions for updating the Key Performance Indicator cards. | |
| /** | |
| * Updates all KPI cards with current metrics. | |
| * | |
| * KPIs DISPLAYED: | |
| * - Total Tasks: Number of planning entities | |
| * - Assigned: Tasks with non-null planning variable | |
| * - Violations: Hard constraint violations | |
| * - Score: Current HardSoftScore | |
| * | |
| * ANIMATION: | |
| * KPI values pulse when they change (using .kpi-pulse class). | |
| * | |
| * CUSTOMIZATION: | |
| * Modify this to show metrics relevant to your domain. | |
| * | |
| * @param {Object} schedule - The schedule data | |
| */ | |
| function updateKPIs(schedule) { | |
| // Calculate metrics | |
| const totalTasks = schedule.tasks ? schedule.tasks.length : 0; | |
| const assignedTasks = schedule.tasks | |
| ? schedule.tasks.filter(t => t.resource != null).length | |
| : 0; | |
| const violations = countViolations(schedule); | |
| const score = schedule.score || '?'; | |
| // Update KPI values with pulse animation | |
| updateKPIValue('#kpiTotalTasks', totalTasks); | |
| updateKPIValue('#kpiAssigned', assignedTasks); | |
| updateKPIValue('#kpiViolations', violations); | |
| updateKPIValue('#kpiScore', formatScore(score)); | |
| } | |
| /** | |
| * Updates a single KPI value with optional pulse animation. | |
| * | |
| * @param {string} selector - jQuery selector for the KPI value element | |
| * @param {string|number} newValue - The new value to display | |
| */ | |
| function updateKPIValue(selector, newValue) { | |
| const el = $(selector); | |
| const oldValue = el.text(); | |
| // Only animate if value changed | |
| if (oldValue !== String(newValue)) { | |
| el.text(newValue); | |
| el.addClass('kpi-pulse'); | |
| setTimeout(() => el.removeClass('kpi-pulse'), 500); | |
| } | |
| } | |
| /** | |
| * Counts the number of hard constraint violations. | |
| * | |
| * CUSTOMIZATION: | |
| * Implement your domain-specific violation counting here. | |
| * This example counts: | |
| * - Required skill violations | |
| * - Capacity violations | |
| * | |
| * @param {Object} schedule - The schedule data | |
| * @returns {number} Number of violations | |
| */ | |
| function countViolations(schedule) { | |
| if (!schedule.tasks || !schedule.resources) { | |
| return 0; | |
| } | |
| let violations = 0; | |
| // Count required skill violations | |
| schedule.tasks.forEach(task => { | |
| if (task.resource && task.requiredSkill) { | |
| const resource = schedule.resources.find(r => r.name === task.resource); | |
| if (resource && resource.skills) { | |
| if (!resource.skills.includes(task.requiredSkill)) { | |
| violations++; | |
| } | |
| } | |
| } | |
| }); | |
| // Count capacity violations | |
| schedule.resources.forEach(resource => { | |
| const totalDuration = schedule.tasks | |
| .filter(t => t.resource === resource.name) | |
| .reduce((sum, t) => sum + t.duration, 0); | |
| if (totalDuration > resource.capacity) { | |
| violations++; | |
| } | |
| }); | |
| return violations; | |
| } | |
| /** | |
| * Formats a score string for display. | |
| * | |
| * EXAMPLES: | |
| * - "0hard/-50soft" -> "0/-50" | |
| * - "-2hard/-15soft" -> "-2/-15" | |
| * - null -> "?" | |
| * | |
| * @param {string|null} score - The score string | |
| * @returns {string} Formatted score | |
| */ | |
| function formatScore(score) { | |
| if (!score || score === '?') { | |
| return '?'; | |
| } | |
| const components = getScoreComponents(score); | |
| // Format as hard/soft | |
| return `${components.hard}/${components.soft}`; | |
| } | |
| // ============================================================================= | |
| // 8. SOLVING OPERATIONS | |
| // ============================================================================= | |
| // Functions for starting, stopping, and monitoring the solver. | |
| /** | |
| * Starts the optimization solver. | |
| * | |
| * FLOW: | |
| * 1. Get current constraint weights from UI sliders | |
| * 2. POST schedule + weights to /schedules | |
| * 3. Backend returns a job ID (UUID) | |
| * 4. Store job ID and start polling for updates | |
| * | |
| * POLLING: | |
| * While solving, refreshSchedule() is called every 2 seconds | |
| * via setInterval(). This polls GET /schedules/{jobId}. | |
| */ | |
| function solve() { | |
| // Check that we have data to solve | |
| if (!loadedSchedule) { | |
| showNotification("No data loaded. Please select a dataset first.", "warning"); | |
| return; | |
| } | |
| // Get constraint weights from UI sliders | |
| const constraintWeights = getConstraintWeights(); | |
| console.log('Constraint weights:', constraintWeights); | |
| // Build the request payload with schedule and weights | |
| const payload = { | |
| ...loadedSchedule, | |
| constraintWeights: constraintWeights | |
| }; | |
| console.log('Starting solver with payload:', payload); | |
| // Send the schedule to the solver | |
| $.post("/schedules", JSON.stringify(payload), function (data) { | |
| // Store the job ID for future requests | |
| scheduleId = data; | |
| console.log('Solving started, job ID:', scheduleId); | |
| // Update UI to show solving state | |
| refreshSolvingButtons(true); | |
| showNotification("Solver started!", "success"); | |
| }).fail(function (xhr, ajaxOptions, thrownError) { | |
| showNotification("Failed to start solving: " + thrownError, "danger"); | |
| console.error('Failed to start solving:', xhr.responseText); | |
| refreshSolvingButtons(false); | |
| }, "text"); | |
| } | |
| /** | |
| * Stops the currently running solver. | |
| * | |
| * FLOW: | |
| * 1. DELETE /schedules/{jobId} | |
| * 2. Backend terminates the solver | |
| * 3. Update UI to idle state | |
| * 4. Refresh to show final solution | |
| */ | |
| function stopSolving() { | |
| if (!scheduleId) { | |
| console.warn('No active solving job to stop'); | |
| return; | |
| } | |
| console.log('Stopping solver, job ID:', scheduleId); | |
| $.delete(`/schedules/${scheduleId}`, function () { | |
| // Update UI to show stopped state | |
| refreshSolvingButtons(false); | |
| // Refresh to get final solution | |
| refreshSchedule(); | |
| showNotification("Solver stopped", "info"); | |
| }).fail(function (xhr, ajaxOptions, thrownError) { | |
| showNotification("Failed to stop solving: " + thrownError, "danger"); | |
| console.error('Failed to stop solving:', xhr.responseText); | |
| }); | |
| } | |
| /** | |
| * Updates the UI to reflect solving/not-solving state. | |
| * | |
| * WHEN SOLVING: | |
| * - Hides Solve button, shows Stop button | |
| * - Shows spinner animation | |
| * - Starts polling for updates every 2 seconds | |
| * | |
| * WHEN NOT SOLVING: | |
| * - Shows Solve button, hides Stop button | |
| * - Hides spinner | |
| * - Stops polling | |
| * | |
| * @param {boolean} solving - Whether solving is currently in progress | |
| */ | |
| function refreshSolvingButtons(solving) { | |
| if (solving) { | |
| // Solving state | |
| $("#solveButton").hide(); | |
| $("#stopSolvingButton").show(); | |
| $("#solvingSpinner").addClass("active"); | |
| // Start polling for updates if not already polling | |
| if (autoRefreshIntervalId == null) { | |
| autoRefreshIntervalId = setInterval(refreshSchedule, 2000); | |
| } | |
| } else { | |
| // Idle state | |
| $("#solveButton").show(); | |
| $("#stopSolvingButton").hide(); | |
| $("#solvingSpinner").removeClass("active"); | |
| // Stop polling | |
| if (autoRefreshIntervalId != null) { | |
| clearInterval(autoRefreshIntervalId); | |
| autoRefreshIntervalId = null; | |
| } | |
| } | |
| } | |
| // ============================================================================= | |
| // 9. SCORE ANALYSIS | |
| // ============================================================================= | |
| // Functions for displaying the score analysis modal. | |
| /** | |
| * Shows the score analysis modal with constraint breakdown. | |
| * | |
| * FLOW: | |
| * 1. Show the modal | |
| * 2. PUT /schedules/analyze with current schedule | |
| * 3. Render constraint breakdown table | |
| * | |
| * DISPLAY: | |
| * - Warning icon for violated hard constraints | |
| * - Check icon for satisfied constraints | |
| * - Match count and score contribution | |
| */ | |
| function analyze() { | |
| // Show the modal | |
| const modal = new bootstrap.Modal("#scoreAnalysisModal"); | |
| modal.show(); | |
| const modalContent = $("#scoreAnalysisContent"); | |
| modalContent.html('<p class="text-center"><i class="fas fa-spinner fa-spin me-2"></i>Analyzing...</p>'); | |
| // Check if we have a score to analyze | |
| if (!loadedSchedule) { | |
| modalContent.html('<p class="text-muted text-center">No data loaded.</p>'); | |
| return; | |
| } | |
| // Update the score label in the modal header | |
| $('#scoreAnalysisScore').text(loadedSchedule.score || '?'); | |
| // Fetch the score analysis from the backend | |
| $.put("/schedules/analyze", JSON.stringify(loadedSchedule), function (scoreAnalysis) { | |
| renderScoreAnalysis(scoreAnalysis, modalContent); | |
| }).fail(function (xhr, ajaxOptions, thrownError) { | |
| modalContent.html('<p class="text-danger text-center">Failed to analyze score.</p>'); | |
| console.error('Failed to analyze score:', xhr.responseText); | |
| }, "json"); | |
| } | |
| /** | |
| * Renders the score analysis table in the modal. | |
| * | |
| * TABLE COLUMNS: | |
| * - Icon: Warning/check status | |
| * - Constraint: Name of the constraint | |
| * - Type: hard/soft | |
| * - Matches: Number of violations | |
| * - Weight: Constraint weight | |
| * - Score: Score contribution | |
| * | |
| * @param {Object} scoreAnalysis - The analysis data from the backend | |
| * @param {jQuery} container - The container element to render into | |
| */ | |
| function renderScoreAnalysis(scoreAnalysis, container) { | |
| container.empty(); | |
| let constraints = scoreAnalysis.constraints || []; | |
| if (constraints.length === 0) { | |
| container.html('<p class="text-muted text-center">No constraint data available.</p>'); | |
| return; | |
| } | |
| // Sort constraints: violated hard constraints first, then by impact | |
| constraints.sort((a, b) => { | |
| let aComponents = getScoreComponents(a.score); | |
| let bComponents = getScoreComponents(b.score); | |
| // Hard constraints with negative score first | |
| if (aComponents.hard < 0 && bComponents.hard >= 0) return -1; | |
| if (aComponents.hard >= 0 && bComponents.hard < 0) return 1; | |
| // Then by absolute hard score | |
| if (Math.abs(aComponents.hard) !== Math.abs(bComponents.hard)) { | |
| return Math.abs(bComponents.hard) - Math.abs(aComponents.hard); | |
| } | |
| // Then by soft score | |
| return Math.abs(bComponents.soft) - Math.abs(aComponents.soft); | |
| }); | |
| // Build the analysis table | |
| let html = '<table class="table table-sm">'; | |
| html += ` | |
| <thead> | |
| <tr> | |
| <th></th> | |
| <th>Constraint</th> | |
| <th>Type</th> | |
| <th>Matches</th> | |
| <th>Score</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| `; | |
| constraints.forEach(constraint => { | |
| const components = getScoreComponents(constraint.score || "0hard/0soft"); | |
| const isHard = components.hard !== 0; | |
| const isViolated = components.hard < 0 || components.soft < 0; | |
| const matchCount = constraint.matches ? constraint.matches.length : 0; | |
| // Status icon | |
| let icon = ''; | |
| if (isHard && components.hard < 0) { | |
| icon = '<i class="fas fa-exclamation-triangle text-danger"></i>'; | |
| } else if (matchCount === 0) { | |
| icon = '<i class="fas fa-check-circle text-success"></i>'; | |
| } else { | |
| icon = '<i class="fas fa-minus-circle text-warning"></i>'; | |
| } | |
| // Type badge | |
| const typeBadge = isHard | |
| ? '<span class="badge bg-danger">hard</span>' | |
| : '<span class="badge bg-success">soft</span>'; | |
| // Score display | |
| const scoreDisplay = isHard ? components.hard : components.soft; | |
| html += ` | |
| <tr> | |
| <td>${icon}</td> | |
| <td>${constraint.name}</td> | |
| <td>${typeBadge}</td> | |
| <td><strong>${matchCount}</strong></td> | |
| <td>${scoreDisplay}</td> | |
| </tr> | |
| `; | |
| }); | |
| html += '</tbody></table>'; | |
| container.html(html); | |
| } | |
| /** | |
| * Parses a score string into its component parts. | |
| * | |
| * EXAMPLES: | |
| * - "0hard/0soft" -> {hard: 0, soft: 0} | |
| * - "-2hard/-15soft" -> {hard: -2, soft: -15} | |
| * | |
| * @param {string} score - The score string to parse | |
| * @returns {Object} Object with hard, medium, soft properties | |
| */ | |
| function getScoreComponents(score) { | |
| let components = {hard: 0, medium: 0, soft: 0}; | |
| if (!score || typeof score !== 'string') { | |
| return components; | |
| } | |
| // Match patterns like "-2hard", "0soft", "-5medium" | |
| const matches = [...score.matchAll(/(-?\d*\.?\d+)(hard|medium|soft)/g)]; | |
| matches.forEach(match => { | |
| components[match[2]] = parseFloat(match[1]); | |
| }); | |
| return components; | |
| } | |
| // ============================================================================= | |
| // 10. TAB NAVIGATION HELPERS | |
| // ============================================================================= | |
| // Functions for switching between tabs programmatically. | |
| /** | |
| * Navigates to Build tab and shows a specific file. | |
| * | |
| * Used by code-link elements to view source code. | |
| * | |
| * @param {string} filename - The file to show in the Build tab | |
| */ | |
| function showInBuild(filename) { | |
| // Switch to Build tab using Bootstrap 5 API | |
| const tabEl = document.querySelector('[data-bs-target="#build"]'); | |
| if (tabEl) { | |
| const tab = new bootstrap.Tab(tabEl); | |
| tab.show(); | |
| } | |
| // Load the requested file after a short delay to ensure tab is visible | |
| setTimeout(() => { | |
| loadSourceFile(filename); | |
| }, 100); | |
| } | |
| /** | |
| * Navigates from Build tab to Demo tab. | |
| * | |
| * Used by "See in Demo" button in the code viewer. | |
| */ | |
| function showInDemo() { | |
| // Switch to Demo tab using Bootstrap 5 API | |
| const tabEl = document.querySelector('[data-bs-target="#demo"]'); | |
| if (tabEl) { | |
| const tab = new bootstrap.Tab(tabEl); | |
| tab.show(); | |
| } | |
| } | |
| // ============================================================================= | |
| // 11. BUILD TAB - Source Code Viewer | |
| // ============================================================================= | |
| // Functions for the source code viewer with syntax highlighting. | |
| /** | |
| * Sets up click handlers for the file navigator. | |
| */ | |
| function setupBuildTab() { | |
| // File item click handlers | |
| $('.file-item').click(function() { | |
| const filename = $(this).data('file'); | |
| if (filename) { | |
| // Update active state | |
| $('.file-item').removeClass('active'); | |
| $(this).addClass('active'); | |
| // Load the file | |
| loadSourceFile(filename); | |
| } | |
| }); | |
| } | |
| /** | |
| * Loads and displays a source file in the code viewer. | |
| * | |
| * FLOW: | |
| * 1. Fetch source code from /source-code/{filename} API | |
| * 2. Update the code viewer header with file path | |
| * 3. Set the code content and language class | |
| * 4. Trigger Prism.js highlighting | |
| * 5. If section is provided, find its line number and scroll to it | |
| * | |
| * RUNTIME LINE DETECTION: | |
| * When a section name is provided (e.g., "updateKPIs"), we search the loaded | |
| * content for patterns that indicate where that section is defined: | |
| * - Python: "def section_name" or "class SectionName" | |
| * - JavaScript: "function sectionName" or "sectionName(" or "const sectionName" | |
| * | |
| * This approach is more robust than hardcoded line numbers because: | |
| * - Line numbers change as code is edited | |
| * - Different environments might have different line endings | |
| * - The search adapts to the actual file content at runtime | |
| * | |
| * @param {string} filename - The file to load | |
| * @param {string} [section] - Optional section/function name to scroll to | |
| */ | |
| function loadSourceFile(filename, section = null) { | |
| console.log('Loading source file:', filename, section ? `(section: ${section})` : ''); | |
| currentFile = filename; | |
| // Determine language for syntax highlighting based on file extension | |
| // Prism.js uses different language identifiers for different file types | |
| let language = 'python'; | |
| if (filename.endsWith('.js')) { | |
| language = 'javascript'; | |
| } else if (filename.endsWith('.html')) { | |
| language = 'markup'; // Prism uses 'markup' for HTML | |
| } | |
| // Update header with file path and appropriate icon | |
| const icon = language === 'python' ? 'fab fa-python' | |
| : language === 'javascript' ? 'fab fa-js' | |
| : 'fab fa-html5'; | |
| const path = filename.endsWith('.py') | |
| ? `src/my_quickstart/${filename}` | |
| : `static/${filename}`; | |
| $('#currentFilePath').html(`<i class="${icon} me-2"></i>${path}`); | |
| // Show loading state while fetching | |
| const codeEl = $('#codeContent'); | |
| codeEl.text('Loading...'); | |
| // Fetch source code from API | |
| // The /source-code/{filename} endpoint returns {filename, content} | |
| $.getJSON(`/source-code/${filename}`, function(data) { | |
| currentFileContent = data.content || '// File not found'; | |
| // Update code content in the <code> element | |
| codeEl.text(currentFileContent); | |
| codeEl.attr('class', `language-${language}`); | |
| // Trigger Prism.js syntax highlighting | |
| // This transforms plain text into highlighted HTML with line numbers | |
| if (typeof Prism !== 'undefined') { | |
| Prism.highlightElement(codeEl[0]); | |
| } | |
| // RUNTIME LINE DETECTION: If a section was requested, find and scroll to it | |
| // We do this AFTER Prism highlighting because: | |
| // 1. The content needs to be rendered before we can scroll | |
| // 2. Prism adds line-numbers-rows elements we use for accurate scrolling | |
| if (section) { | |
| // Small delay to ensure Prism.js has finished rendering line numbers | |
| // Prism's highlightElement is synchronous, but DOM updates need a tick | |
| setTimeout(() => { | |
| const lineNumber = findSectionLineNumber(currentFileContent, section, language); | |
| if (lineNumber > 0) { | |
| console.log(`Found "${section}" at line ${lineNumber}`); | |
| scrollToLine(lineNumber); | |
| } else { | |
| console.warn(`Section "${section}" not found in ${filename}`); | |
| } | |
| }, 50); | |
| } | |
| }).fail(function(xhr, status, error) { | |
| console.error('Failed to load source file:', error); | |
| codeEl.text('// Error loading file: ' + error); | |
| currentFileContent = ''; | |
| }); | |
| } | |
| /** | |
| * Finds the line number where a section (function/class) is defined. | |
| * | |
| * RUNTIME LINE DETECTION EXPLAINED: | |
| * --------------------------------- | |
| * This is the core of our "smart scroll" feature. Instead of hardcoding line | |
| * numbers (which break when code changes), we search the actual file content | |
| * at runtime to find where a function or class is defined. | |
| * | |
| * HOW IT WORKS: | |
| * 1. Split the file content into lines | |
| * 2. Build regex patterns based on the language and section name | |
| * 3. Search each line for a match | |
| * 4. Return the 1-based line number (or 0 if not found) | |
| * | |
| * PATTERNS SEARCHED: | |
| * - Python: "def section_name(" or "class SectionName" | |
| * - JavaScript: "function sectionName(" or "sectionName = function" | |
| * or "const/let/var sectionName" or "sectionName(" at definition | |
| * | |
| * WHY THIS APPROACH: | |
| * - Resilient: Works even as code is edited and line numbers change | |
| * - Flexible: Can find functions, classes, or any named definition | |
| * - Language-aware: Uses appropriate patterns for Python vs JavaScript | |
| * | |
| * TRADE-OFFS: | |
| * - May not find minified code or unusual formatting | |
| * - Could match wrong occurrence if same name appears multiple times | |
| * (we return the FIRST match, which is usually the definition) | |
| * | |
| * @param {string} content - The full file content | |
| * @param {string} sectionName - The function/class name to find | |
| * @param {string} language - The file language ('python', 'javascript', 'markup') | |
| * @returns {number} 1-based line number, or 0 if not found | |
| */ | |
| function findSectionLineNumber(content, sectionName, language) { | |
| if (!content || !sectionName) { | |
| return 0; | |
| } | |
| // Split content into lines for line-by-line search | |
| const lines = content.split('\n'); | |
| // Build search patterns based on language | |
| // We use multiple patterns to catch different definition styles | |
| const patterns = []; | |
| if (language === 'python') { | |
| // Python patterns: | |
| // - "def function_name(" - function definition | |
| // - "class ClassName" - class definition (may or may not have parens) | |
| // - "@decorator" followed by def - decorated functions | |
| patterns.push(new RegExp(`^\\s*def\\s+${sectionName}\\s*\\(`)); | |
| patterns.push(new RegExp(`^\\s*class\\s+${sectionName}\\b`)); | |
| patterns.push(new RegExp(`^\\s*async\\s+def\\s+${sectionName}\\s*\\(`)); | |
| } else if (language === 'javascript') { | |
| // JavaScript patterns: | |
| // - "function functionName(" - classic function declaration | |
| // - "functionName = function" - function expression | |
| // - "const/let/var functionName" - modern declaration | |
| // - "functionName(" in object/class context | |
| // - "async function" variants | |
| patterns.push(new RegExp(`^\\s*function\\s+${sectionName}\\s*\\(`)); | |
| patterns.push(new RegExp(`^\\s*async\\s+function\\s+${sectionName}\\s*\\(`)); | |
| patterns.push(new RegExp(`^\\s*(const|let|var)\\s+${sectionName}\\s*=`)); | |
| patterns.push(new RegExp(`^\\s*${sectionName}\\s*[:=]\\s*(async\\s+)?function`)); | |
| patterns.push(new RegExp(`^\\s*${sectionName}\\s*\\(`)); // Method shorthand | |
| } else { | |
| // HTML/markup: search for id or class attributes | |
| patterns.push(new RegExp(`id=["']${sectionName}["']`)); | |
| patterns.push(new RegExp(`class=["'][^"']*${sectionName}[^"']*["']`)); | |
| } | |
| // Search each line for any of our patterns | |
| for (let i = 0; i < lines.length; i++) { | |
| const line = lines[i]; | |
| for (const pattern of patterns) { | |
| if (pattern.test(line)) { | |
| // Return 1-based line number (lines array is 0-indexed) | |
| return i + 1; | |
| } | |
| } | |
| } | |
| // Fallback: simple substring search for the section name | |
| // This catches cases our patterns missed (e.g., comments mentioning the section) | |
| for (let i = 0; i < lines.length; i++) { | |
| if (lines[i].includes(sectionName)) { | |
| console.log(`Fallback match for "${sectionName}" at line ${i + 1}`); | |
| return i + 1; | |
| } | |
| } | |
| return 0; // Not found | |
| } | |
| /** | |
| * Scrolls the code viewer to a specific line. | |
| * | |
| * Uses multiple approaches to accurately scroll to a line: | |
| * 1. Try to find Prism.js line-numbers-rows spans | |
| * 2. Fall back to measuring line height from rendered code | |
| * | |
| * @param {number} lineNumber - The line to scroll to | |
| */ | |
| function scrollToLine(lineNumber) { | |
| const viewer = $('.code-viewer-body'); | |
| const preEl = viewer.find('pre')[0]; | |
| const codeEl = viewer.find('code')[0]; | |
| if (!preEl || !codeEl) { | |
| console.warn('No code element found for scrolling'); | |
| return; | |
| } | |
| let offset = 0; | |
| // Method 1: Try to use Prism.js line-numbers-rows spans | |
| const lineNumbersRows = preEl.querySelector('.line-numbers-rows'); | |
| if (lineNumbersRows && lineNumbersRows.children.length > 0) { | |
| // Get the height of a single line number span | |
| const firstLineSpan = lineNumbersRows.children[0]; | |
| if (firstLineSpan) { | |
| const lineHeight = firstLineSpan.getBoundingClientRect().height; | |
| offset = (lineNumber - 1) * lineHeight; | |
| console.log(`Using line-numbers-rows method: lineHeight=${lineHeight}px, offset=${offset}px`); | |
| } | |
| } | |
| // Method 2: Fall back to measuring from pre element | |
| if (offset === 0) { | |
| const preStyle = window.getComputedStyle(preEl); | |
| let lineHeight = parseFloat(preStyle.lineHeight); | |
| // If lineHeight is 'normal', compute from font size | |
| if (isNaN(lineHeight) || lineHeight <= 0) { | |
| const fontSize = parseFloat(preStyle.fontSize) || 14; | |
| lineHeight = fontSize * 1.5; | |
| } | |
| offset = (lineNumber - 1) * lineHeight; | |
| console.log(`Using fallback method: lineHeight=${lineHeight}px, offset=${offset}px`); | |
| } | |
| // Add padding offset from the pre element | |
| const preStyle = window.getComputedStyle(preEl); | |
| const paddingTop = parseFloat(preStyle.paddingTop) || 0; | |
| offset += paddingTop; | |
| // Animate scroll with some padding above the target line | |
| const viewerHeight = viewer.height(); | |
| const scrollTarget = Math.max(0, offset - (viewerHeight * 0.2)); | |
| viewer.animate({ | |
| scrollTop: scrollTarget | |
| }, 300); | |
| console.log(`Scrolling to line ${lineNumber}, final offset ${scrollTarget}px`); | |
| // Highlight the line briefly | |
| highlightLine(lineNumber); | |
| } | |
| /** | |
| * Briefly highlights a line in the code viewer. | |
| * | |
| * @param {number} lineNumber - The line to highlight | |
| */ | |
| function highlightLine(lineNumber) { | |
| // Remove any existing highlights | |
| $('.line-highlight').remove(); | |
| const viewer = $('.code-viewer-body'); | |
| const preEl = viewer.find('pre')[0]; | |
| if (!preEl) return; | |
| // Get line height | |
| const lineNumbersRows = preEl.querySelector('.line-numbers-rows'); | |
| let lineHeight = 21; // default | |
| if (lineNumbersRows && lineNumbersRows.children.length > 0) { | |
| lineHeight = lineNumbersRows.children[0].getBoundingClientRect().height; | |
| } | |
| const preStyle = window.getComputedStyle(preEl); | |
| const paddingTop = parseFloat(preStyle.paddingTop) || 0; | |
| // Create highlight element | |
| const highlight = $('<div class="line-highlight"></div>'); | |
| highlight.css({ | |
| position: 'absolute', | |
| left: 0, | |
| right: 0, | |
| top: paddingTop + (lineNumber - 1) * lineHeight, | |
| height: lineHeight, | |
| background: 'rgba(62, 0, 255, 0.15)', | |
| borderLeft: '3px solid #3E00FF', | |
| pointerEvents: 'none', | |
| zIndex: 10 | |
| }); | |
| // Add to pre element (needs relative positioning) | |
| $(preEl).css('position', 'relative').append(highlight); | |
| // Fade out after 2 seconds | |
| setTimeout(() => { | |
| highlight.fadeOut(500, function() { | |
| $(this).remove(); | |
| }); | |
| }, 2000); | |
| } | |
| /** | |
| * Copies the current code to clipboard. | |
| */ | |
| function copyCurrentCode() { | |
| const code = currentFileContent || ''; | |
| if (!code) { | |
| showNotification('No code loaded to copy', 'warning'); | |
| return; | |
| } | |
| navigator.clipboard.writeText(code).then(() => { | |
| showNotification('Code copied to clipboard!', 'success'); | |
| }).catch(err => { | |
| console.error('Failed to copy:', err); | |
| showNotification('Failed to copy code', 'danger'); | |
| }); | |
| } | |
| // ============================================================================= | |
| // 12. INTERACTIVE CODE FEATURES - Click-to-Code Navigation | |
| // ============================================================================= | |
| // The "code-link" feature: clicking UI elements reveals their source code. | |
| /** | |
| * Sets up click handlers for code-link elements. | |
| * | |
| * Elements with class="code-link" and data-target="file:section" | |
| * will navigate to the Build tab and highlight the relevant code. | |
| * | |
| * EXAMPLES: | |
| * - data-target="app.js:updateKPIs" -> app.js, scrolls to updateKPIs function | |
| * - data-target="constraints.py:required_skill" -> constraints.py | |
| */ | |
| function setupCodeLinkHandlers() { | |
| // Main code-link elements | |
| $(document).on('click', '.code-link', function(e) { | |
| // Don't trigger for nested clickable elements | |
| if ($(e.target).closest('.btn').length > 0) { | |
| return; // Let button clicks work normally | |
| } | |
| const target = $(this).data('target'); | |
| if (target) { | |
| navigateToCode(target); | |
| } | |
| }); | |
| // Constraint badges | |
| $(document).on('click', '.constraint-badge', function(e) { | |
| e.stopPropagation(); | |
| const target = $(this).data('target'); | |
| if (target) { | |
| navigateToCode(target); | |
| } | |
| }); | |
| } | |
| /** | |
| * Navigates to a specific code location from an code-link target. | |
| * | |
| * TARGET FORMAT: "filename:section" | |
| * - filename: The file to show (e.g., "app.js", "constraints.py") | |
| * - section: Optional section/function name to scroll to (e.g., "updateKPIs") | |
| * | |
| * RUNTIME LINE DETECTION: | |
| * Unlike static line numbers that would break when code changes, this function | |
| * uses runtime search to find the section. After the file loads from the API, | |
| * we search the actual content for the section name (function/class definition) | |
| * and scroll to where it's found. This keeps code-links working even as code evolves. | |
| * | |
| * @param {string} target - The target in "file:section" format | |
| */ | |
| function navigateToCode(target) { | |
| console.log('Navigating to code:', target); | |
| // Parse target: "filename:section" -> ["filename", "section"] | |
| // The section is optional - if not provided, we just show the file from the top | |
| const [filename, section] = target.split(':'); | |
| // Switch to Build tab using Bootstrap 5 Tab API | |
| // We access the nav-link element and create a Tab instance to show it | |
| const tabEl = document.querySelector('[data-bs-target="#build"]'); | |
| if (tabEl) { | |
| const tab = new bootstrap.Tab(tabEl); | |
| tab.show(); | |
| } | |
| // Load the file after a short delay to ensure tab transition completes | |
| // The delay is needed because Bootstrap's tab.show() is asynchronous | |
| setTimeout(() => { | |
| // Update file navigator active state for visual feedback | |
| $('.file-item').removeClass('active'); | |
| $(`.file-item[data-file="${filename}"]`).addClass('active'); | |
| // Load the file, passing the section name for line scrolling | |
| // If section is provided, loadSourceFile will search for it after loading | |
| loadSourceFile(filename, section); | |
| }, 100); | |
| // Show notification with file and section info | |
| const sectionInfo = section ? ` → ${section}` : ''; | |
| showNotification(`Viewing ${filename}${sectionInfo}`, 'info'); | |
| } | |
| // ============================================================================= | |
| // 13. NOTIFICATIONS | |
| // ============================================================================= | |
| // Toast-style notification messages. | |
| /** | |
| * Shows a notification toast message. | |
| * | |
| * TYPES: | |
| * - 'success': Green checkmark | |
| * - 'danger': Red X | |
| * - 'warning': Yellow warning | |
| * - 'info': Blue info | |
| * | |
| * @param {string} message - The message to display | |
| * @param {string} type - The notification type (success, danger, warning, info) | |
| */ | |
| function showNotification(message, type = 'info') { | |
| const panel = $('#notificationPanel'); | |
| // Create the toast | |
| const toast = $(` | |
| <div class="alert alert-${type} alert-dismissible fade show" role="alert"> | |
| ${message} | |
| <button type="button" class="btn-close" data-bs-dismiss="alert"></button> | |
| </div> | |
| `); | |
| panel.append(toast); | |
| // Auto-dismiss after 5 seconds | |
| setTimeout(() => { | |
| toast.alert('close'); | |
| }, 5000); | |
| } | |
| // ============================================================================= | |
| // 14. UTILITY FUNCTIONS | |
| // ============================================================================= | |
| // Helper functions used throughout the application. | |
| /** | |
| * Escapes HTML special characters to prevent XSS. | |
| * | |
| * @param {string} text - The text to escape | |
| * @returns {string} Escaped text safe for HTML insertion | |
| */ | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // ============================================================================= | |
| // 15. RESOURCE & TASK CRUD OPERATIONS | |
| // ============================================================================= | |
| // Functions for adding and removing resources and tasks dynamically. | |
| /** | |
| * Shows the Add Resource modal dialog. | |
| * | |
| * @param {Event} event - Click event (to stop propagation) | |
| */ | |
| function showAddResourceModal(event) { | |
| event.stopPropagation(); | |
| // Clear previous values | |
| $('#resourceName').val(''); | |
| $('#resourceCapacity').val(100); | |
| $('#resourceSkills').val(''); | |
| // Show modal | |
| const modal = new bootstrap.Modal('#addResourceModal'); | |
| modal.show(); | |
| } | |
| /** | |
| * Adds a new resource to the schedule. | |
| * | |
| * Reads values from the Add Resource modal form and adds | |
| * a new resource to loadedSchedule.resources. | |
| */ | |
| function addResource() { | |
| const name = $('#resourceName').val().trim(); | |
| const capacity = parseInt($('#resourceCapacity').val()) || 100; | |
| const skillsStr = $('#resourceSkills').val().trim(); | |
| const skills = skillsStr ? skillsStr.split(',').map(s => s.trim().toLowerCase()) : []; | |
| // Validate | |
| if (!name) { | |
| showNotification('Please enter a resource name', 'warning'); | |
| return; | |
| } | |
| // Check for duplicate | |
| if (loadedSchedule && loadedSchedule.resources) { | |
| if (loadedSchedule.resources.some(r => r.name === name)) { | |
| showNotification('A resource with this name already exists', 'warning'); | |
| return; | |
| } | |
| } | |
| // Initialize schedule if needed | |
| if (!loadedSchedule) { | |
| loadedSchedule = { resources: [], tasks: [] }; | |
| } | |
| if (!loadedSchedule.resources) { | |
| loadedSchedule.resources = []; | |
| } | |
| // Add the resource | |
| loadedSchedule.resources.push({ | |
| name: name, | |
| capacity: capacity, | |
| skills: skills | |
| }); | |
| // Close modal and re-render | |
| bootstrap.Modal.getInstance('#addResourceModal').hide(); | |
| renderSchedule(loadedSchedule); | |
| showNotification(`Added resource: ${name}`, 'success'); | |
| } | |
| /** | |
| * Removes a resource from the schedule. | |
| * | |
| * @param {string} resourceName - Name of the resource to remove | |
| * @param {Event} event - Click event (to stop propagation) | |
| */ | |
| function removeResource(resourceName, event) { | |
| event.stopPropagation(); | |
| if (!loadedSchedule || !loadedSchedule.resources) { | |
| return; | |
| } | |
| // Remove the resource | |
| loadedSchedule.resources = loadedSchedule.resources.filter(r => r.name !== resourceName); | |
| // Unassign any tasks assigned to this resource | |
| if (loadedSchedule.tasks) { | |
| loadedSchedule.tasks.forEach(task => { | |
| if (task.resource === resourceName) { | |
| task.resource = null; | |
| } | |
| }); | |
| } | |
| // Re-render | |
| renderSchedule(loadedSchedule); | |
| showNotification(`Removed resource: ${resourceName}`, 'info'); | |
| } | |
| /** | |
| * Shows the Add Task modal dialog. | |
| * | |
| * @param {Event} event - Click event (to stop propagation) | |
| */ | |
| function showAddTaskModal(event) { | |
| event.stopPropagation(); | |
| // Clear previous values | |
| $('#taskName').val(''); | |
| $('#taskDuration').val(30); | |
| $('#taskSkill').val(''); | |
| // Show modal | |
| const modal = new bootstrap.Modal('#addTaskModal'); | |
| modal.show(); | |
| } | |
| /** | |
| * Adds a new task to the schedule. | |
| * | |
| * Reads values from the Add Task modal form and adds | |
| * a new task to loadedSchedule.tasks. | |
| */ | |
| function addTask() { | |
| const name = $('#taskName').val().trim(); | |
| const duration = parseInt($('#taskDuration').val()) || 30; | |
| const requiredSkill = $('#taskSkill').val().trim().toLowerCase(); | |
| // Validate | |
| if (!name) { | |
| showNotification('Please enter a task name', 'warning'); | |
| return; | |
| } | |
| // Initialize schedule if needed | |
| if (!loadedSchedule) { | |
| loadedSchedule = { resources: [], tasks: [] }; | |
| } | |
| if (!loadedSchedule.tasks) { | |
| loadedSchedule.tasks = []; | |
| } | |
| // Generate unique ID | |
| const existingIds = loadedSchedule.tasks.map(t => t.id); | |
| let newId = `task-${loadedSchedule.tasks.length + 1}`; | |
| let counter = loadedSchedule.tasks.length + 1; | |
| while (existingIds.includes(newId)) { | |
| counter++; | |
| newId = `task-${counter}`; | |
| } | |
| // Add the task | |
| loadedSchedule.tasks.push({ | |
| id: newId, | |
| name: name, | |
| duration: duration, | |
| requiredSkill: requiredSkill || '', | |
| resource: null | |
| }); | |
| // Close modal and re-render | |
| bootstrap.Modal.getInstance('#addTaskModal').hide(); | |
| renderSchedule(loadedSchedule); | |
| showNotification(`Added task: ${name}`, 'success'); | |
| } | |
| /** | |
| * Removes a task from the schedule. | |
| * | |
| * @param {string} taskId - ID of the task to remove | |
| * @param {Event} event - Click event (to stop propagation) | |
| */ | |
| function removeTask(taskId, event) { | |
| event.stopPropagation(); | |
| if (!loadedSchedule || !loadedSchedule.tasks) { | |
| return; | |
| } | |
| // Find task name for notification | |
| const task = loadedSchedule.tasks.find(t => t.id === taskId); | |
| const taskName = task ? task.name : taskId; | |
| // Remove the task | |
| loadedSchedule.tasks = loadedSchedule.tasks.filter(t => t.id !== taskId); | |
| // Re-render | |
| renderSchedule(loadedSchedule); | |
| showNotification(`Removed task: ${taskName}`, 'info'); | |
| } | |
| // ============================================================================= | |
| // 16. CONSTRAINT WEIGHT CONTROLS | |
| // ============================================================================= | |
| // Functions for adjusting constraint weights via sliders. | |
| /** | |
| * Default constraint weight values. | |
| * Hard constraints default to 100, soft constraints to 50. | |
| */ | |
| const DEFAULT_WEIGHTS = { | |
| RequiredSkill: 100, | |
| ResourceCapacity: 100, | |
| MinimizeDuration: 50, | |
| BalanceLoad: 50 | |
| }; | |
| /** | |
| * Updates the displayed value for a constraint weight slider. | |
| * | |
| * Called by oninput on each slider. | |
| * | |
| * @param {string} constraintName - Name of the constraint (e.g., "RequiredSkill") | |
| */ | |
| function updateWeightDisplay(constraintName) { | |
| const value = $(`#weight${constraintName}`).val(); | |
| $(`#weight${constraintName}Value`).text(value); | |
| } | |
| /** | |
| * Resets all constraint weights to their default values. | |
| */ | |
| function resetConstraintWeights() { | |
| Object.keys(DEFAULT_WEIGHTS).forEach(name => { | |
| $(`#weight${name}`).val(DEFAULT_WEIGHTS[name]); | |
| $(`#weight${name}Value`).text(DEFAULT_WEIGHTS[name]); | |
| }); | |
| showNotification('Constraint weights reset to defaults', 'info'); | |
| } | |
| /** | |
| * Gets the current constraint weights from the sliders. | |
| * | |
| * @returns {Object} Object with constraint names and their weights (0-100) | |
| */ | |
| function getConstraintWeights() { | |
| return { | |
| required_skill: parseInt($('#weightRequiredSkill').val()) || 100, | |
| resource_capacity: parseInt($('#weightResourceCapacity').val()) || 100, | |
| minimize_duration: parseInt($('#weightMinimizeDuration').val()) || 50, | |
| balance_load: parseInt($('#weightBalanceLoad').val()) || 50 | |
| }; | |
| } | |