feat: Add Analysis page, run comparison, notifications, and config editor
Dashboard enhancements:
- Add Analysis page with tabs: Overview, Parameters, Pareto, Correlations, Constraints, Surrogate, Runs
- Add PlotlyCorrelationHeatmap for parameter-objective correlation analysis
- Add PlotlyFeasibilityChart for constraint satisfaction visualization
- Add PlotlySurrogateQuality for FEA vs NN prediction comparison
- Add PlotlyRunComparison for comparing optimization runs within a study
Real-time improvements:
- Replace watchdog file-watching with SQLite database polling for better Windows reliability
- Add DatabasePoller class with 2-second polling interval
- Enhanced WebSocket messages: trial_completed, new_best, pareto_update, progress
Desktop notifications:
- Add useNotifications hook using Web Notifications API
- Add NotificationSettings toggle component
- Notify users when new best solutions are found
Config editor:
- Add PUT /studies/{study_id}/config endpoint with auto-backup
- Add ConfigEditor modal with tabs: General, Variables, Objectives, Settings, JSON
- Prevents editing while optimization is running
Enhanced Pareto visualization:
- Add dark mode styling with transparent backgrounds
- Add stats bar showing Pareto, FEA, NN, and infeasible counts
- Add Pareto front connecting line for 2D view
- Add table showing top 10 Pareto-optimal solutions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
172
atomizer-dashboard/frontend/src/hooks/useNotifications.ts
Normal file
172
atomizer-dashboard/frontend/src/hooks/useNotifications.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
interface NotificationOptions {
|
||||
title: string;
|
||||
body: string;
|
||||
icon?: string;
|
||||
tag?: string;
|
||||
requireInteraction?: boolean;
|
||||
}
|
||||
|
||||
interface UseNotificationsReturn {
|
||||
permission: NotificationPermission | 'unsupported';
|
||||
requestPermission: () => Promise<boolean>;
|
||||
showNotification: (options: NotificationOptions) => void;
|
||||
isEnabled: boolean;
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'atomizer-notifications-enabled';
|
||||
|
||||
export function useNotifications(): UseNotificationsReturn {
|
||||
const [permission, setPermission] = useState<NotificationPermission | 'unsupported'>(
|
||||
typeof Notification !== 'undefined' ? Notification.permission : 'unsupported'
|
||||
);
|
||||
const [isEnabled, setIsEnabledState] = useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
return stored === 'true';
|
||||
});
|
||||
|
||||
// Update permission state when it changes
|
||||
useEffect(() => {
|
||||
if (typeof Notification === 'undefined') {
|
||||
setPermission('unsupported');
|
||||
return;
|
||||
}
|
||||
setPermission(Notification.permission);
|
||||
}, []);
|
||||
|
||||
const requestPermission = useCallback(async (): Promise<boolean> => {
|
||||
if (typeof Notification === 'undefined') {
|
||||
console.warn('Notifications not supported in this browser');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
setPermission('granted');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Notification.permission === 'denied') {
|
||||
setPermission('denied');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await Notification.requestPermission();
|
||||
setPermission(result);
|
||||
return result === 'granted';
|
||||
} catch (error) {
|
||||
console.error('Error requesting notification permission:', error);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setEnabled = useCallback((enabled: boolean) => {
|
||||
setIsEnabledState(enabled);
|
||||
localStorage.setItem(STORAGE_KEY, enabled.toString());
|
||||
}, []);
|
||||
|
||||
const showNotification = useCallback((options: NotificationOptions) => {
|
||||
if (typeof Notification === 'undefined') {
|
||||
console.warn('Notifications not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Notification.permission !== 'granted') {
|
||||
console.warn('Notification permission not granted');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const notification = new Notification(options.title, {
|
||||
body: options.body,
|
||||
icon: options.icon || '/favicon.ico',
|
||||
tag: options.tag,
|
||||
requireInteraction: options.requireInteraction || false,
|
||||
silent: false
|
||||
});
|
||||
|
||||
// Auto close after 5 seconds unless requireInteraction is true
|
||||
if (!options.requireInteraction) {
|
||||
setTimeout(() => notification.close(), 5000);
|
||||
}
|
||||
|
||||
// Focus window on click
|
||||
notification.onclick = () => {
|
||||
window.focus();
|
||||
notification.close();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error showing notification:', error);
|
||||
}
|
||||
}, [isEnabled]);
|
||||
|
||||
return {
|
||||
permission,
|
||||
requestPermission,
|
||||
showNotification,
|
||||
isEnabled,
|
||||
setEnabled
|
||||
};
|
||||
}
|
||||
|
||||
// Notification types for optimization events
|
||||
export interface OptimizationNotification {
|
||||
type: 'new_best' | 'completed' | 'error' | 'milestone';
|
||||
studyName: string;
|
||||
message: string;
|
||||
value?: number;
|
||||
improvement?: number;
|
||||
}
|
||||
|
||||
export function formatOptimizationNotification(notification: OptimizationNotification): NotificationOptions {
|
||||
switch (notification.type) {
|
||||
case 'new_best':
|
||||
return {
|
||||
title: `New Best Found - ${notification.studyName}`,
|
||||
body: notification.improvement
|
||||
? `${notification.message} (${notification.improvement.toFixed(1)}% improvement)`
|
||||
: notification.message,
|
||||
tag: `best-${notification.studyName}`,
|
||||
requireInteraction: false
|
||||
};
|
||||
|
||||
case 'completed':
|
||||
return {
|
||||
title: `Optimization Complete - ${notification.studyName}`,
|
||||
body: notification.message,
|
||||
tag: `complete-${notification.studyName}`,
|
||||
requireInteraction: true
|
||||
};
|
||||
|
||||
case 'error':
|
||||
return {
|
||||
title: `Error - ${notification.studyName}`,
|
||||
body: notification.message,
|
||||
tag: `error-${notification.studyName}`,
|
||||
requireInteraction: true
|
||||
};
|
||||
|
||||
case 'milestone':
|
||||
return {
|
||||
title: `Milestone Reached - ${notification.studyName}`,
|
||||
body: notification.message,
|
||||
tag: `milestone-${notification.studyName}`,
|
||||
requireInteraction: false
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
title: notification.studyName,
|
||||
body: notification.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default useNotifications;
|
||||
Reference in New Issue
Block a user