From 232b3d317904a9e2c5d51ce81fb3826617712db4 Mon Sep 17 00:00:00 2001 From: Aarsh Date: Thu, 11 Sep 2025 22:22:51 -0500 Subject: [PATCH] Fix data loading box --- .../src/components/editing/WidgetsEditor.tsx | 287 +++++++++--------- 1 file changed, 147 insertions(+), 140 deletions(-) diff --git a/utk_curio/frontend/urban-workflows/src/components/editing/WidgetsEditor.tsx b/utk_curio/frontend/urban-workflows/src/components/editing/WidgetsEditor.tsx index 901a11da..c65f95db 100644 --- a/utk_curio/frontend/urban-workflows/src/components/editing/WidgetsEditor.tsx +++ b/utk_curio/frontend/urban-workflows/src/components/editing/WidgetsEditor.tsx @@ -1,14 +1,14 @@ import React, { useEffect, useState, useRef } from "react"; - + // Bootstrap import "bootstrap/dist/css/bootstrap.min.css"; import { WidgetType, BoxType } from "../../constants"; import { PythonInterpreter } from "../../PythonInterpreter"; import { useFlowContext } from "../../providers/FlowProvider"; import { useProvenanceContext } from "../../providers/ProvenanceProvider"; - + import "./WidgetsEditor.css"; - + type WidgetsEditorProps = { userCode: any; // grammar or python sendReplacedCode: any; // bubble up the code (python or grammar) with the marks resolved @@ -18,7 +18,7 @@ type WidgetsEditorProps = { data?: any; // data object containing pythonInterpreter, input, inputTypes, outputCallback disableWidgets?: boolean; // Added prop to freeze widget buttons instead of hiding them }; - + function WidgetsEditor({ userCode, sendReplacedCode, @@ -29,10 +29,11 @@ function WidgetsEditor({ disableWidgets, }: WidgetsEditorProps) { const [nonValidatedValues, setNonValidatedValues] = useState({}); - + // CSV file upload states const [selectedFile, setSelectedFile] = useState(null); const [fileInfo, setFileInfo] = useState(null); + const [fileKind, setFileKind] = useState<"csv" | "geojson" | null>(null); const [csvContent, setCsvContent] = useState(""); const [isProcessing, setIsProcessing] = useState(false); const [uploadResult, setUploadResult] = useState<{ @@ -40,110 +41,123 @@ function WidgetsEditor({ message: string; savedPath: string | null; } | null>(null); - + const markersDirtyBypass = useRef(false); const { workflowNameRef } = useFlowContext(); const { boxExecProv } = useProvenanceContext(); - + // File upload handling functions const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { setSelectedFile(file); + const lower = file.name.toLowerCase(); + const kind: "csv" | "geojson" = + lower.endsWith(".csv") ? "csv" : "geojson"; + setFileKind(kind); + setFileInfo({ name: file.name, size: (file.size / 1024).toFixed(2) + " KB", - type: file.type || "text/csv", + type: file.type || (kind === "csv" ? "text/csv" : "application/geo+json"), lastModified: new Date(file.lastModified).toLocaleString(), }); - - // Read file content using FileReader + const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; setCsvContent(content); }; reader.readAsText(file); - - // Reset previous results + setUploadResult(null); } }; - + const handleRunCode = async () => { if (!selectedFile || !csvContent) { - setUploadResult({ - success: false, - message: "Please select a CSV file first", - savedPath: null, - }); + setUploadResult({ success: false, message: "Please select a file first", savedPath: null }); return; } - if (!data || !data.pythonInterpreter) { - setUploadResult({ - success: false, - message: "Python interpreter not available", - savedPath: null, - }); + setUploadResult({ success: false, message: "Python interpreter not available", savedPath: null }); return; } - + setIsProcessing(true); - + try { - // Generate Python code to create DataFrame from CSV content using StringIO - // Escape the CSV content properly for Python string - const escapedCsvContent = csvContent + const escaped = csvContent .replace(/\\/g, "\\\\") .replace(/'''/g, "\\'''"); - - const pythonCode = `import pandas as pd + + let pythonCode = ""; + + if (fileKind === "csv") { + pythonCode = `import pandas as pd from io import StringIO - -# CSV content from uploaded file -csv_content = '''${escapedCsvContent}''' - -# Create DataFrame from CSV content + +csv_content = '''${escaped}''' df = pd.read_csv(StringIO(csv_content)) print(f"DataFrame shape: {df.shape}") print(f"Columns: {list(df.columns)}") -print(f"First 5 rows:") +print("First 5 rows:") print(df.head()) return df`; - - console.log("Generated Python code for CSV processing"); - - // Use PythonInterpreter to execute the code (same as Code tab) + } else { + // geojson/json + pythonCode = `import json +import pandas as pd + +geojson_text = '''${escaped}''' +gj = json.loads(geojson_text) + +# Try GeoPandas if available; otherwise load properties with pandas +try: + import geopandas as gpd + # Handle both FeatureCollection and bare features list + features = gj["features"] if "features" in gj else gj + gdf = gpd.GeoDataFrame.from_features(features) + print(f"GeoDataFrame shape: {gdf.shape}") + print(f"Columns: {list(gdf.columns)}") + print("First 5 rows:") + print(gdf.head()) + return gdf +except Exception as e: + print("Geopandas not available or failed to parse geometry, falling back to pandas:", e) + features = gj["features"] if "features" in gj else gj + # Normalize properties; geometry kept as raw dict for visibility + df = pd.json_normalize(features) + print(f"DataFrame shape: {df.shape}") + print(f"Columns: {list(df.columns)}") + print("First 5 rows:") + print(df.head()) + return df`; + } + data.pythonInterpreter.interpretCode( - pythonCode, // unresolvedUserCode - pythonCode, // userCode (resolved) - data.input, // input - data.inputTypes, // inputTypes + pythonCode, + pythonCode, + data.input, + data.inputTypes, (result: any) => { - console.log("Python execution result:", result); setIsProcessing(false); - + if (result.stderr && result.stderr.trim() !== "") { - setUploadResult({ - success: false, - message: "Python execution error: " + result.stderr, - savedPath: null, - }); + setUploadResult({ success: false, message: "Python execution error: " + result.stderr, savedPath: null }); } else { setUploadResult({ success: true, - message: `CSV "${selectedFile.name}" processed successfully! DataFrame saved.`, + message: + `${selectedFile.name} processed successfully!` + + (fileKind === "geojson" ? " Parsed as GeoJSON." : " Parsed as CSV."), savedPath: result.output?.path || "Unknown path", }); - - // Call output callback if available if (data.outputCallback && result.output) { data.outputCallback(data.nodeId, result.output); } } - - // Send code to widgets for display + if (sendReplacedCode) { sendReplacedCode(pythonCode); } @@ -154,31 +168,25 @@ return df`; boxExecProv ); } catch (error) { - console.error("File processing error:", error); setIsProcessing(false); - setUploadResult({ - success: false, - message: "Error: " + (error as Error).message, - savedPath: null, - }); + setUploadResult({ success: false, message: "Error: " + (error as Error).message, savedPath: null }); } }; - - // returns {widget: string, value: string} with converted value if valid or {} if invalid + const validateWidgetValue = (widget: string, value: string) => { const isANumber = (elem: string) => { let num = parseFloat(elem); if (!isNaN(num)) return true; - + num = parseInt(elem); if (!isNaN(num)) return true; - + return false; }; - + let valid = false; let convertedValue = value; - + if (widget == WidgetType.CHECKBOX) { if (value == "True" || value == "False") { valid = true; @@ -203,7 +211,7 @@ return df`; } else if (widget == WidgetType.RANGE) { try { let list = JSON.parse(value); // checking if it is a valid array - + if ( list.length == 2 && isANumber(list[0]) && @@ -223,40 +231,40 @@ return df`; valid = true; convertedValue = '"' + value + '"'; // surrounding the value in quotes to be understood as a string } - + if (valid) { return { widget: widget, value: convertedValue }; } else { return {}; } }; - + // look for markers in this format [!! variable$widget$default !!] const resolveMarks = (userCode: string, currentWidgetsValues: any) => { const computeMark = (content: string, prevWidgetsValues: any) => { let config = content.split("$"); - + if (config.length < 3 || config.length > 4) { alert( "Invalid marker [!! " + - content + - " !!]. Markers must follow [!! variable$widget$default$arg1;arg2;arg3 !!]" + content + + " !!]. Markers must follow [!! variable$widget$default$arg1;arg2;arg3 !!]" ); return {}; } - + let args = undefined; - + if (config.length == 4) { args = config[3].split(";"); } - + if ( prevWidgetsValues[config[0]] != undefined && prevWidgetsValues[config[0]].widget == config[1] ) { // this is not a new marker, carry the previous value of the widget - + if (args != undefined) return { [config[0]]: { @@ -275,16 +283,16 @@ return df`; }; } else { let resolvedMark = validateWidgetValue(config[1], config[2]); // validate what comes from default values in the marks - + if (Object.keys(resolvedMark).length == 0) { alert( "Invalid widget and default value combination for [!! " + - content + - " !!]" + content + + " !!]" ); return {}; } - + if (args != undefined) return { [config[0]]: { @@ -303,19 +311,19 @@ return df`; }; } }; - + // Regular expression to match the content inside [!! !!] markers globally // @ts-ignore const regex = /\[\!\!\s*(.*?)\s*\!\!\]/g; - + let widgetsValues: any = {}; - + let errorReplacing = false; - + const replacedCode = userCode.replace(regex, (match, content) => { const param = computeMark(content, currentWidgetsValues); const atribs = Object.keys(param); - + if (atribs.length == 0) { errorReplacing = true; return ""; @@ -326,11 +334,11 @@ return df`; value: param[variable].value, args: param[variable].args, }; - + return param[variable].value as string; } }); - + if (errorReplacing) { alert("Could not resolve marks"); return {}; @@ -339,37 +347,37 @@ return df`; return widgetsValues; } }; - + const updateCurrentWidgets = () => { // Update currentWidgets (the user pressed exec) let div = document.getElementById( "widgetsEditor" + nodeId ) as HTMLElement; - + let inputs = div.querySelectorAll("input"); let selects = div.querySelectorAll("select"); - + let newCurrentWidgetsValues: any = {}; - + let validation = true; // flag to indicate if the input from the user was validated sucessfully - + // computing user input setNonValidatedValues((prev: any) => { let variables = Object.keys(prev); - + for (const elem of variables) { inputs.forEach(function (input) { let splitId = input.id.split(";"); - + let widget = splitId[0]; let variable = splitId[1]; - + if (elem == variable) { let validatedValue = validateWidgetValue( widget, prev[elem].value ); - + if (Object.keys(validatedValue).length != 0) { // correctly validated newCurrentWidgetsValues[elem] = { @@ -383,19 +391,19 @@ return df`; } } }); - + selects.forEach(function (select) { let splitId = select.id.split(";"); - + let widget = splitId[0]; let variable = splitId[1]; - + if (elem == variable) { let validatedValue = validateWidgetValue( widget, prev[elem].value ); - + if (Object.keys(validatedValue).length != 0) { // correctly validated newCurrentWidgetsValues[elem] = { @@ -410,51 +418,51 @@ return df`; } }); } - + // setCurrentWidgetsValues({...newCurrentWidgetsValues}); - + return prev; }); - + if (validation) { // computing marks and considering defaults let newWidgetsValues = resolveMarks( userCode, newCurrentWidgetsValues ); - + setNonValidatedValues({ ...newWidgetsValues }); } else { alert("Invalid input(s) for widget(s)"); } }; - + useEffect(() => { if (markersDirtyBypass.current) { updateCurrentWidgets(); } - + markersDirtyBypass.current = true; }, [markersDirty]); - + useEffect(() => { if (customWidgetsCallback != undefined) { const div = document.getElementById("widgetsEditor" + nodeId); - + customWidgetsCallback(div); } }, []); - + const inputChanged = (event: any) => { let splitId = event.target.id.split(";"); - + let widget = splitId[0]; let variable = splitId[1]; - + let variables = Object.keys(nonValidatedValues); - + let newNonValidatedValues: any = {}; - + for (const elem of variables) { if (elem == variable) { // updating the value of the interacted variable @@ -487,10 +495,10 @@ return df`; }; } } - + setNonValidatedValues(newNonValidatedValues); }; - + const getHTMLWidget = ( data: { widget: string; value: string; args: string[] | undefined }, key: number, @@ -616,21 +624,21 @@ return df`; > {data.args != undefined ? data.args.map((option: string, key2: number) => { - return ( - - ); - }) + return ( + + ); + }) : null} ); @@ -655,10 +663,10 @@ return df`; ); } }; - + // when user interacts with widgets currentWidgetsValues must be updated. Make sure to convert the input to the right type // validate input on the widgets every time the focus is out - + return (
- + {uploadResult && (
)}
- + {fileInfo && (
@@ -792,6 +800,5 @@ return df`;
); } - + export default WidgetsEditor; - \ No newline at end of file