From 5cb3bf9a88571ff56026b4bf8810e2afec4d9776 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 31 Oct 2025 17:46:51 +0100 Subject: [PATCH] Dashboard: don't rebuild html if config data hasn't changed --- .serena/project.yml | 1 + src/serena/resources/dashboard/dashboard.js | 740 ++++++++------------ src/serena/resources/dashboard/index.html | 2 +- 3 files changed, 300 insertions(+), 443 deletions(-) diff --git a/.serena/project.yml b/.serena/project.yml index e74ef79f..edba1eeb 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -60,5 +60,6 @@ initial_prompt: "" project_name: "serena" languages: - python +- typescript included_optional_tools: [] encoding: utf-8 diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index e76e665b..f24742bc 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -22,27 +22,22 @@ class LogMessage { highlightToolNames(message, toolNames) { let highlightedMessage = message; - toolNames.forEach(function(toolName) { + toolNames.forEach(function (toolName) { const regex = new RegExp('\\b' + toolName + '\\b', 'gi'); highlightedMessage = highlightedMessage.replace(regex, '' + toolName + ''); }); return highlightedMessage; } - escapeHtml (convertString) { + escapeHtml(convertString) { if (typeof convertString !== 'string') return convertString; const patterns = { - '<' : '<', - '>' : '>', - '&' : '&', - '"' : '"', - '\'' : ''', - '`' : '`' + '<': '<', '>': '>', '&': '&', '"': '"', '\'': ''', '`': '`' }; return convertString.replace(/[<>&"'`]/g, match => patterns[match]); - }; + }; } class Dashboard { @@ -52,6 +47,7 @@ class Dashboard { // Page state this.currentPage = 'overview'; this.configData = null; + this.lastConfigDataJson = null; // Cache for comparison this.jetbrainsMode = false; this.activeProjectName = null; this.languageToRemove = null; @@ -146,7 +142,7 @@ class Dashboard { // Register event handlers this.$copyLogsBtn.click(this.copyLogs.bind(this)); - this.$menuShutdown.click(function(e) { + this.$menuShutdown.click(function (e) { e.preventDefault(); self.shutdown(); }); @@ -170,7 +166,7 @@ class Dashboard { this.$createMemoryCreateBtn.click(this.createMemoryFromModal.bind(this)); this.$createMemoryCancelBtn.click(this.closeCreateMemoryModal.bind(this)); this.$modalCloseCreateMemory.click(this.closeCreateMemoryModal.bind(this)); - this.$createMemoryNameInput.keypress(function(e) { + this.$createMemoryNameInput.keypress(function (e) { if (e.which === 13) { // Enter key e.preventDefault(); self.createMemoryFromModal(); @@ -184,58 +180,58 @@ class Dashboard { this.$modalCloseEditSerenaConfig.click(this.closeEditSerenaConfigModal.bind(this)); // Page navigation - $('[data-page]').click(function(e) { + $('[data-page]').click(function (e) { e.preventDefault(); const page = $(this).data('page'); self.navigateToPage(page); }); // Close menu when clicking outside - $(document).click(function(e) { + $(document).click(function (e) { if (!$(e.target).closest('.header-nav').length) { self.$menuDropdown.hide(); } }); // Close modals when clicking outside - this.$addLanguageModal.click(function(e) { + this.$addLanguageModal.click(function (e) { if ($(e.target).hasClass('modal')) { self.closeLanguageModal(); } }); - this.$removeLanguageModal.click(function(e) { + this.$removeLanguageModal.click(function (e) { if ($(e.target).hasClass('modal')) { self.closeRemoveLanguageModal(); } }); - this.$editMemoryModal.click(function(e) { + this.$editMemoryModal.click(function (e) { if ($(e.target).hasClass('modal')) { self.closeEditMemoryModal(); } }); - this.$deleteMemoryModal.click(function(e) { + this.$deleteMemoryModal.click(function (e) { if ($(e.target).hasClass('modal')) { self.closeDeleteMemoryModal(); } }); - this.$createMemoryModal.click(function(e) { + this.$createMemoryModal.click(function (e) { if ($(e.target).hasClass('modal')) { self.closeCreateMemoryModal(); } }); - this.$editSerenaConfigModal.click(function(e) { + this.$editSerenaConfigModal.click(function (e) { if ($(e.target).hasClass('modal')) { self.closeEditSerenaConfigModal(); } }); // Collapsible sections - $('.collapsible-header').click(function() { + $('.collapsible-header').click(function () { const $header = $(this); const $content = $header.next('.collapsible-content'); const $icon = $header.find('.toggle-icon'); @@ -248,7 +244,7 @@ class Dashboard { this.initializeTheme(); // Add ESC key handler for closing modals - $(document).keydown(function(e) { + $(document).keydown(function (e) { if (e.key === 'Escape' || e.keyCode === 27) { if (self.$addLanguageModal.is(':visible')) { self.closeLanguageModal(); @@ -265,7 +261,7 @@ class Dashboard { }); // Initialize the application - this.loadToolNames().then(function() { + this.loadToolNames().then(function () { // Start on overview page self.loadConfigOverview(); self.startConfigPolling(); @@ -335,21 +331,29 @@ class Dashboard { console.log('Polling for config overview...'); let self = this; $.ajax({ - url: '/get_config_overview', - type: 'GET', - success: function(response) { + url: '/get_config_overview', type: 'GET', success: function (response) { self.failureCount = 0; - self.configData = response; - self.jetbrainsMode = response.jetbrains_mode; - self.activeProjectName = response.active_project.name; - self.displayConfig(response); - self.displayBasicStats(response.tool_stats_summary); - self.displayProjects(response.registered_projects); - self.displayAvailableTools(response.available_tools); - self.displayAvailableModes(response.available_modes); - self.displayAvailableContexts(response.available_contexts); - }, - error: function(xhr, status, error) { + + // Check if the config data has actually changed + const currentConfigJson = JSON.stringify(response); + const hasChanged = self.lastConfigDataJson !== currentConfigJson; + + if (hasChanged) { + console.log('Config has changed, updating display'); + self.lastConfigDataJson = currentConfigJson; + self.configData = response; + self.jetbrainsMode = response.jetbrains_mode; + self.activeProjectName = response.active_project.name; + self.displayConfig(response); + self.displayBasicStats(response.tool_stats_summary); + self.displayProjects(response.registered_projects); + self.displayAvailableTools(response.available_tools); + self.displayAvailableModes(response.available_modes); + self.displayAvailableContexts(response.available_contexts); + } else { + console.log('Config unchanged, skipping display update'); + } + }, error: function (xhr, status, error) { console.error('Error loading config overview:', error); self.failureCount++; if (self.failureCount >= 3) { @@ -362,8 +366,7 @@ class Dashboard { self.$availableToolsDisplay.html('
Error loading tools
'); self.$availableModesDisplay.html('
Error loading modes
'); self.$availableContextsDisplay.html('
Error loading contexts
'); - }, - complete: function() { + }, complete: function () { self.waitingForConfigPollingResult = false; } }); @@ -393,172 +396,172 @@ class Dashboard { let html = '
'; - // Project info - html += '
Active Project:
'; - if (config.active_project.name && config.active_project.path) { - const configPath = config.active_project.path + '/.serena/project.yml'; - html += '
' + config.active_project.name + '
'; - } else { - html += '
' + (config.active_project.name || 'None') + '
'; - } + // Project info + html += '
Active Project:
'; + if (config.active_project.name && config.active_project.path) { + const configPath = config.active_project.path + '/.serena/project.yml'; + html += '
' + config.active_project.name + '
'; + } else { + html += '
' + (config.active_project.name || 'None') + '
'; + } - html += '
Languages:
'; - if (this.jetbrainsMode) { - html += '
Using JetBrains backend
'; - } else { - html += '
'; - if (config.languages && config.languages.length > 0) { - html += '
'; - config.languages.forEach(function(language, index) { - const isRemovable = config.languages.length > 1; - html += '
'; - html += language; - if (isRemovable) { - html += '×'; + html += '
Languages:
'; + if (this.jetbrainsMode) { + html += '
Using JetBrains backend
'; + } else { + html += '
'; + if (config.languages && config.languages.length > 0) { + html += '
'; + config.languages.forEach(function (language, index) { + const isRemovable = config.languages.length > 1; + html += '
'; + html += language; + if (isRemovable) { + html += '×'; + } + html += '
'; + }); + // Add the "Add Language" button inline with language badges (only if active project exists) + if (config.active_project && config.active_project.name) { + // TODO: address after refactoring, it's not awesome to keep depending on state + if (this.isAddingLanguage) { + html += '
'; + } else { + html += ''; + html += ''; } html += '
'; - }); - // Add the "Add Language" button inline with language badges (only if active project exists) - if (config.active_project && config.active_project.name) { - // TODO: address after refactoring, it's not awesome to keep depending on state - if (this.isAddingLanguage) { - html += '
'; - } else { - html += ''; - html += ''; + } else { + html += 'N/A'; } html += '
'; - } else { - html += 'N/A'; } - html += '
'; - } - // Context info - html += '
Context:
'; - html += '
' + config.context.name + '
'; + // Context info + html += '
Context:
'; + html += '
' + config.context.name + '
'; - // Modes info - html += '
Active Modes:
'; - html += '
'; - if (config.modes.length > 0) { - const modeSpans = config.modes.map(function(mode) { - return '' + mode.name + ''; - }); - html += modeSpans.join(', '); - } else { - html += 'None'; - } - html += '
'; - - // File Encoding info - html += '
File Encoding:
'; - html += '
' + (config.encoding || 'N/A') + '
'; - - html += '
'; - - // Active tools - collapsible - html += '
'; - html += '

'; - html += 'Active Tools (' + config.active_tools.length + ')'; - html += '▼'; - html += '

'; - html += '
'; - config.active_tools.forEach(function(tool) { - html += '
' + tool + '
'; - }); - html += '
'; - html += '
'; - - // Available memories - collapsible (show if memories exist or if project exists) - if (config.active_project && config.active_project.name) { - html += '
'; - html += '

'; - const memoryCount = (config.available_memories && config.available_memories.length) || 0; - html += 'Available Memories (' + memoryCount + ')'; - html += '▼'; - html += '

'; - html += '
'; - if (config.available_memories && config.available_memories.length > 0) { - config.available_memories.forEach(function(memory) { - html += '
'; - html += memory; - html += '×'; - html += '
'; + // Modes info + html += '
Active Modes:
'; + html += '
'; + if (config.modes.length > 0) { + const modeSpans = config.modes.map(function (mode) { + return '' + mode.name + ''; }); + html += modeSpans.join(', '); + } else { + html += 'None'; } - // Add Create Memory button - html += ''; + html += '
'; + + // File Encoding info + html += '
File Encoding:
'; + html += '
' + (config.encoding || 'N/A') + '
'; + + html += '
'; + + // Active tools - collapsible + html += '
'; + html += '

'; + html += 'Active Tools (' + config.active_tools.length + ')'; + html += '▼'; + html += '

'; + html += '
'; + config.active_tools.forEach(function (tool) { + html += '
' + tool + '
'; + }); html += '
'; html += '
'; - } - // Configuration help link and edit config button - html += '
'; - html += '
'; - html += '📖 '; - html += 'View Configuration Guide'; - html += '
'; - html += ''; - html += '
'; + // Available memories - collapsible (show if memories exist or if project exists) + if (config.active_project && config.active_project.name) { + html += '
'; + html += '

'; + const memoryCount = (config.available_memories && config.available_memories.length) || 0; + html += 'Available Memories (' + memoryCount + ')'; + html += '▼'; + html += '

'; + html += '
'; + if (config.available_memories && config.available_memories.length > 0) { + config.available_memories.forEach(function (memory) { + html += '
'; + html += memory; + html += '×'; + html += '
'; + }); + } + // Add Create Memory button + html += ''; + html += '
'; + html += '
'; + } - this.$configDisplay.html(html); + // Configuration help link and edit config button + html += '
'; + html += '
'; + html += '📖 '; + html += 'View Configuration Guide'; + html += '
'; + html += ''; + html += '
'; - // Attach event handlers for the dynamically created add language button - $('#add-language-btn').click(this.openLanguageModal.bind(this)); + this.$configDisplay.html(html); - // Attach event handler for edit serena config button - $('#edit-serena-config-btn').click(this.openEditSerenaConfigModal.bind(this)); + // Attach event handlers for the dynamically created add language button + $('#add-language-btn').click(this.openLanguageModal.bind(this)); - // Attach event handlers for language remove buttons - const self = this; - $('.language-remove').click(function(e) { - e.preventDefault(); - e.stopPropagation(); - const language = $(this).data('language'); - self.confirmRemoveLanguage(language); - }); + // Attach event handler for edit serena config button + $('#edit-serena-config-btn').click(this.openEditSerenaConfigModal.bind(this)); - // Attach event handlers for memory items - $('.memory-item').click(function(e) { - e.preventDefault(); - const memoryName = $(this).data('memory'); - self.openEditMemoryModal(memoryName); - }); + // Attach event handlers for language remove buttons + const self = this; + $('.language-remove').click(function (e) { + e.preventDefault(); + e.stopPropagation(); + const language = $(this).data('language'); + self.confirmRemoveLanguage(language); + }); - // Attach event handlers for memory remove buttons - $('.memory-remove').click(function(e) { - e.preventDefault(); - e.stopPropagation(); - const memoryName = $(this).data('memory'); - self.confirmDeleteMemory(memoryName); - }); + // Attach event handlers for memory items + $('.memory-item').click(function (e) { + e.preventDefault(); + const memoryName = $(this).data('memory'); + self.openEditMemoryModal(memoryName); + }); - // Attach event handler for create memory button - $('#create-memory-btn').click(this.openCreateMemoryModal.bind(this)); + // Attach event handlers for memory remove buttons + $('.memory-remove').click(function (e) { + e.preventDefault(); + e.stopPropagation(); + const memoryName = $(this).data('memory'); + self.confirmDeleteMemory(memoryName); + }); - // Re-attach collapsible handler for the newly created tools header - $('#tools-header').click(function() { - const $header = $(this); - const $content = $('#tools-content'); - const $icon = $header.find('.toggle-icon'); + // Attach event handler for create memory button + $('#create-memory-btn').click(this.openCreateMemoryModal.bind(this)); - $content.slideToggle(300); - $icon.toggleClass('expanded'); - }); + // Re-attach collapsible handler for the newly created tools header + $('#tools-header').click(function () { + const $header = $(this); + const $content = $('#tools-content'); + const $icon = $header.find('.toggle-icon'); - // Re-attach collapsible handler for the newly created memories header - $('#memories-header').click(function() { - const $header = $(this); - const $content = $('#memories-content'); - const $icon = $header.find('.toggle-icon'); + $content.slideToggle(300); + $icon.toggleClass('expanded'); + }); - $content.slideToggle(300); - $icon.toggleClass('expanded'); - }); + // Re-attach collapsible handler for the newly created memories header + $('#memories-header').click(function () { + const $header = $(this); + const $content = $('#memories-content'); + const $icon = $header.find('.toggle-icon'); + + $content.slideToggle(300); + $icon.toggleClass('expanded'); + }); } catch (error) { console.error('Error in displayConfig:', error); this.$configDisplay.html('
Error displaying configuration: ' + error.message + '
'); @@ -579,7 +582,7 @@ class Dashboard { const maxCalls = Math.max(...sortedTools.map(tool => stats[tool].num_calls)); let html = ''; - sortedTools.forEach(function(toolName) { + sortedTools.forEach(function (toolName) { const count = stats[toolName].num_calls; const percentage = maxCalls > 0 ? (count / maxCalls * 100) : 0; @@ -602,7 +605,7 @@ class Dashboard { } let html = ''; - projects.forEach(function(project) { + projects.forEach(function (project) { const activeClass = project.is_active ? ' active' : ''; html += '
'; html += '
' + project.name + '
'; @@ -620,7 +623,7 @@ class Dashboard { } let html = ''; - tools.forEach(function(tool) { + tools.forEach(function (tool) { html += '
' + tool.name + '
'; }); @@ -634,7 +637,7 @@ class Dashboard { } let html = ''; - modes.forEach(function(mode) { + modes.forEach(function (mode) { const activeClass = mode.is_active ? ' active' : ''; html += '
' + mode.name + '
'; }); @@ -649,7 +652,7 @@ class Dashboard { } let html = ''; - contexts.forEach(function(context) { + contexts.forEach(function (context) { const activeClass = context.is_active ? ' active' : ''; html += '
' + context.name + '
'; }); @@ -662,16 +665,13 @@ class Dashboard { loadQueuedExecutions() { let self = this; $.ajax({ - url: '/queued_task_executions', - type: 'GET', - success: function(response) { + url: '/queued_task_executions', type: 'GET', success: function (response) { if (response.status === 'success') { self.displayActiveExecutionsQueue(response.queued_executions || []); } else { console.error('Error loading executions:', response.message); } - }, - error: function(xhr, status, error) { + }, error: function (xhr, status, error) { console.error('Error loading executions:', error); self.$activeExecutionQueueDisplay.html('
Error loading executions
'); } @@ -681,9 +681,7 @@ class Dashboard { loadLastExecution() { let self = this; $.ajax({ - url: '/last_execution', - type: 'GET', - success: function(response) { + url: '/last_execution', type: 'GET', success: function (response) { if (response.status === 'success') { if (response.last_execution !== null && response.last_execution.logged) { self.displayLastExecution(response.last_execution); @@ -691,8 +689,7 @@ class Dashboard { } else { console.error('Error loading last execution:', response.message); } - }, - error: function(xhr, status, error) { + }, error: function (xhr, status, error) { console.error('Error loading last execution:', error); self.$lastExecutionDisplay.html('
Error loading last execution
'); } @@ -718,7 +715,7 @@ class Dashboard { let html = '
'; let self = this; - executions.forEach(function(execution) { + executions.forEach(function (execution) { const isRunning = execution.is_running; const logged = execution.logged; @@ -756,7 +753,7 @@ class Dashboard { this.$activeExecutionQueueDisplay.html(html); // Attach event handlers for cancel buttons - $('.execution-cancel-btn').click(function(e) { + $('.execution-cancel-btn').click(function (e) { e.preventDefault(); console.log('Cancel button clicked'); const $item = $(this).closest('.execution-item'); @@ -817,7 +814,7 @@ class Dashboard { let html = '
'; - cancelledExecs.forEach(function(execution) { + cancelledExecs.forEach(function (execution) { const isAbandoned = execution.is_running; html += '
'; @@ -863,13 +860,9 @@ class Dashboard { // Call backend API to cancel the task $.ajax({ - url: '/cancel_task_execution', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({ + url: '/cancel_task_execution', type: 'POST', contentType: 'application/json', data: JSON.stringify({ task_id: executionData.task_id - }), - success: function(response) { + }), success: function (response) { console.log('Cancel task response:', response); if (response.status === 'error') { @@ -882,7 +875,7 @@ class Dashboard { if (response.was_cancelled) { console.log('Task ' + executionData.task_id + ' was successfully cancelled'); // Add to cancelled list (only managed in JS, not persisted) - const alreadyCancelled = self.cancelledExecutions.some(function(exec) { + const alreadyCancelled = self.cancelledExecutions.some(function (exec) { return exec.task_id === executionData.task_id; }); if (!alreadyCancelled) { @@ -893,7 +886,7 @@ class Dashboard { console.log('Execution already in cancelled list'); } } else { - console.log('Task ' + executionData.task_id + ' could not be cancelled (may have already completed). ' + response.message ); + console.log('Task ' + executionData.task_id + ' could not be cancelled (may have already completed). ' + response.message); } // Refresh display regardless self.loadQueuedExecutions(); @@ -901,8 +894,7 @@ class Dashboard { console.error('Unexpected response status:', response.status); alert('Unexpected response from server'); } - }, - error: function(xhr, status, error) { + }, error: function (xhr, status, error) { console.error('AJAX error cancelling task:'); console.error(' Status:', status); console.error(' Error:', error); @@ -930,12 +922,7 @@ class Dashboard { if (typeof text !== 'string') return text; const patterns = { - '<': '<', - '>': '>', - '&': '&', - '"': '"', - "'": ''', - '`': '`' + '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '`': '`' }; return text.replace(/[<>&"'`]/g, match => patterns[match]); @@ -950,13 +937,10 @@ class Dashboard { loadToolNames() { let self = this; return $.ajax({ - url: '/get_tool_names', - type: 'GET', - success: function(response) { + url: '/get_tool_names', type: 'GET', success: function (response) { self.toolNames = response.tool_names || []; console.log('Loaded tool names:', self.toolNames); - }, - error: function(xhr, status, error) { + }, error: function (xhr, status, error) { console.error('Error loading tool names:', error); } }); @@ -998,13 +982,9 @@ class Dashboard { // Make API call $.ajax({ - url: '/get_log_messages', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({ + url: '/get_log_messages', type: 'POST', contentType: 'application/json', data: JSON.stringify({ start_idx: 0 - }), - success: function(response) { + }), success: function (response) { // Clear existing logs self.$logContainer.empty(); @@ -1013,7 +993,7 @@ class Dashboard { // Display each log message if (response.messages && response.messages.length > 0) { - response.messages.forEach(function(message) { + response.messages.forEach(function (message) { self.displayLogMessage(message); }); @@ -1028,11 +1008,9 @@ class Dashboard { // Start periodic polling for new logs self.startPeriodicPolling(); - }, - error: function(xhr, status, error) { + }, error: function (xhr, status, error) { console.error('Error loading logs:', error); - self.$errorContainer.html('
Error loading logs: ' + - (xhr.responseJSON ? xhr.responseJSON.detail : error) + '
'); + self.$errorContainer.html('
Error loading logs: ' + (xhr.responseJSON ? xhr.responseJSON.detail : error) + '
'); } }); } @@ -1041,13 +1019,9 @@ class Dashboard { let self = this; console.log("Polling logs", this.currentMaxIdx); $.ajax({ - url: '/get_log_messages', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({ + url: '/get_log_messages', type: 'POST', contentType: 'application/json', data: JSON.stringify({ start_idx: self.currentMaxIdx + 1 - }), - success: function(response) { + }), success: function (response) { self.failureCount = 0; // Only append new messages if we have any if (response.messages && response.messages.length > 0) { @@ -1060,7 +1034,7 @@ class Dashboard { } // Append new messages - response.messages.forEach(function(message) { + response.messages.forEach(function (message) { self.displayLogMessage(message); }); @@ -1078,8 +1052,7 @@ class Dashboard { // Update window title with active project self.updateTitle(response.active_project); - }, - error: function(xhr, status, error) { + }, error: function (xhr, status, error) { console.error('Error polling for new logs:', error); self.failureCount++; if (self.failureCount >= 3) { @@ -1104,14 +1077,14 @@ class Dashboard { loadStats() { let self = this; - $.when( - $.ajax({ url: '/get_tool_stats', type: 'GET' }), - $.ajax({ url: '/get_token_count_estimator_name', type: 'GET' }) - ).done(function(statsResp, estimatorResp) { + $.when($.ajax({url: '/get_tool_stats', type: 'GET'}), $.ajax({ + url: '/get_token_count_estimator_name', + type: 'GET' + })).done(function (statsResp, estimatorResp) { const stats = statsResp[0].stats; const tokenCountEstimatorName = estimatorResp[0].token_count_estimator_name; self.displayStats(stats, tokenCountEstimatorName); - }).fail(function() { + }).fail(function () { console.error('Error loading stats or estimator name'); }); } @@ -1119,12 +1092,9 @@ class Dashboard { clearStats() { let self = this; $.ajax({ - url: '/clear_tool_stats', - type: 'POST', - success: function() { + url: '/clear_tool_stats', type: 'POST', success: function () { self.loadStats(); - }, - error: function(xhr, status, error) { + }, error: function (xhr, status, error) { console.error('Error clearing stats:', error); } }); @@ -1132,7 +1102,7 @@ class Dashboard { displayStats(stats, tokenCountEstimatorName) { const names = Object.keys(stats); - // If no stats collected + // If no stats collected if (names.length === 0) { // hide summary, charts, estimator name $('#stats-summary').hide(); @@ -1187,27 +1157,18 @@ class Dashboard { // Tool calls pie chart this.countChart = new Chart(countCtx, { - type: 'pie', - data: { - labels: names, - datasets: [{ - data: counts, - backgroundColor: colors + type: 'pie', data: { + labels: names, datasets: [{ + data: counts, backgroundColor: colors }] - }, - options: { + }, options: { plugins: { legend: { - display: true, - labels: { + display: true, labels: { color: textColor } - }, - datalabels: { - display: true, - color: 'white', - font: { weight: 'bold' }, - formatter: (value) => value + }, datalabels: { + display: true, color: 'white', font: {weight: 'bold'}, formatter: (value) => value } } } @@ -1215,27 +1176,18 @@ class Dashboard { // Input tokens pie chart this.inputChart = new Chart(inputCtx, { - type: 'pie', - data: { - labels: names, - datasets: [{ - data: inputTokens, - backgroundColor: colors + type: 'pie', data: { + labels: names, datasets: [{ + data: inputTokens, backgroundColor: colors }] - }, - options: { + }, options: { plugins: { legend: { - display: true, - labels: { + display: true, labels: { color: textColor } - }, - datalabels: { - display: true, - color: 'white', - font: { weight: 'bold' }, - formatter: (value) => value + }, datalabels: { + display: true, color: 'white', font: {weight: 'bold'}, formatter: (value) => value } } } @@ -1243,27 +1195,18 @@ class Dashboard { // Output tokens pie chart this.outputChart = new Chart(outputCtx, { - type: 'pie', - data: { - labels: names, - datasets: [{ - data: outputTokens, - backgroundColor: colors + type: 'pie', data: { + labels: names, datasets: [{ + data: outputTokens, backgroundColor: colors }] - }, - options: { + }, options: { plugins: { legend: { - display: true, - labels: { + display: true, labels: { color: textColor } - }, - datalabels: { - display: true, - color: 'white', - font: { weight: 'bold' }, - formatter: (value) => value + }, datalabels: { + display: true, color: 'white', font: {weight: 'bold'}, formatter: (value) => value } } } @@ -1271,78 +1214,42 @@ class Dashboard { // Combined input/output tokens bar chart this.tokensChart = new Chart(tokensCtx, { - type: 'bar', - data: { - labels: names, - datasets: [ - { - label: 'Input Tokens', - data: inputTokens, - backgroundColor: colors.map(color => color + '80'), // Semi-transparent - borderColor: colors, - borderWidth: 2, - borderSkipped: false, - yAxisID: 'y' - }, - { - label: 'Output Tokens', - data: outputTokens, - backgroundColor: colors, - yAxisID: 'y1' - } - ] - }, - options: { - responsive: true, - plugins: { + type: 'bar', data: { + labels: names, datasets: [{ + label: 'Input Tokens', data: inputTokens, backgroundColor: colors.map(color => color + '80'), // Semi-transparent + borderColor: colors, borderWidth: 2, borderSkipped: false, yAxisID: 'y' + }, { + label: 'Output Tokens', data: outputTokens, backgroundColor: colors, yAxisID: 'y1' + }] + }, options: { + responsive: true, plugins: { legend: { labels: { color: textColor } } - }, - scales: { + }, scales: { x: { ticks: { color: textColor - }, - grid: { + }, grid: { color: gridColor } - }, - y: { - type: 'linear', - display: true, - position: 'left', - beginAtZero: true, - title: { - display: true, - text: 'Input Tokens', + }, y: { + type: 'linear', display: true, position: 'left', beginAtZero: true, title: { + display: true, text: 'Input Tokens', color: textColor + }, ticks: { color: textColor - }, - ticks: { - color: textColor - }, - grid: { + }, grid: { color: gridColor } - }, - y1: { - type: 'linear', - display: true, - position: 'right', - beginAtZero: true, - title: { - display: true, - text: 'Output Tokens', + }, y1: { + type: 'linear', display: true, position: 'right', beginAtZero: true, title: { + display: true, text: 'Output Tokens', color: textColor + }, ticks: { color: textColor - }, - ticks: { - color: textColor - }, - grid: { - drawOnChartArea: false, - color: gridColor + }, grid: { + drawOnChartArea: false, color: gridColor } } } @@ -1351,10 +1258,7 @@ class Dashboard { } generateColors(count) { - const colors = [ - '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', - '#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF6384' - ]; + const colors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF6384']; return Array.from({length: count}, (_, i) => colors[i % colors.length]); } @@ -1533,21 +1437,16 @@ class Dashboard { const self = this; $.ajax({ - url: '/remove_language', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({ + url: '/remove_language', type: 'POST', contentType: 'application/json', data: JSON.stringify({ language: language - }), - success: function(response) { + }), success: function (response) { if (response.status === 'success') { // Reload config to show updated language list self.loadConfigOverview(); } else { alert('Error removing language ' + language + ": " + response.message); } - }, - error: function(xhr, status, error) { + }, error: function (xhr, status, error) { console.error('Error removing language:', error); alert('Error removing language: ' + (xhr.responseJSON ? xhr.responseJSON.message : error)); } @@ -1574,9 +1473,7 @@ class Dashboard { loadAvailableLanguages() { let self = this; $.ajax({ - url: '/get_available_languages', - type: 'GET', - success: function(response) { + url: '/get_available_languages', type: 'GET', success: function (response) { const languages = response.languages || []; // Clear all existing options self.$modalLanguageSelect.empty(); @@ -1587,13 +1484,12 @@ class Dashboard { self.$modalAddBtn.prop('disabled', true); } else { // Add language options - languages.forEach(function(language) { + languages.forEach(function (language) { self.$modalLanguageSelect.append($('