diff --git a/src/App.tsx b/src/App.tsx
index 5ef027692..6b6129c1b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,6 +9,7 @@ import { ConfigProvider } from './config/ConfigService';
import { PythonConverterService } from './PythonConverter/PythonConverterService';
import { Auth } from './services/AuthService';
import { DialogProvider } from './services/DialogService';
+import { Geant4DatasetContextProvider } from './services/Geant4DatasetContextProvider';
import { Geant4LocalWorkerSimulationContextProvider } from './services/Geant4LocalWorkerSimulationContextProvider';
import { KeycloakAuth } from './services/KeycloakAuthService';
import { Loader } from './services/LoaderService';
@@ -102,6 +103,7 @@ function App() {
,
,
,
+ ,
,
,
,
diff --git a/src/Geant4Worker/Geant4DatasetCacheService.ts b/src/Geant4Worker/Geant4DatasetCacheService.ts
new file mode 100644
index 000000000..1bf1769b2
--- /dev/null
+++ b/src/Geant4Worker/Geant4DatasetCacheService.ts
@@ -0,0 +1,277 @@
+/**
+ * Service to detect and manage Geant4 dataset cache status in IndexedDB.
+ * The Emscripten preload scripts store datasets in IndexedDB with:
+ * - Database name: EM_PRELOAD_CACHE
+ * - Store: METADATA (contains package UUIDs and chunk counts)
+ * - Store: PACKAGES (contains the actual data chunks)
+ *
+ * Cache keys are constructed as: metadata/
+ * where PACKAGE_PATH is URL-encoded window.location.pathname + '/'
+ * and PACKAGE_NAME comes from the preload script (e.g., '/memfs/.../G4EMLOW8.6.1.data')
+ *
+ * We detect cached datasets by matching the .data file suffix in IndexedDB keys.
+ */
+
+export const GEANT4_DATASETS = [
+ { name: 'G4EMLOW8.6.1', dataFile: 'G4EMLOW8.6.1.data', approximateSizeMB: 649.079 },
+ { name: 'G4ENSDFSTATE3.0', dataFile: 'G4ENSDFSTATE3.0.data', approximateSizeMB: 1.874 },
+ { name: 'G4NDL4.7.1', dataFile: 'G4NDL4.7.1.data', approximateSizeMB: 1082.079 },
+ { name: 'G4PARTICLEXS4.1', dataFile: 'G4PARTICLEXS4.1.data', approximateSizeMB: 23.533 },
+ { name: 'G4SAIDDATA2.0', dataFile: 'G4SAIDDATA2.0.data', approximateSizeMB: 0.106 },
+ {
+ name: 'PhotonEvaporation6.1',
+ dataFile: 'PhotonEvaporation6.1.data',
+ approximateSizeMB: 39.609
+ }
+] as const;
+
+export const TOTAL_DATASET_SIZE_MB = GEANT4_DATASETS.reduce(
+ (sum, ds) => sum + ds.approximateSizeMB,
+ 0
+);
+
+const DB_NAME = 'EM_PRELOAD_CACHE';
+const DB_VERSION = 1;
+const METADATA_STORE_NAME = 'METADATA';
+const PACKAGES_STORE_NAME = 'PACKAGES';
+
+export interface DatasetCacheStatus {
+ name: string;
+ isCached: boolean;
+ approximateSizeMB: number;
+}
+
+export interface CacheStatusResult {
+ datasets: Record;
+ cachedCount: number;
+ totalCount: number;
+ estimatedCachedSizeMB: number;
+ estimatedTotalSizeMB: number;
+}
+
+export interface StorageEstimate {
+ usedMB: number;
+ quotaMB: number;
+ percentUsed: number;
+}
+
+/**
+ * Opens the IndexedDB database used by Emscripten for caching
+ */
+async function openDatabase(): Promise {
+ return new Promise((resolve, reject) => {
+ if (typeof indexedDB === 'undefined') {
+ reject('IndexedDB is not supported in this environment');
+
+ return;
+ }
+
+ const openRequest = indexedDB.open(DB_NAME, DB_VERSION);
+
+ openRequest.onupgradeneeded = event => {
+ const db = (event.target as IDBOpenDBRequest).result;
+ // Create stores if they don't exist (they won't have data anyway)
+
+ if (!db.objectStoreNames.contains(PACKAGES_STORE_NAME)) {
+ db.createObjectStore(PACKAGES_STORE_NAME);
+ }
+
+ if (!db.objectStoreNames.contains(METADATA_STORE_NAME)) {
+ db.createObjectStore(METADATA_STORE_NAME);
+ }
+ };
+
+ openRequest.onsuccess = event => {
+ const db = (event.target as IDBOpenDBRequest).result;
+ resolve(db);
+ };
+
+ openRequest.onerror = () => {
+ reject('Failed to open IndexedDB database');
+ };
+ });
+}
+
+/**
+ * Checks if a specific dataset is cached in IndexedDB by looking for any key ending with the dataFile
+ * The cache keys end with the .data filename (e.g., 'G4EMLOW8.6.1.data')
+ */
+async function checkDatasetCachedByDataFile(
+ db: IDBDatabase,
+ dataFile: string,
+ allMetadataKeys: string[]
+): Promise {
+ // Find any key that ends with our dataFile (after removing 'metadata/' prefix)
+ const matchingKey = allMetadataKeys.find(key => {
+ // Keys are stored as 'metadata/'
+ // where PACKAGE_NAME ends with the dataFile (e.g., '/memfs/.../G4EMLOW8.6.1.data')
+ const keyWithoutPrefix = key.replace(/^metadata\//, '');
+ // The keyWithoutPrefix might be URL-encoded, so try both
+ const decoded = decodeURIComponent(keyWithoutPrefix);
+
+ return keyWithoutPrefix.endsWith(dataFile) || decoded.endsWith(dataFile);
+ });
+
+ if (matchingKey) {
+ const result = await getMetadataValue(db, matchingKey);
+
+ if (result && result.uuid) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Get the metadata value for a specific key
+ */
+async function getMetadataValue(
+ db: IDBDatabase,
+ key: string
+): Promise<{ uuid: string; chunkCount: number } | null> {
+ return new Promise(resolve => {
+ try {
+ const transaction = db.transaction([METADATA_STORE_NAME], 'readonly');
+ const metadata = transaction.objectStore(METADATA_STORE_NAME);
+ const getRequest = metadata.get(key);
+
+ getRequest.onsuccess = () => {
+ resolve(getRequest.result || null);
+ };
+
+ getRequest.onerror = () => {
+ resolve(null);
+ };
+ } catch {
+ resolve(null);
+ }
+ });
+}
+
+/**
+ * Lists all keys in the METADATA store for debugging
+ */
+async function listAllMetadataKeys(db: IDBDatabase): Promise {
+ return new Promise(resolve => {
+ try {
+ const transaction = db.transaction([METADATA_STORE_NAME], 'readonly');
+ const metadata = transaction.objectStore(METADATA_STORE_NAME);
+ const getAllKeysRequest = metadata.getAllKeys();
+
+ getAllKeysRequest.onsuccess = () => {
+ const keys = getAllKeysRequest.result as string[];
+ resolve(keys);
+ };
+
+ getAllKeysRequest.onerror = () => {
+ resolve([]);
+ };
+ } catch (error) {
+ resolve([]);
+ }
+ });
+}
+
+/**
+ * Checks the cache status of all Geant4 datasets
+ */
+export async function checkAllDatasetsCacheStatus(): Promise {
+ const db = await openDatabase();
+
+ if (!db) {
+ return {
+ datasets: Object.fromEntries(
+ GEANT4_DATASETS.map(ds => [
+ ds.name,
+ {
+ name: ds.name,
+ isCached: false,
+ approximateSizeMB: ds.approximateSizeMB
+ }
+ ])
+ ),
+ cachedCount: 0,
+ totalCount: GEANT4_DATASETS.length,
+ estimatedCachedSizeMB: 0,
+ estimatedTotalSizeMB: TOTAL_DATASET_SIZE_MB
+ };
+ }
+
+ try {
+ // First, list all metadata keys for debugging
+ const allKeys = await listAllMetadataKeys(db);
+
+ const datasetStatuses: Record = await Promise.all(
+ GEANT4_DATASETS.map(async ds => {
+ const isCached = await checkDatasetCachedByDataFile(db, ds.dataFile, allKeys);
+
+ return [
+ ds.name,
+ {
+ name: ds.name,
+ isCached,
+ approximateSizeMB: ds.approximateSizeMB
+ }
+ ] as const;
+ })
+ ).then(entries => Object.fromEntries(entries));
+
+ const cachedDatasets = Object.values(datasetStatuses).filter(ds => ds.isCached);
+
+ const result: CacheStatusResult = {
+ datasets: datasetStatuses,
+ cachedCount: cachedDatasets.length,
+ totalCount: GEANT4_DATASETS.length,
+ estimatedCachedSizeMB: cachedDatasets.reduce(
+ (sum, ds) => sum + ds.approximateSizeMB,
+ 0
+ ),
+ estimatedTotalSizeMB: TOTAL_DATASET_SIZE_MB
+ };
+
+ return result;
+ } finally {
+ db.close();
+ }
+}
+
+/**
+ * Gets browser storage estimate using the Storage API
+ */
+export async function getStorageEstimate(): Promise {
+ if (!navigator.storage || !navigator.storage.estimate) {
+ return null;
+ }
+
+ try {
+ const estimate = await navigator.storage.estimate();
+ const usedMB = (estimate.usage ?? 0) / (1024 * 1024);
+ const quotaMB = (estimate.quota ?? 0) / (1024 * 1024);
+
+ return {
+ usedMB,
+ quotaMB,
+ percentUsed: quotaMB > 0 ? (usedMB / quotaMB) * 100 : 0
+ };
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Clears all cached Geant4 datasets from IndexedDB
+ */
+export async function clearDatasetCache(): Promise {
+ return new Promise(resolve => {
+ if (typeof indexedDB === 'undefined') {
+ return resolve(false);
+ }
+
+ const deleteRequest = indexedDB.deleteDatabase(DB_NAME);
+
+ deleteRequest.onsuccess = () => resolve(true);
+ deleteRequest.onerror = () => resolve(false);
+ deleteRequest.onblocked = () => resolve(false);
+ });
+}
diff --git a/src/Geant4Worker/Geant4DatasetDownloadManager.ts b/src/Geant4Worker/Geant4DatasetDownloadManager.ts
deleted file mode 100644
index f8992242f..000000000
--- a/src/Geant4Worker/Geant4DatasetDownloadManager.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-// Additional credits:
-// - @kmichalik
-
-import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
-
-import Geant4Worker from './Geant4Worker';
-
-export enum DownloadManagerStatus {
- IDLE,
- WORKING,
- FINISHED,
- ERROR
-}
-
-export enum DatasetDownloadStatus {
- IDLE,
- DOWNLOADING,
- PROCESSING,
- DONE
-}
-
-const statusTypeMap: Record = {
- downloading: DatasetDownloadStatus.DOWNLOADING,
- preparing: DatasetDownloadStatus.PROCESSING,
- done: DatasetDownloadStatus.DONE
-};
-
-export interface DatasetStatus {
- name: string;
- status: DatasetDownloadStatus;
- done?: number;
- total?: number;
-}
-
-async function fetchProgress(
- worker: Geant4Worker,
- setDatasetStates: Dispatch>>
-) {
- if (!worker.getIsInitialized()) return;
-
- const progress = await worker.pollDatasetProgress();
-
- if (progress) {
- const newDatasetStates: Record = {};
-
- for (const [datasetName, datasetProgress] of Object.entries(progress)) {
- let status = statusTypeMap[datasetProgress.stage] ?? DatasetDownloadStatus.IDLE;
-
- newDatasetStates[datasetName] = {
- name: datasetName,
- status,
- done: Math.floor(datasetProgress.progress * 100),
- total: 100
- };
- }
-
- setDatasetStates(prev => ({ ...prev, ...newDatasetStates }));
- }
-}
-
-type StartDownloadArgs = {
- worker: Geant4Worker;
- managerState: DownloadManagerStatus;
- setManagerState: Dispatch>;
- setDatasetStates: Dispatch>>;
- setIdle: Dispatch>;
-};
-
-function startDownload({
- worker,
- managerState,
- setManagerState,
- setDatasetStates,
- setIdle
-}: StartDownloadArgs) {
- if (managerState !== DownloadManagerStatus.IDLE || !worker.getIsInitialized()) {
- return;
- }
-
- const loadDepsPromise = worker.loadDeps();
-
- const interval = setInterval(async () => {
- await fetchProgress(worker, setDatasetStates);
- }, 500);
-
- loadDepsPromise
- .then(async () => {
- clearInterval(interval);
-
- await fetchProgress(worker, setDatasetStates);
-
- setManagerState(DownloadManagerStatus.FINISHED);
- worker.destroy();
- })
- .catch(error => {
- console.error('Dataset download error:', error);
- setManagerState(DownloadManagerStatus.ERROR);
- clearInterval(interval);
- });
- setManagerState(DownloadManagerStatus.WORKING);
- setIdle(false);
-}
-
-export function useDatasetDownloadManager() {
- const [managerState, setManagerState] = useState(
- DownloadManagerStatus.IDLE
- );
- const [datasetStates, setDatasetStates] = useState>({});
- const [idle, setIdle] = useState(false);
- const [worker] = useState(new Geant4Worker());
- const initCalledRef = useRef(false);
-
- useEffect(() => {
- if (initCalledRef.current) return;
- worker
- .init()
- .then(() => setIdle(true))
- .catch(error => {
- setManagerState(DownloadManagerStatus.ERROR);
- console.error('Failed to initialize Geant4 worker for dataset download:', error);
- });
- initCalledRef.current = true;
- }, [worker]);
-
- const startDownloadSimple = useCallback(() => {
- if (!idle) return;
-
- startDownload({ worker, managerState, setManagerState, setDatasetStates, setIdle });
- }, [worker, idle, managerState]);
-
- return {
- managerState,
- datasetStates: Object.values(datasetStates),
- startDownload: startDownloadSimple
- };
-}
diff --git a/src/Geant4Worker/Geant4DatasetManager.ts b/src/Geant4Worker/Geant4DatasetManager.ts
new file mode 100644
index 000000000..7f4be446b
--- /dev/null
+++ b/src/Geant4Worker/Geant4DatasetManager.ts
@@ -0,0 +1,243 @@
+// Additional credits:
+// - @kmichalik
+
+import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
+
+import {
+ checkAllDatasetsCacheStatus,
+ clearDatasetCache,
+ GEANT4_DATASETS,
+ getStorageEstimate,
+ StorageEstimate,
+ TOTAL_DATASET_SIZE_MB
+} from './Geant4DatasetCacheService';
+import Geant4Worker from './Geant4Worker';
+
+export enum DownloadManagerStatus {
+ IDLE,
+ WORKING,
+ FINISHED,
+ ERROR
+}
+
+export enum DatasetDownloadStatus {
+ IDLE,
+ DOWNLOADING,
+ PROCESSING,
+ DONE
+}
+
+const statusTypeMap: Record = {
+ downloading: DatasetDownloadStatus.DOWNLOADING,
+ preparing: DatasetDownloadStatus.PROCESSING,
+ done: DatasetDownloadStatus.DONE
+};
+
+export interface DatasetStatus {
+ name: string;
+ status: DatasetDownloadStatus;
+ done?: number;
+ total?: number;
+ totalSizeMB?: number;
+ cached?: boolean;
+}
+
+async function fetchProgress(
+ worker: Geant4Worker,
+ setDatasetStates: Dispatch>>
+) {
+ if (!worker.getIsInitialized()) return;
+
+ const progress = await worker.pollDatasetProgress();
+
+ if (progress) {
+ setDatasetStates(prev => {
+ const newStates: Record = { ...prev };
+
+ for (const [datasetName, datasetProgress] of Object.entries(progress)) {
+ let status = statusTypeMap[datasetProgress.stage] ?? DatasetDownloadStatus.IDLE;
+
+ newStates[datasetName] = {
+ ...newStates[datasetName],
+ name: datasetName,
+ status,
+ done: Math.floor(datasetProgress.progress * 100),
+ total: 100
+ };
+ }
+
+ return newStates;
+ });
+ }
+}
+
+type StartDownloadArgs = {
+ worker: Geant4Worker;
+ managerState: DownloadManagerStatus;
+ setManagerState: Dispatch>;
+ setDatasetStates: Dispatch>>;
+ setIdle: Dispatch>;
+};
+
+function startDownload({
+ worker,
+ managerState,
+ setManagerState,
+ setDatasetStates,
+ setIdle
+}: StartDownloadArgs) {
+ if (managerState !== DownloadManagerStatus.IDLE || !worker.getIsInitialized()) {
+ return;
+ }
+
+ const loadDepsPromise = worker.loadDeps();
+
+ const interval = setInterval(async () => {
+ await fetchProgress(worker, setDatasetStates);
+ }, 500);
+
+ loadDepsPromise
+ .then(async () => {
+ clearInterval(interval);
+
+ await fetchProgress(worker, setDatasetStates);
+
+ setManagerState(DownloadManagerStatus.FINISHED);
+ worker.destroy();
+ })
+ .catch(error => {
+ console.error('Dataset download error:', error);
+ setManagerState(DownloadManagerStatus.ERROR);
+ clearInterval(interval);
+ });
+ setManagerState(DownloadManagerStatus.WORKING);
+ setIdle(false);
+}
+
+export interface UseDatasetManagerResult {
+ managerState: DownloadManagerStatus;
+ datasetStatus: Record;
+ storageEstimate: StorageEstimate | null;
+ isLoading: boolean;
+ cachedCount: number;
+ totalCount: number;
+ downloadSizeNeededMB: number;
+ startDownload: () => void;
+ refresh: () => Promise;
+ clearCache: () => Promise;
+}
+
+export function useDatasetManager(): UseDatasetManagerResult {
+ const [managerState, setManagerState] = useState(
+ DownloadManagerStatus.IDLE
+ );
+ const [datasetStates, setDatasetStates] = useState>({});
+ const [idle, setIdle] = useState(false);
+ const [worker] = useState(new Geant4Worker());
+ const initCalledRef = useRef(false);
+
+ const [storageEstimate, setStorageEstimate] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [cachedCount, setCachedCount] = useState(0);
+ const [totalCount, setTotalCount] = useState(0);
+ const [downloadSizeNeededMB, setDownloadSizeNeededMB] = useState(0);
+
+ const refresh = useCallback(async () => {
+ setIsLoading(true);
+
+ try {
+ const [status, storage] = await Promise.all([
+ checkAllDatasetsCacheStatus(),
+ getStorageEstimate()
+ ]);
+
+ const cachedCount = status?.cachedCount ?? 0;
+ const totalCount = status?.totalCount ?? 0;
+ const downloadSizeNeededMB =
+ cachedCount === totalCount
+ ? 0
+ : TOTAL_DATASET_SIZE_MB - (status?.estimatedCachedSizeMB ?? 0);
+
+ setCachedCount(cachedCount);
+ setTotalCount(totalCount);
+ setDownloadSizeNeededMB(downloadSizeNeededMB);
+ setStorageEstimate(storage);
+ setDatasetStates(prev => {
+ const newStates: Record = { ...prev };
+
+ for (const ds of GEANT4_DATASETS) {
+ newStates[ds.name] = {
+ ...newStates[ds.name],
+
+ name: ds.name,
+ status: newStates[ds.name]?.status ?? DatasetDownloadStatus.IDLE,
+ totalSizeMB: ds.approximateSizeMB,
+ cached: status.datasets[ds.name]?.isCached ?? false
+ };
+ }
+
+ return newStates;
+ });
+ } catch (error) {
+ console.error('Failed to check dataset cache status:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ const clearCache = useCallback(async () => {
+ const success = await clearDatasetCache();
+
+ if (success) {
+ if (!worker.getIsInitialized()) {
+ await worker.init();
+ }
+
+ setIdle(true);
+ setManagerState(DownloadManagerStatus.IDLE);
+ setDatasetStates({});
+
+ await refresh();
+ }
+
+ return success;
+ }, [refresh, worker]);
+
+ useEffect(() => {
+ refresh();
+ }, [refresh]);
+
+ useEffect(() => {
+ if (initCalledRef.current) return;
+ worker
+ .init()
+ .then(() => {
+ setIdle(true);
+ })
+ .catch(error => {
+ setManagerState(DownloadManagerStatus.ERROR);
+ console.error('Failed to initialize Geant4 worker for dataset download:', error);
+ });
+ initCalledRef.current = true;
+ }, [worker]);
+
+ const startDownloadSimple = useCallback(() => {
+ if (!idle) return;
+ if (!worker.getIsInitialized()) return;
+
+ startDownload({ worker, managerState, setManagerState, setDatasetStates, setIdle });
+ }, [worker, idle, managerState]);
+
+ return {
+ managerState,
+ datasetStatus: datasetStates,
+ storageEstimate,
+ isLoading,
+ cachedCount,
+ totalCount,
+ downloadSizeNeededMB,
+ refresh,
+ clearCache,
+ startDownload: startDownloadSimple
+ };
+}
diff --git a/src/Geant4Worker/Geant4Worker.ts b/src/Geant4Worker/Geant4Worker.ts
index 2ebe3b1fa..1961241ab 100644
--- a/src/Geant4Worker/Geant4Worker.ts
+++ b/src/Geant4Worker/Geant4Worker.ts
@@ -157,6 +157,8 @@ export default class Geant4Worker {
destroy() {
this.worker?.terminate();
this.worker = undefined;
+ this.isInitialized = false;
+ this.depsLoaded = false;
}
async loadDeps() {
diff --git a/src/Geant4Worker/Geant4WorkerCallbacks.ts b/src/Geant4Worker/Geant4WorkerCallbacks.ts
index b6c18bba8..9ba694733 100644
--- a/src/Geant4Worker/Geant4WorkerCallbacks.ts
+++ b/src/Geant4Worker/Geant4WorkerCallbacks.ts
@@ -47,17 +47,17 @@ const initDatasets: Geant4WorkerCallbacksType = async args => {
// This, in theory, could enable the app to run offline.
try {
downloadTracker.reset();
- downloadTracker.setCurrentDataset('G4ENSDFSTATE');
+ downloadTracker.setCurrentDataset('G4ENSDFSTATE3.0');
await initG4ENSDFSTATE(wasmModule);
- downloadTracker.setCurrentDataset('G4EMLOW');
+ downloadTracker.setCurrentDataset('G4EMLOW8.6.1');
await initG4EMLOW(wasmModule);
- downloadTracker.setCurrentDataset('G4NDL');
+ downloadTracker.setCurrentDataset('G4NDL4.7.1');
await initG4NDL(wasmModule);
- downloadTracker.setCurrentDataset('G4PARTICLEXS');
+ downloadTracker.setCurrentDataset('G4PARTICLEXS4.1');
await initG4PARTICLEXS(wasmModule);
- downloadTracker.setCurrentDataset('G4SAIDDATA');
+ downloadTracker.setCurrentDataset('G4SAIDDATA2.0');
await initG4SAIDDATA(wasmModule);
- downloadTracker.setCurrentDataset('PhotonEvaporation');
+ downloadTracker.setCurrentDataset('PhotonEvaporation6.1');
await initPhotoEvaporation(wasmModule);
} catch (error: unknown) {
console.error('Error initializing lazy files:', (error as Error).message);
diff --git a/src/WrapperApp/WrapperApp.tsx b/src/WrapperApp/WrapperApp.tsx
index fba847f76..b49bbbdc1 100644
--- a/src/WrapperApp/WrapperApp.tsx
+++ b/src/WrapperApp/WrapperApp.tsx
@@ -3,7 +3,6 @@ import { styled } from '@mui/material/styles';
import { SyntheticEvent, useEffect, useState } from 'react';
import { useConfig } from '../config/ConfigService';
-import { useDatasetDownloadManager } from '../Geant4Worker/Geant4DatasetDownloadManager';
import { useAuth } from '../services/AuthService';
import { useStore } from '../services/StoreService';
import { EditorToolbar } from '../ThreeEditor/components/Editor/EditorToolbar';
@@ -66,12 +65,6 @@ function WrapperApp() {
const [providedInputFiles, setProvidedInputFiles] = useState();
const [highlightRunForm, setHighLightRunForm] = useState(false);
- const {
- managerState: geant4DownloadManagerState,
- datasetStates: geant4DatasetStates,
- startDownload: geant4DatasetDownload
- } = useDatasetDownloadManager();
-
useEffect(() => {
if (Object.keys(providedInputFiles ?? {}).length > 0) {
setHighLightRunForm(true);
@@ -240,9 +233,6 @@ function WrapperApp() {
clearInputFiles={() => setProvidedInputFiles(undefined)}
runSimulation={runSimulation}
setSource={setSimulationPanelPresentedSimulationsSource}
- geant4DownloadManagerState={geant4DownloadManagerState}
- geant4DatasetDownloadStart={geant4DatasetDownload}
- geant4DatasetStates={geant4DatasetStates}
geant4DatasetType={geant4DatasetType}
setGeant4DatasetType={setGeant4DatasetType}
/>
diff --git a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx
index 51f10f585..4027aa479 100644
--- a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx
+++ b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx
@@ -1,25 +1,34 @@
// Full credits to @kmichalik
+import CachedIcon from '@mui/icons-material/Cached';
import CheckIcon from '@mui/icons-material/Check';
+import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
+import RefreshIcon from '@mui/icons-material/Refresh';
+import StorageIcon from '@mui/icons-material/Storage';
import {
AccordionDetails,
AccordionSummary,
Box,
Button,
+ Chip,
+ CircularProgress,
LinearProgress,
ToggleButton,
+ Tooltip,
Typography,
useTheme
} from '@mui/material';
-import { useState } from 'react';
+import { JSX, useEffect, useState } from 'react';
import {
DatasetDownloadStatus,
DatasetStatus,
DownloadManagerStatus
-} from '../../../Geant4Worker/Geant4DatasetDownloadManager';
+} from '../../../Geant4Worker/Geant4DatasetManager';
import { useDialog } from '../../../services/DialogService';
+import { useSharedDatasetManager } from '../../../services/Geant4DatasetContextProvider';
import StyledAccordion from '../../../shared/components/StyledAccordion';
import { StyledExclusiveToggleButtonGroup } from '../../../shared/components/StyledExclusiveToggleButtonGroup';
@@ -28,45 +37,178 @@ export enum Geant4DatasetsType {
FULL
}
-export interface Geant4DatasetsProps {
- geant4DownloadManagerState: DownloadManagerStatus;
- geant4DatasetStates: DatasetStatus[];
- geant4DatasetDownloadStart: () => void;
- geant4DatasetType: Geant4DatasetsType;
- setGeant4DatasetType: (type: Geant4DatasetsType) => void;
-}
-
function DatasetCurrentStatus(props: { status: DatasetStatus }) {
const { status } = props;
+ const idleIcon = status.cached ? (
+
+ ) : (
+
+ );
+
+ const datasetStatusIcon: Map = new Map([
+ [DatasetDownloadStatus.IDLE, idleIcon],
+ [DatasetDownloadStatus.DOWNLOADING, ],
+ [DatasetDownloadStatus.PROCESSING, ],
+ [DatasetDownloadStatus.DONE, ]
+ ]);
+
+ const datasetProgressBar: Map = new Map([
+ [
+ DatasetDownloadStatus.DOWNLOADING,
+
+ ],
+ [
+ DatasetDownloadStatus.PROCESSING,
+
+ ]
+ ]);
+
return (
{status.name}
- {status.status === DatasetDownloadStatus.DONE && }
+ {datasetStatusIcon.get(status.status)}
-
- {status.status === DatasetDownloadStatus.DOWNLOADING && (
-
- )}
- {status.status === DatasetDownloadStatus.PROCESSING && (
-
- )}
- {status.status === DatasetDownloadStatus.IDLE && (
- {datasetProgressBar.get(status.status)}
+
+ );
+}
+
+enum DatasetChipType {
+ CACHED,
+ PARTIALLY_CACHED,
+ NOT_CACHED
+}
+
+type DatasetCachedChipInfo = {
+ title: string;
+ chip: {
+ icon: JSX.Element;
+ label: string;
+ color: 'primary' | 'warning' | 'error';
+ };
+};
+
+type DatasetCachedChipProps = {
+ cachedCount: number;
+ totalCount: number;
+ downloadSizeNeededMB: number;
+ type: DatasetChipType;
+};
+
+function DatasetCachedChip(props: DatasetCachedChipProps) {
+ const { cachedCount, totalCount, downloadSizeNeededMB, type } = props;
+
+ const chipPropsPerStatus: Record = {
+ [DatasetChipType.CACHED]: {
+ title: 'All datasets are cached in your browser. Loading will be quick!',
+ chip: {
+ icon: ,
+ label: 'Cached',
+ color: 'primary'
+ }
+ },
+ [DatasetChipType.PARTIALLY_CACHED]: {
+ title: `${cachedCount} of ${totalCount} datasets cached. ~${downloadSizeNeededMB.toFixed(0)} MB needs to be downloaded.`,
+ chip: {
+ icon: ,
+ label: `Partially cached (${cachedCount}/${totalCount})`,
+ color: 'warning'
+ }
+ },
+ [DatasetChipType.NOT_CACHED]: {
+ title: `No datasets cached. ~${downloadSizeNeededMB.toFixed(0)} MB will be downloaded from S3.`,
+ chip: {
+ icon: ,
+ label: 'Not cached',
+ color: 'error'
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+}
+
+function CacheStatusIndicator() {
+ const { isLoading, cachedCount, totalCount, downloadSizeNeededMB, storageEstimate, refresh } =
+ useSharedDatasetManager();
+ const allCached = cachedCount === totalCount;
+
+ if (isLoading) {
+ return (
+
+
+
+ Checking cache status...
+
+
+ );
+ }
+
+ return (
+
+
+ 0
+ ? DatasetChipType.PARTIALLY_CACHED
+ : DatasetChipType.NOT_CACHED
+ }
+ />
+
+
+ }
+ label='Refresh'
+ size='small'
+ variant='outlined'
+ onClick={refresh}
+ sx={{ cursor: 'pointer', px: 1 }}
/>
- )}
+
+ {storageEstimate && (
+
+
+ {cachedCount > 0 && `${storageEstimate.usedMB.toFixed(0)} MB used`}
+ {!allCached &&
+ (cachedCount > 0
+ ? `, ~${downloadSizeNeededMB.toFixed(0)} MB remaining to download`
+ : `~${downloadSizeNeededMB.toFixed(0)} MB download required (may take several minutes)`)}
+
+
+ )}
);
}
@@ -135,10 +277,44 @@ export function Geant4DatasetDownloadSelector(props: {
);
}
-export function Geant4Datasets(props: Geant4DatasetsProps) {
+export function Geant4Datasets() {
const theme = useTheme();
- const { geant4DownloadManagerState, geant4DatasetStates, geant4DatasetDownloadStart } = props;
- const [open, setOpen] = useState(true);
+ const {
+ managerState: geant4DownloadManagerState,
+ datasetStatus,
+ startDownload: geant4DatasetDownloadStart,
+ cachedCount,
+ totalCount,
+ refresh,
+ clearCache
+ } = useSharedDatasetManager();
+ const [open, setOpen] = useState(geant4DownloadManagerState !== DownloadManagerStatus.FINISHED);
+
+ const allCached = cachedCount === totalCount;
+
+ // Refresh cache status when download finishes
+ useEffect(() => {
+ if (geant4DownloadManagerState === DownloadManagerStatus.FINISHED) {
+ // Add a small delay to ensure IndexedDB is updated
+ setOpen(false);
+ setTimeout(() => {
+ refresh();
+ }, 1000);
+ }
+ }, [geant4DownloadManagerState, refresh]);
+
+ const buttonText = allCached ? 'Load from cache' : 'Start download';
+ const buttonIcon = allCached ? : ;
+
+ const enableDownloadButton = geant4DownloadManagerState === DownloadManagerStatus.IDLE;
+ const enableClearButton =
+ geant4DownloadManagerState === DownloadManagerStatus.IDLE ||
+ geant4DownloadManagerState === DownloadManagerStatus.FINISHED;
+
+ const showDownloadProgress =
+ geant4DownloadManagerState === DownloadManagerStatus.WORKING ||
+ geant4DownloadManagerState === DownloadManagerStatus.FINISHED ||
+ cachedCount > 0;
return (
- {geant4DownloadManagerState === DownloadManagerStatus.IDLE && (
+
+
+ {showDownloadProgress &&
+ Object.values(datasetStatus).map(status => (
+
+ ))}
+
+
- )}
- {geant4DatasetStates.map(status => (
-
- ))}
+ {cachedCount > 0 && (
+ }
+ color='error'>
+ Clear
+
+ )}
+
+
{geant4DownloadManagerState === DownloadManagerStatus.ERROR && (
Failed to download datasets. Please check your connection and try again.
diff --git a/src/WrapperApp/components/Simulation/Modal/DatasetsFullInfoModal.tsx b/src/WrapperApp/components/Simulation/Modal/DatasetsFullInfoModal.tsx
index 3da546abc..14af44aaa 100644
--- a/src/WrapperApp/components/Simulation/Modal/DatasetsFullInfoModal.tsx
+++ b/src/WrapperApp/components/Simulation/Modal/DatasetsFullInfoModal.tsx
@@ -9,7 +9,9 @@ import {
TableHead,
TableRow
} from '@mui/material';
+import { useEffect } from 'react';
+import { useSharedDatasetManager } from '../../../../services/Geant4DatasetContextProvider';
import {
ConcreteDialogProps,
CustomDialog
@@ -42,13 +44,19 @@ const datasetSummaries = [
'Data evaluated from the SAID database for proton, neutron, pion inelastic, elastic, and charge exchange cross sections of nucleons below 3 GeV'
},
{
- name: 'G4PhotonEvaporation6.1',
+ name: 'PhotonEvaporation6.1',
description:
'Data for photon emission and internal conversion following nuclear de-excitation, used by the photon evaporation model in radioactive decay and photo-nuclear processes.'
}
];
export function DatasetsFullInfoDialog({ onClose }: ConcreteDialogProps) {
+ const { datasetStatus, refresh } = useSharedDatasetManager();
+
+ useEffect(() => {
+ refresh();
+ }, [refresh]);
+
return (
- Currently there is no indicator showing if you have already downloaded the
- datasets. Each time you start a new simulation with FULL datasets option,
- you have to press 'Start Download' button. If the datasets are already in
- your browser storage, the datasets will be loaded from cache.
+ sx={{ mt: 1 }}>
+ Simulation input data and results may be removed if you close the browser
+ tab or restart your computer. In contrast, Geant4 datasets stored in your
+ browser's cache (IndexedDB) are saved on your drive and persist across
+ sessions.
- Total size of downloaded datasets is about 2GB. Each time you clear your
- browser data, you will need to re-download these datasets for optimal
- simulation performance.
+ sx={{ mt: 1 }}>
+ If you clear your browser data (for example, using antivirus software or PC
+ cleanup tools), you'll need to download these datasets again to ensure
+ optimal simulation performance.
+ sx={{ mt: 1 }}>
Name
+ Cached
Description
@@ -97,6 +105,9 @@ export function DatasetsFullInfoDialog({ onClose }: ConcreteDialogProps) {
'verticalAlign': 'top'
}}>
{ds.name}
+
+ {datasetStatus[ds.name]?.cached ? 'Yes' : 'No'}
+
{ds.description}
))}
diff --git a/src/WrapperApp/components/Simulation/RunSimulationForm.tsx b/src/WrapperApp/components/Simulation/RunSimulationForm.tsx
index 6587bbaa0..da01d4cf2 100644
--- a/src/WrapperApp/components/Simulation/RunSimulationForm.tsx
+++ b/src/WrapperApp/components/Simulation/RunSimulationForm.tsx
@@ -21,8 +21,9 @@ import {
import Typography from '@mui/material/Typography';
import { MouseEvent, SyntheticEvent, useEffect, useState } from 'react';
-import { DownloadManagerStatus } from '../../../Geant4Worker/Geant4DatasetDownloadManager';
+import { DownloadManagerStatus } from '../../../Geant4Worker/Geant4DatasetManager';
import { usePythonConverter } from '../../../PythonConverter/PythonConverterService';
+import { useSharedDatasetManager } from '../../../services/Geant4DatasetContextProvider';
import { useStore } from '../../../services/StoreService';
import StyledAccordion from '../../../shared/components/StyledAccordion';
import { StyledExclusiveToggleButtonGroup } from '../../../shared/components/StyledExclusiveToggleButtonGroup';
@@ -76,7 +77,6 @@ export type RunSimulationFormProps = {
clearInputFiles?: () => void;
runSimulation?: RunSimulationFunctionType;
setSource?: (source: SimulationFetchSource) => void;
- geant4DownloadManagerState: DownloadManagerStatus;
geant4DatasetType: Geant4DatasetsType;
setGeant4DatasetType: (type: Geant4DatasetsType) => void;
};
@@ -89,7 +89,6 @@ export function RunSimulationForm({
clearInputFiles = () => {},
runSimulation = () => {},
setSource = () => {},
- geant4DownloadManagerState,
geant4DatasetType,
setGeant4DatasetType
}: RunSimulationFormProps) {
@@ -193,6 +192,7 @@ export function RunSimulationForm({
const [selectedScriptParamsTab, setSelectedScriptParamsTab] = useState(0);
const { isConverterReady } = usePythonConverter();
const [runPreconditionsMet, setRunPreconditionsMet] = useState(false);
+ const { managerState: geant4DownloadManagerState } = useSharedDatasetManager();
useEffect(() => {
const runFormIsValid = !isNaN(nTasks) && !isNaN(overridePrimariesCount);
diff --git a/src/WrapperApp/components/Simulation/RunSimulationPanel.tsx b/src/WrapperApp/components/Simulation/RunSimulationPanel.tsx
index 8833d0e13..fa6d56b28 100644
--- a/src/WrapperApp/components/Simulation/RunSimulationPanel.tsx
+++ b/src/WrapperApp/components/Simulation/RunSimulationPanel.tsx
@@ -7,11 +7,11 @@ import { useAuth } from '../../../services/AuthService';
import { useStore } from '../../../services/StoreService';
import StyledAccordion from '../../../shared/components/StyledAccordion';
import { SimulatorNames, SimulatorType } from '../../../types/RequestTypes';
-import { Geant4Datasets, Geant4DatasetsProps, Geant4DatasetsType } from './Geant4DatasetDownload';
+import { Geant4Datasets, Geant4DatasetsType } from './Geant4DatasetDownload';
import RecentSimulations from './RecentSimulations';
import { RunSimulationForm, RunSimulationFormProps } from './RunSimulationForm';
-export type RunSimulationPanelProps = RunSimulationFormProps & Geant4DatasetsProps;
+export type RunSimulationPanelProps = RunSimulationFormProps;
export default function RunSimulationPanel(props: RunSimulationPanelProps) {
const theme = useTheme();
@@ -27,9 +27,7 @@ export default function RunSimulationPanel(props: RunSimulationPanelProps) {
<>
{yaptideEditor?.contextManager.currentSimulator === SimulatorType.GEANT4 &&
- props.geant4DatasetType === Geant4DatasetsType.FULL && (
-
- )}
+ props.geant4DatasetType === Geant4DatasetsType.FULL && }
>
) : (
diff --git a/src/services/Geant4DatasetContextProvider.tsx b/src/services/Geant4DatasetContextProvider.tsx
new file mode 100644
index 000000000..da1a125d1
--- /dev/null
+++ b/src/services/Geant4DatasetContextProvider.tsx
@@ -0,0 +1,17 @@
+import { useDatasetManager, UseDatasetManagerResult } from '../Geant4Worker/Geant4DatasetManager';
+import { createGenericContext, GenericContextProviderProps } from './GenericContext';
+
+const [useSharedDatasetManager, InnerDatasetManagerContextProvider] =
+ createGenericContext();
+
+function Geant4DatasetContextProvider({ children }: GenericContextProviderProps) {
+ const datasetManager = useDatasetManager();
+
+ return (
+
+ {children}
+
+ );
+}
+
+export { Geant4DatasetContextProvider, useSharedDatasetManager };