mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 08:15:59 +00:00
Added dark/light mode theme switcher to dashboard (#435)
This commit is contained in:
1 parent
a99ad802ef
commit
f12547d55a
3 files changed
+336
-52
No files matched your search
@@ -61,6 +61,9 @@ class Dashboard {
|
||||
this.$statsSection = $('#stats-section');
|
||||
this.$refreshStats = $('#refresh-stats');
|
||||
this.$clearStats = $('#clear-stats');
|
||||
this.$themeToggle = $('#theme-toggle');
|
||||
this.$themeIcon = $('#theme-icon');
|
||||
this.$themeText = $('#theme-text');
|
||||
|
||||
this.countChart = null;
|
||||
this.tokensChart = null;
|
||||
@@ -73,6 +76,10 @@ class Dashboard {
|
||||
this.$toggleStats.click(this.toggleStats.bind(this));
|
||||
this.$refreshStats.click(this.loadStats.bind(this));
|
||||
this.$clearStats.click(this.clearStats.bind(this));
|
||||
this.$themeToggle.click(this.toggleTheme.bind(this));
|
||||
|
||||
// initialize theme
|
||||
this.initializeTheme();
|
||||
|
||||
// initialize the application
|
||||
this.loadToolNames().then(function() {
|
||||
@@ -301,6 +308,11 @@ class Dashboard {
|
||||
// Register datalabels plugin
|
||||
Chart.register(ChartDataLabels);
|
||||
|
||||
// Get theme-aware colors
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const textColor = isDark ? '#ffffff' : '#000000';
|
||||
const gridColor = isDark ? '#444' : '#ddd';
|
||||
|
||||
// Tool calls pie chart
|
||||
this.countChart = new Chart(countCtx, {
|
||||
type: 'pie',
|
||||
@@ -313,7 +325,12 @@ class Dashboard {
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
color: textColor
|
||||
}
|
||||
},
|
||||
datalabels: {
|
||||
display: true,
|
||||
color: 'white',
|
||||
@@ -336,7 +353,12 @@ class Dashboard {
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
color: textColor
|
||||
}
|
||||
},
|
||||
datalabels: {
|
||||
display: true,
|
||||
color: 'white',
|
||||
@@ -359,7 +381,12 @@ class Dashboard {
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
color: textColor
|
||||
}
|
||||
},
|
||||
datalabels: {
|
||||
display: true,
|
||||
color: 'white',
|
||||
@@ -395,21 +422,56 @@ class Dashboard {
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: textColor
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
color: textColor
|
||||
},
|
||||
grid: {
|
||||
color: gridColor
|
||||
}
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'left',
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Input Tokens' }
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Input Tokens',
|
||||
color: textColor
|
||||
},
|
||||
ticks: {
|
||||
color: textColor
|
||||
},
|
||||
grid: {
|
||||
color: gridColor
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Output Tokens' },
|
||||
grid: { drawOnChartArea: false }
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Output Tokens',
|
||||
color: textColor
|
||||
},
|
||||
ticks: {
|
||||
color: textColor
|
||||
},
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
color: gridColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,6 +499,135 @@ class Dashboard {
|
||||
$('#stats-summary').html(tableHtml);
|
||||
}
|
||||
|
||||
initializeTheme() {
|
||||
// Check if user has manually set a theme preference
|
||||
const savedTheme = localStorage.getItem('serena-theme');
|
||||
|
||||
if (savedTheme) {
|
||||
// User has manually set a preference, use it
|
||||
this.setTheme(savedTheme);
|
||||
} else {
|
||||
// No manual preference, detect system color scheme
|
||||
this.detectSystemTheme();
|
||||
}
|
||||
|
||||
// Listen for system theme changes
|
||||
this.setupSystemThemeListener();
|
||||
}
|
||||
|
||||
detectSystemTheme() {
|
||||
// Check if system prefers dark mode
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const theme = prefersDark ? 'dark' : 'light';
|
||||
this.setTheme(theme);
|
||||
}
|
||||
|
||||
setupSystemThemeListener() {
|
||||
// Listen for changes in system color scheme
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleSystemThemeChange = (e) => {
|
||||
// Only auto-switch if user hasn't manually set a preference
|
||||
const savedTheme = localStorage.getItem('serena-theme');
|
||||
if (!savedTheme) {
|
||||
const newTheme = e.matches ? 'dark' : 'light';
|
||||
this.setTheme(newTheme);
|
||||
}
|
||||
};
|
||||
|
||||
// Add listener for system theme changes
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
mediaQuery.addListener(handleSystemThemeChange);
|
||||
}
|
||||
}
|
||||
|
||||
toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
|
||||
// When user manually toggles, save their preference
|
||||
localStorage.setItem('serena-theme', newTheme);
|
||||
this.setTheme(newTheme);
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
// Set the theme on the document element
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
|
||||
// Update the toggle button
|
||||
if (theme === 'dark') {
|
||||
this.$themeIcon.text('☀️');
|
||||
this.$themeText.text('Light');
|
||||
} else {
|
||||
this.$themeIcon.text('🌙');
|
||||
this.$themeText.text('Dark');
|
||||
}
|
||||
|
||||
// Update the logo based on theme
|
||||
this.updateLogo(theme);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('serena-theme', theme);
|
||||
|
||||
// Update charts if they exist
|
||||
this.updateChartsTheme();
|
||||
}
|
||||
|
||||
updateLogo(theme) {
|
||||
const logoElement = document.getElementById('serena-logo');
|
||||
if (logoElement) {
|
||||
if (theme === 'dark') {
|
||||
logoElement.src = 'serena-logs-dark-mode.png';
|
||||
} else {
|
||||
logoElement.src = 'serena-logs.png';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateChartsTheme() {
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const textColor = isDark ? '#ffffff' : '#000000';
|
||||
const gridColor = isDark ? '#444' : '#ddd';
|
||||
|
||||
// Update existing charts
|
||||
if (this.countChart) {
|
||||
this.countChart.options.scales.x.ticks.color = textColor;
|
||||
this.countChart.options.scales.y.ticks.color = textColor;
|
||||
this.countChart.options.scales.x.grid.color = gridColor;
|
||||
this.countChart.options.scales.y.grid.color = gridColor;
|
||||
this.countChart.update();
|
||||
}
|
||||
|
||||
if (this.inputChart) {
|
||||
this.inputChart.options.scales.x.ticks.color = textColor;
|
||||
this.inputChart.options.scales.y.ticks.color = textColor;
|
||||
this.inputChart.options.scales.x.grid.color = gridColor;
|
||||
this.inputChart.options.scales.y.grid.color = gridColor;
|
||||
this.inputChart.update();
|
||||
}
|
||||
|
||||
if (this.outputChart) {
|
||||
this.outputChart.options.scales.x.ticks.color = textColor;
|
||||
this.outputChart.options.scales.y.ticks.color = textColor;
|
||||
this.outputChart.options.scales.x.grid.color = gridColor;
|
||||
this.outputChart.options.scales.y.grid.color = gridColor;
|
||||
this.outputChart.update();
|
||||
}
|
||||
|
||||
if (this.tokensChart) {
|
||||
this.tokensChart.options.scales.x.ticks.color = textColor;
|
||||
this.tokensChart.options.scales.y.ticks.color = textColor;
|
||||
this.tokensChart.options.scales.y1.ticks.color = textColor;
|
||||
this.tokensChart.options.scales.x.grid.color = gridColor;
|
||||
this.tokensChart.options.scales.y.grid.color = gridColor;
|
||||
this.tokensChart.options.scales.y1.grid.color = gridColor;
|
||||
this.tokensChart.update();
|
||||
}
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
const self = this;
|
||||
const _shutdown = function () {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -12,21 +13,65 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
|
||||
<script src="dashboard.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
/* Light theme variables */
|
||||
--bg-primary: #f5f5f5;
|
||||
--bg-secondary: #ffffff;
|
||||
--text-primary: #000000;
|
||||
--text-secondary: #333333;
|
||||
--text-muted: #666666;
|
||||
--border-color: #ddd;
|
||||
--btn-primary: #eaa45d;
|
||||
--btn-hover: #dca662;
|
||||
--btn-disabled: #6c757d;
|
||||
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--tool-highlight: #ffff00;
|
||||
--tool-highlight-text: #000000;
|
||||
--log-debug: #808080;
|
||||
--log-info: #000000;
|
||||
--log-warning: #FF8C00;
|
||||
--log-error: #FF0000;
|
||||
--stats-header: #f8f9fa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Dark theme variables */
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #e0e0e0;
|
||||
--text-muted: #b0b0b0;
|
||||
--border-color: #444;
|
||||
--btn-primary: #eaa45d;
|
||||
--btn-hover: #dca662;
|
||||
--btn-disabled: #6c757d;
|
||||
--shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
--tool-highlight: #ffd700;
|
||||
--tool-highlight-text: #000000;
|
||||
--log-debug: #808080;
|
||||
--log-info: #ffffff;
|
||||
--log-warning: #FF8C00;
|
||||
--log-error: #FF0000;
|
||||
--stats-header: #3a3a3a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.log-container {
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
@@ -35,11 +80,18 @@
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.controls {
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@@ -48,58 +100,84 @@
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #eaa45d;
|
||||
background-color: var(--btn-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.btn:hover {
|
||||
background-color: #dca662;
|
||||
background-color: var(--btn-hover);
|
||||
}
|
||||
|
||||
|
||||
.btn:disabled {
|
||||
background-color: #6c757d;
|
||||
background-color: var(--btn-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.theme-toggle .icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.log-debug {
|
||||
color: #808080; /* Gray */
|
||||
color: var(--log-debug);
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: #000000; /* Black */
|
||||
color: var(--log-info);
|
||||
}
|
||||
|
||||
.log-warning {
|
||||
color: #FF8C00; /* Dark Orange */
|
||||
color: var(--log-warning);
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: #FF0000; /* Red */
|
||||
color: var(--log-error);
|
||||
}
|
||||
|
||||
.log-default {
|
||||
color: #000000; /* Black */
|
||||
color: var(--log-info);
|
||||
}
|
||||
|
||||
|
||||
/* Tool name highlighting */
|
||||
.tool-name {
|
||||
background-color: #ffff00; /* Yellow background */
|
||||
background-color: var(--tool-highlight);
|
||||
color: var(--tool-highlight-text);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
.error-message {
|
||||
color: #FF0000;
|
||||
color: var(--log-error);
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
|
||||
.charts-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -108,72 +186,86 @@
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
.chart-group {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.chart-wide {
|
||||
flex: 0 0 100%;
|
||||
min-width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
.chart-group h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
|
||||
.stats-summary {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: var(--shadow);
|
||||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.stats-summary th,
|
||||
.stats-summary td {
|
||||
padding: 10px 20px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
transition: border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.stats-summary th {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--stats-header);
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.stats-summary tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.charts-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.chart-group,
|
||||
.chart-wide {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<img src="serena-logs.png" alt="Serena" style="max-width: 400px; height: auto;">
|
||||
<img id="serena-logo" src="serena-logs.png" alt="Serena" style="max-width: 400px; height: auto;">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="controls">
|
||||
<button id="load-logs" class="btn">Reload Log</button>
|
||||
<button id="shutdown" class="btn">Shutdown Server</button>
|
||||
<button id="toggle-stats" class="btn">Show Stats</button>
|
||||
<div id="theme-toggle" class="theme-toggle" title="Toggle theme">
|
||||
<span class="icon" id="theme-icon">🌙</span>
|
||||
<span id="theme-text">Dark</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-container"></div>
|
||||
@@ -184,14 +276,14 @@
|
||||
<button id="refresh-stats" class="btn">Refresh Stats</button>
|
||||
<button id="clear-stats" class="btn">Clear Stats</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="stats-summary" style="margin-bottom:20px; text-align:center;"></div>
|
||||
<div id="estimator-name" style="text-align:center; margin-bottom:10px;"></div>
|
||||
<div id="no-stats-message" style="text-align:center; color:#666; font-style:italic; display:none;">
|
||||
No tool stats collected. Have you enabled tool stats collection in the configuration?
|
||||
<div id="no-stats-message" style="text-align:center; color:var(--text-muted); font-style:italic; display:none;">
|
||||
No tool stats collected. Have you enabled tool stats collection in the configuration?
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="charts-container">
|
||||
<div class="chart-group">
|
||||
<h3>Tool Calls</h3>
|
||||
@@ -213,9 +305,10 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$(document).ready(function () {
|
||||
const dashboard = new Dashboard();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in new issue
Block a user