Spaces:
Runtime error
Runtime error
import ScheduleSendIcon from '@mui/icons-material/ScheduleSend'; | |
import { | |
Backdrop, // Import Backdrop component | |
Box, | |
Button, | |
CircularProgress, // Import CircularProgress component | |
Container, | |
FormControl, | |
InputLabel, | |
MenuItem, | |
Paper, | |
Select, | |
Table, | |
TableBody, | |
TableCell, | |
TableContainer, | |
TableHead, | |
TableRow, | |
TextField, | |
RadioGroup, | |
Radio, | |
FormControlLabel, | |
FormLabel, | |
Alert | |
} from "@mui/material"; | |
import { styled } from "@mui/material/styles"; | |
import Papa from "papaparse"; | |
import React, { useContext, useState, useCallback } from "react"; | |
import { TopicsContext } from "./UploadFileContext"; | |
const VisuallyHiddenInput = styled("input")({ | |
clip: "rect(0 0 0 0)", | |
clipPath: "inset(50%)", | |
height: 1, | |
overflow: "hidden", | |
position: "absolute", | |
bottom: 0, | |
left: 0, | |
whiteSpace: "nowrap", | |
width: 1, | |
}); | |
function QueryView() { | |
const [fileData, setFileData] = useState([]); | |
const [selectedColumn, setSelectedColumn] = useState(""); | |
const [selectedFile, setSelectedFile] = useState(null); | |
const [selectedColumnData, setSelectedColumnData] = useState([]); | |
const [openSelector, setOpenSelector] = React.useState(false); | |
const [xLeftWord, setXLeftWord] = useState("past"); | |
const [xRightWord, setXRightWord] = useState("future"); | |
const [yTopWord, setYTopWord] = useState("positive"); | |
const [yBottomWord, setYBottomWord] = useState("negative"); | |
const [radiusSize, setRadiusSize] = useState(0.5); | |
const [nClusters, setNClusters] = useState(15); | |
const [minCountTerms, setMinCountTerms] = useState(1); | |
const [nameLength, setNameLength] = useState(3); | |
const [cleanTopics, setCleanTopics] = useState(false); | |
const [language, setLanguage] = useState("english"); | |
const { uploadFile, isLoading, selectedView, refreshBourdieuQuery } = useContext(TopicsContext); | |
const [fileDataTooLong, setFileDataTooLong] = useState(false); | |
const [fileDataError, setFileDataError] = useState(null); | |
/** | |
* Column name selector handler | |
*/ | |
const handleClose = () => { | |
setOpenSelector(false); | |
}; | |
const handleOpen = () => { | |
setOpenSelector(true); | |
}; | |
/** | |
* Parse the CSV and take a sample to display the preview | |
* @param {*} file | |
* @param {*} sampleSize | |
* @returns | |
*/ | |
const parseCSVFile = (file, sampleSize = 100) => | |
new Promise((resolve, reject) => { | |
const reader = new FileReader(); | |
reader.onload = (e) => { | |
const csvData = e.target.result; | |
const lines = csvData.split("\n"); | |
setFileDataTooLong(lines.length > 10000); | |
// Take a sample of the first 500 lines to display preview | |
const sampleLines = lines.slice(0, sampleSize).join("\n"); | |
Papa.parse(sampleLines, { | |
complete: (result) => { | |
resolve(result.data); | |
}, | |
error: (parseError) => { | |
reject(parseError.message); | |
}, | |
}); | |
}; | |
reader.readAsText(file); | |
}); | |
/** | |
* Handler the file selection ui workflow | |
* @param {Event} e | |
* @returns | |
*/ | |
const handleFileChange = async (e) => { | |
const file = e.target.files[0]; | |
setSelectedFile(file); | |
if (!file) return; | |
// prepare data for the preview Table | |
try { | |
const parsedData = await parseCSVFile(file); | |
setFileData(parsedData); | |
setSelectedColumn(""); // Clear the selected column when a new file is uploaded | |
if (fileDataTooLong === false) { | |
handleOpen(); | |
} | |
else { | |
handleClose(); | |
} | |
} catch (exc) { | |
setFileDataError("Error parsing the CSV file, please check your file before uploading"); | |
console.error("Error parsing CSV:", exc); | |
} | |
}; | |
const handleColumnSelect = (e) => { | |
const columnName = e.target.value; | |
setSelectedColumn(columnName); | |
// Extract the content of the selected column | |
const columnIndex = fileData[0].indexOf(columnName); | |
const columnData = fileData.slice(1).map((row) => row[columnIndex]); | |
setSelectedColumnData(columnData); | |
}; | |
/** | |
* Launch the upload and processing | |
*/ | |
const handleProcessTopics = async () => { | |
// Return if no column selected | |
if (selectedColumnData.length === 0) return; | |
if (selectedFile && !isLoading) { | |
uploadFile(selectedFile, { | |
nClusters, | |
selectedColumn, | |
selectedView, | |
xLeftWord, | |
xRightWord, | |
yTopWord, | |
yBottomWord, | |
radiusSize, | |
nameLength, | |
minCountTerms, | |
language, | |
cleanTopics | |
}); | |
} | |
}; | |
const handleRefreshQuery = useCallback(async () => { | |
if (!isLoading) { | |
await refreshBourdieuQuery({ | |
topic_param: { | |
n_clusters: nClusters, | |
name_lenght: nameLength, | |
min_count_terms: minCountTerms, | |
language: language, | |
clean_topics: cleanTopics | |
}, | |
bourdieu_query: { | |
x_left_words: xLeftWord.split(","), | |
x_right_words: xRightWord.split(","), | |
y_top_words: yTopWord.split(","), | |
y_bottom_words: yBottomWord.split(","), | |
radius_size: radiusSize, | |
} | |
}); | |
} | |
}); | |
const openTableContainer = selectedColumnData.length > 0 && fileData.length > 0 && fileData.length <= 10000 && fileDataTooLong === false && fileDataError == null; | |
return ( | |
<Container component="form"> | |
{selectedView === "map" && ( | |
<> | |
<Box marginBottom={2}> | |
<Button component="label" variant="outlined" endIcon={<ScheduleSendIcon />}> | |
Upload a CSV (max 10 000 lines) and queue processing | |
<VisuallyHiddenInput type="file" onChange={handleFileChange} required /> | |
</Button> | |
</Box> | |
<Box marginBottom={2}> | |
<FormControl variant="outlined" fullWidth> | |
<InputLabel>Select a Column</InputLabel> | |
<Select value={selectedColumn} onChange={handleColumnSelect} onClose={handleClose} onOpen={handleOpen} open={openSelector}> | |
{fileData[0]?.map((header, index) => ( | |
<MenuItem key={`${header}`} value={header}> | |
{header} | |
</MenuItem> | |
))} | |
</Select> | |
</FormControl> | |
</Box> | |
</> | |
)} | |
{isLoading ? ( | |
<Backdrop open={isLoading} style={{ zIndex: 9999 }}> | |
<CircularProgress color="primary" /> | |
</Backdrop> | |
) : ( | |
// Content when not loading | |
<div> | |
{openTableContainer && ( | |
<TableContainer component={Paper} style={{ maxHeight: "400px", overflowY: "auto" }}> | |
<Table> | |
<TableHead> | |
<TableRow> | |
<TableCell>{selectedColumn}</TableCell> | |
</TableRow> | |
</TableHead> | |
<TableBody> | |
{selectedColumnData.map((cell, index) => ( | |
<TableRow key={`table-${index}`}> | |
<TableCell>{cell}</TableCell> | |
</TableRow> | |
))} | |
</TableBody> | |
</Table> | |
</TableContainer> | |
)} | |
{fileDataTooLong && ( | |
<Alert severity="error">CSV must have less than 10 000 lines (this is a demo)</Alert> | |
)} | |
{fileDataError && ( | |
<Alert severity="error">CSV must have less than 10 000 lines (this is a demo)</Alert> | |
)} | |
{selectedView === "bourdieu" && ( | |
<Box marginTop={2} display="flex" alignItems="center" flexDirection="column"> | |
<FormControl variant="outlined"> | |
<TextField required id="input-bourdieu-xl" sx={{ marginBottom: "0.5em" }} label="X left words (comma separated)" variant="outlined" onChange={e => setXLeftWord(e.target.value)} value={xLeftWord} /> | |
<TextField required id="input-bourdieu-xr" sx={{ marginBottom: "1em" }} label="X right words (comma separated)" variant="outlined" onChange={e => setXRightWord(e.target.value)} value={xRightWord} /> | |
<TextField required id="input-bourdieu-yt" sx={{ marginBottom: "1em" }} label="Y top words (comma separated)" variant="outlined" onChange={e => setYTopWord(e.target.value)} value={yTopWord} /> | |
<TextField required id="input-bourdieu-yb" sx={{ marginBottom: "1em" }} label="Y bottom words (comma separated)" variant="outlined" onChange={e => setYBottomWord(e.target.value)} value={yBottomWord} /> | |
<TextField required id="input-bourdieu-radius" sx={{ marginBottom: "1em" }} label="Radius Size" variant="outlined" onChange={e => setRadiusSize(e.target.value)} value={radiusSize} /> | |
<TextField required id="input-map-nclusters" sx={{ marginBottom: "1em" }} label="N° Clusters" variant="outlined" onChange={e => setNClusters(e.target.value)} value={nClusters} /> | |
<TextField required id="input-map-namelength" sx={{ marginBottom: "1em" }} label="Name length" variant="outlined" onChange={e => setNameLength(e.target.value)} value={nameLength} /> | |
<TextField required id="input-map-mincountterms" sx={{ marginBottom: "1em" }} label="Min Count Terms" variant="outlined" onChange={e => setMinCountTerms(e.target.value)} value={minCountTerms} /> | |
<RadioGroup required name="cleantopics-radio-group" defaultValue={cleanTopics} onChange={e => setCleanTopics(e.target.value)} variant="outlined" sx={{ marginBottom: "1em" }} disabled> | |
<FormLabel id="clean-topics-group-label">Clean Topics</FormLabel> | |
<FormControlLabel value={true} label="Yes" control={<Radio />} disabled /> | |
<FormControlLabel value={false} label="No" control={<Radio />} disabled /> | |
</RadioGroup> | |
</FormControl> | |
<Button variant="contained" color="primary" onClick={handleRefreshQuery} disabled={isLoading || fileDataTooLong === true || fileDataError !== null}> | |
{isLoading ? "Processing..." : "Refresh Bourdieu Axes"} | |
</Button> | |
</Box> | |
)} | |
{selectedView === "map" && ( | |
<Box marginTop={2} display="flex" alignItems="center" flexDirection="column"> | |
<Button variant="contained" color="primary" onClick={handleProcessTopics} disabled={selectedColumnData.length === 0 || isLoading || fileDataTooLong === true || fileDataError !== null}> | |
{isLoading ? "Processing..." : "Process Topics"} | |
</Button> | |
<FormControl variant="outlined" sx={{ marginTop: "1em", marginLeft: "1em" }}> | |
<TextField required id="input-map-nclusters" sx={{ marginBottom: "1em" }} label="N° Clusters" variant="outlined" onChange={e => setNClusters(e.target.value)} value={nClusters} /> | |
<TextField required id="input-map-namelength" sx={{ marginBottom: "1em" }} label="Name length" variant="outlined" onChange={e => setNameLength(e.target.value)} value={nameLength} /> | |
<TextField required id="input-map-mincountterms" sx={{ marginBottom: "1em" }} label="Min Count Terms" variant="outlined" onChange={e => setMinCountTerms(e.target.value)} value={minCountTerms} /> | |
<RadioGroup required name="cleantopics-radio-group" defaultValue={cleanTopics} onChange={e => setCleanTopics(e.target.value)} variant="outlined" sx={{ marginBottom: "1em" }} disabled> | |
<FormLabel id="clean-topics-group-label">Clean Topics</FormLabel> | |
<FormControlLabel value={true} label="Yes" control={<Radio />} disabled /> | |
<FormControlLabel value={false} label="No" control={<Radio />} disabled /> | |
</RadioGroup> | |
<RadioGroup required name="language-radio-group" defaultValue={language} onChange={e => setLanguage(e.target.value)} variant="outlined" sx={{ marginBottom: "1em" }}> | |
<FormLabel id="language-group-label">Language</FormLabel> | |
<FormControlLabel value="french" label="fr" control={<Radio />} /> | |
<FormControlLabel value="english" label="en" control={<Radio />} /> | |
</RadioGroup> | |
</FormControl> | |
</Box> | |
)} | |
</div> | |
)} | |
</Container> | |
); | |
} | |
export default QueryView; | |