/** * ============================================================================= * 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 = $(`
  • ${item}
  • `); // 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('

    No tasks in this dataset

    '); return; } // Create the task grid container const grid = $('
    '); // 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: * * * 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 = $(`
    `); // Task name, duration, and remove button const nameRow = $('
    '); nameRow.append($('').text(task.name)); const rightSide = $('
    '); rightSide.append($('').text(`${task.duration}m`)); rightSide.append($(``)); nameRow.append(rightSide); card.append(nameRow); // Required skill (if any) if (task.requiredSkill) { const skillRow = $('
    '); skillRow.append($('').text(task.requiredSkill)); card.append(skillRow); } // Assignment status const assignmentRow = $('
    '); if (isAssigned) { assignmentRow.html(`${task.resource}`); } else { assignmentRow.html('Unassigned'); } 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('

    No resources in this dataset

    '); 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 => `${s}`).join('') : 'No skills'; // Build the card (escaping name for onclick) const escapedName = resource.name.replace(/'/g, "\\'"); const card = $(` `); 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('

    Analyzing...

    '); // Check if we have a score to analyze if (!loadedSchedule) { modalContent.html('

    No data loaded.

    '); 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('

    Failed to analyze score.

    '); 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('

    No constraint data available.

    '); 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 = ''; html += ` `; 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 = ''; } else if (matchCount === 0) { icon = ''; } else { icon = ''; } // Type badge const typeBadge = isHard ? 'hard' : 'soft'; // Score display const scoreDisplay = isHard ? components.hard : components.soft; html += ` `; }); html += '
    Constraint Type Matches Score
    ${icon} ${constraint.name} ${typeBadge} ${matchCount} ${scoreDisplay}
    '; 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(`${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 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 = $('
    '); 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 = $(` `); 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 }; }