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;