Spaces:
Sleeping
Sleeping
| import React, { useRef, useState } from 'react' | |
| import { | |
| Title, | |
| Text, | |
| Button, | |
| Anchor, | |
| Slider, | |
| Switch, | |
| Divider, | |
| Stack, | |
| Box, | |
| Loader, | |
| Select, | |
| NumberInput, | |
| Badge, | |
| Alert, | |
| Group, | |
| } from '@mantine/core' | |
| import { IconCloudUpload, IconChartBar, IconAlertCircle } from '@tabler/icons-react' | |
| import { useXRD } from '../context/XRDContext' | |
| import ExampleDataPanel from './ExampleDataPanel' | |
| const Controls = () => { | |
| const { | |
| rawData, | |
| isLoading, | |
| detectedWavelength, | |
| userWavelength, | |
| setUserWavelength, | |
| wavelengthSource, | |
| dataWarnings, | |
| baselineCorrection, | |
| setBaselineCorrection, | |
| interpolationEnabled, | |
| setInterpolationEnabled, | |
| scalingEnabled, | |
| setScalingEnabled, | |
| interpolationStrategy, | |
| setInterpolationStrategy, | |
| handleFileUpload, | |
| runInference, | |
| MODEL_WAVELENGTH, | |
| MODEL_MIN_2THETA, | |
| MODEL_MAX_2THETA, | |
| } = useXRD() | |
| const fileInputRef = useRef(null) | |
| const [showExamples, setShowExamples] = useState(true) | |
| const handleFileChange = async (event) => { | |
| const file = event.target.files?.[0] | |
| if (file) { | |
| const success = await handleFileUpload(file) | |
| if (success) setShowExamples(false) | |
| // Reset file input so the same file can be uploaded again | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = '' | |
| } | |
| } | |
| } | |
| const handleUploadClick = () => { | |
| fileInputRef.current?.click() | |
| } | |
| return ( | |
| <Stack gap="md"> | |
| {/* File Upload */} | |
| <Box> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept=".xy,.csv,.txt,.cif,.dif" | |
| onChange={handleFileChange} | |
| style={{ display: 'none' }} | |
| /> | |
| <Button | |
| fullWidth | |
| leftSection={<IconCloudUpload size={20} />} | |
| onClick={handleUploadClick} | |
| size="md" | |
| > | |
| Upload XRD Data | |
| </Button> | |
| <Group justify="space-between" mt="xs"> | |
| <Text size="sm" c="dimmed"> | |
| Formats: .xy, .cif, .dif | |
| </Text> | |
| <Anchor | |
| size="sm" | |
| component="button" | |
| type="button" | |
| onClick={() => setShowExamples((v) => !v)} | |
| > | |
| {showExamples ? 'Hide samples' : 'Try samples'} | |
| </Anchor> | |
| </Group> | |
| </Box> | |
| {showExamples && <ExampleDataPanel onSelect={() => setShowExamples(false)} />} | |
| {rawData && ( | |
| <> | |
| <Divider /> | |
| {/* Data Info */} | |
| <Box p="md" style={{ backgroundColor: '#f8f9fa', borderRadius: '8px' }}> | |
| <Stack gap="xs"> | |
| <Text size="sm" c="dimmed"> | |
| Data Points: {rawData.x.length} | |
| </Text> | |
| <Text size="sm" c="dimmed"> | |
| Range: {Math.min(...rawData.x).toFixed(2)}° - {Math.max(...rawData.x).toFixed(2)}° | |
| </Text> | |
| </Stack> | |
| </Box> | |
| {/* Warnings */} | |
| {dataWarnings.length > 0 && ( | |
| <Alert icon={<IconAlertCircle size={16} />} color="yellow" variant="light"> | |
| <Stack gap="xs"> | |
| {dataWarnings.map((warning, idx) => ( | |
| <Text key={idx} size="xs">{warning}</Text> | |
| ))} | |
| </Stack> | |
| </Alert> | |
| )} | |
| <Divider /> | |
| {/* Wavelength Configuration */} | |
| <Title order={4}>Wavelength</Title> | |
| {detectedWavelength && ( | |
| <Badge color="blue" variant="light" size="sm" mb="xs"> | |
| Detected: {detectedWavelength.toFixed(4)} Å | |
| </Badge> | |
| )} | |
| <NumberInput | |
| label="Wavelength (Å)" | |
| description="Cu Kα=1.5406, Mo Kα=0.7107, Synch=0.6199" | |
| value={userWavelength} | |
| onChange={(value) => setUserWavelength(value || MODEL_WAVELENGTH)} | |
| min={0.1} | |
| max={3.0} | |
| step={0.0001} | |
| precision={4} | |
| size="sm" | |
| decimalScale={4} | |
| /> | |
| <Group gap="xs" mt="xs"> | |
| <Button size="xs" variant="light" onClick={() => setUserWavelength(1.5406)}>Cu Kα</Button> | |
| <Button size="xs" variant="light" onClick={() => setUserWavelength(0.7107)}>Mo Kα</Button> | |
| <Button size="xs" variant="light" onClick={() => setUserWavelength(0.6199)}>Synch</Button> | |
| </Group> | |
| <Box mt="md" p="md" style={{ backgroundColor: '#e7f5ff', borderRadius: '8px', border: '1px solid #91a7ff' }}> | |
| <Text size="sm" fw={600} c="#1864ab" mb={4}> | |
| Training Data Specs: | |
| </Text> | |
| <Text size="sm" c="#364fc7" style={{ lineHeight: 1.6 }}> | |
| • λ: {MODEL_WAVELENGTH} Å (20 keV)<br/> | |
| • 2θ: {MODEL_MIN_2THETA}°-{MODEL_MAX_2THETA}° (8192 pts)<br/> | |
| • Intensity: 0-100 scaled | |
| </Text> | |
| </Box> | |
| <Divider /> | |
| {/* Preprocessing Controls */} | |
| <Title order={4}>Preprocessing</Title> | |
| <Switch | |
| label="Baseline Correction" | |
| checked={baselineCorrection} | |
| onChange={(event) => setBaselineCorrection(event.currentTarget.checked)} | |
| /> | |
| <Switch | |
| label="Signal Scaling (0-100)" | |
| description="Normalize to model input range" | |
| checked={scalingEnabled} | |
| onChange={(event) => setScalingEnabled(event.currentTarget.checked)} | |
| /> | |
| <Select | |
| label="Interpolation Strategy" | |
| description="Resampling method for 8192 points" | |
| value={interpolationStrategy} | |
| onChange={(value) => { | |
| if (value) setInterpolationStrategy(value) | |
| }} | |
| data={[ | |
| { value: 'linear', label: 'Linear' }, | |
| { value: 'cubic', label: 'Cubic Spline' }, | |
| ]} | |
| size="sm" | |
| allowDeselect={false} | |
| /> | |
| <Divider /> | |
| </> | |
| )} | |
| {/* Analysis Button - Always visible when data loaded */} | |
| {rawData && ( | |
| <Button | |
| fullWidth | |
| color="violet" | |
| leftSection={isLoading ? <Loader size={18} color="white" /> : <IconChartBar size={20} />} | |
| onClick={runInference} | |
| disabled={isLoading} | |
| size="lg" | |
| style={{ marginTop: 'auto' }} | |
| > | |
| {isLoading ? 'Analyzing...' : 'Run Analysis'} | |
| </Button> | |
| )} | |
| </Stack> | |
| ) | |
| } | |
| export default Controls | |