Skip to content

Commit

Permalink
Add support for file upload in sheets
Browse files Browse the repository at this point in the history
  • Loading branch information
vegito22 committed Oct 28, 2024
1 parent 34d808b commit f3fe209
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 1 deletion.
3 changes: 3 additions & 0 deletions llmstack/assets/apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rest_framework.response import Response as DRFResponse

from llmstack.apps.models import AppDataAssets, AppSessionFiles
from llmstack.sheets.models import PromptlySheetFiles

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -113,6 +114,8 @@ def get_asset_data(
model_cls = AppSessionFiles
elif category == "appdata":
model_cls = AppDataAssets
elif category == "sheets":
model_cls = PromptlySheetFiles

if not model_cls:
logger.error(f"Invalid category for asset model: {category}")
Expand Down
98 changes: 98 additions & 0 deletions llmstack/client/src/components/sheets/Sheet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const SHEET_CELL_TYPE_TAGS = 3;
export const SHEET_CELL_TYPE_BOOLEAN = 4;
export const SHEET_CELL_TYPE_IMAGE = 5;
export const SHEET_CELL_TYPE_OBJECT = 6;
export const SHEET_CELL_TYPE_FILE = 7;

export const SHEET_CELL_STATUS_READY = 0;
export const SHEET_CELL_STATUS_RUNNING = 1;
Expand Down Expand Up @@ -93,6 +94,33 @@ export const sheetFormulaTypes = {
},
};

function memoize(func) {
const cache = {};
return function (...args) {
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
}
const result = func.apply(this, args);
cache[key] = result;
return result;
};
}

function objrefURL(objref) {
if (!objref) {
return "";
}
const objrefParts = objref.split("/");
if (objrefParts.length < 4) {
return "";
}
const objrefId = objrefParts[3];
return `${window.location.origin}/api/sheets/${objrefId}`;
}

const memoizedObjrefURL = memoize(objrefURL);

export const sheetCellTypes = {
[SHEET_CELL_TYPE_TEXT]: {
label: "Text",
Expand Down Expand Up @@ -326,6 +354,39 @@ export const sheetCellTypes = {
return cell.data ? JSON.parse(cell.data) : {};
},
},
[SHEET_CELL_TYPE_FILE]: {
label: "File",
value: "file",
description: "File Object",
kind: GridCellKind.Uri,
getDataGridCell: (cell, column) => {
if (!cell) {
return {
kind: GridCellKind.Uri,
data: "",
displayData: "",
readonly: column?.formula?.type > 0 || false,
allowOverlay: true,
allowWrapping: true,
hoverEffect: true,
};
}

let fileURL = memoizedObjrefURL(cell.value);
return {
kind: GridCellKind.Uri,
data: cell.value || "",
displayData: fileURL,
readonly: true,
allowOverlay: true,
allowWrapping: true,
hoverEffect: true,
};
},
getCellValue: (cell) => {
return cell.data;
},
},
};

const MemoizedSheetHeader = React.memo(SheetHeader);
Expand Down Expand Up @@ -1212,9 +1273,46 @@ function Sheet(props) {
setSheetRunning={setSheetRunning}
selectedRows={selectedRows}
deleteSelectedRows={deleteSelectedRows}
onFileAttach={({ files, startCell }) => {
if (files) {
const cellColumnLetter = startCell[0];
let cellRow = Number(startCell[1]);
let lastGridCell = null;
for (let i = 0; i < files.length; i++) {
const fileData = files[i];
const cellId = `${cellColumnLetter}${cellRow + i}`;
const gridCell = cellIdToGridCell(cellId, columns);
lastGridCell = gridCell;
const cell = {
id: cellId,
value: fileData?.data,
};
const column = columns[gridCell[0]];
if (gridCell) {
setCells((cells) => ({
...cells,
[cell.id]: {
...cells[cell.id],
row: gridCell[1] + 1,
col_letter: column.col_letter,
value: cell.value,
error: cells[cell.id]?.error || null,
status: cell.value ? 0 : cells[cell.id]?.status || 0,
},
}));
updateUserChanges("cells", cellId, cell);
sheetRef.current?.updateCells([{ cell: gridCell }]);
}
if (lastGridCell) {
sheetRef.current?.scrollTo(lastGridCell[0], lastGridCell[1]);
}
}
}
}}
runId={runId}
lastRunAt={lastRunAt}
selectedGrid={selectedGrid}
columns={columns}
/>
<Box
sx={{
Expand Down
58 changes: 58 additions & 0 deletions llmstack/client/src/components/sheets/SheetHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import DeleteIcon from "@mui/icons-material/Delete";
import HistoryIcon from "@mui/icons-material/History";
import AutorenewIcon from "@mui/icons-material/Autorenew";
import UploadIcon from "@mui/icons-material/Upload";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import { useNavigate } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
import moment from "moment";
import { axios } from "../../data/axios";
import UploadFileModal from "./UploadFileModal";
import PreviousRunsModal from "./PreviousRunsModal";
import ScheduleRunsModal from "./ScheduleRunsModal";
import SheetDeleteDialog from "./SheetDeleteDialog";
Expand All @@ -47,20 +49,39 @@ const SheetHeader = ({
setSheetRunning,
selectedRows,
deleteSelectedRows,
onFileAttach,
lastRunAt = null,
selectedGrid = [],
columns = [],
}) => {
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState(null);
const [open, setOpen] = useState(false);
const [isPreviousRunsModalOpen, setIsPreviousRunsModalOpen] = useState(false);
const [isScheduleRunsModalOpen, setIsScheduleRunsModalOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isAttachFileModalOpen, setIsAttachFileModalOpen] = useState(false);
const setSheets = useSetRecoilState(sheetsListSelector);
const [isSaving, setIsSaving] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const [isCommandPressed, setIsCommandPressed] = useState(false);
const [isExportSchemaModalOpen, setIsExportSchemaModalOpen] = useState(false);
const [isFileColumnGridSelected, setIsFileColumnGridSelected] =
useState(false);

useEffect(() => {
const fileColumnsLetters = columns
.filter((column) => column.cell_type === 7)
.map((column) => column.col_letter);
let fileColumnSelected = false;
for (let i = 0; i < selectedGrid.length; i++) {
if (fileColumnsLetters.includes(selectedGrid[i][0])) {
fileColumnSelected = true;
break;
}
}
setIsFileColumnGridSelected(fileColumnSelected);
}, [columns, selectedGrid]);

const getYamlSchemaFromSheet = useCallback(() => {
let schema = {};
Expand Down Expand Up @@ -323,6 +344,31 @@ const SheetHeader = ({
</span>
</Tooltip>
)}
{!sheetRunning && isFileColumnGridSelected && (
<div>
<Tooltip title="Upload File">
<span>
<Button
onClick={() => {
setIsAttachFileModalOpen(true);
}}
variant="contained"
size="medium"
sx={{
bgcolor: "gray.main",
"&:hover": { bgcolor: "white" },
color: "#999",
minWidth: "40px",
padding: "5px",
borderRadius: "4px !important",
}}
>
<AttachFileIcon />
</Button>
</span>
</Tooltip>
</div>
)}
{!sheetRunning && (
<div>
<Tooltip title="Download CSV">
Expand Down Expand Up @@ -478,6 +524,18 @@ const SheetHeader = ({
sheetUuid={sheet.uuid}
/>
)}
{isAttachFileModalOpen && (
<UploadFileModal
open={isAttachFileModalOpen}
onClose={({ files, startCell }) => {
setIsAttachFileModalOpen(false);
onFileAttach({ files, startCell });
}}
sheetUuid={sheet.uuid}
sheet={sheet}
selectedGrid={selectedGrid}
/>
)}
<SheetDeleteDialog
open={isDeleteDialogOpen}
onClose={() => setIsDeleteDialogOpen(false)}
Expand Down
136 changes: 136 additions & 0 deletions llmstack/client/src/components/sheets/UploadFileModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React, { useState, useRef } from "react";
import {
Dialog,
DialogTitle,
DialogContent,
Button,
DialogActions,
} from "@mui/material";
import { LoadingButton } from "@mui/lab";
import { axios } from "../../data/axios";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";

const getFileDataURI = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const base64Data = reader.result.split(",")[1];
const dataURI = `data:${file.type};name=${file.name};base64,${base64Data}`;

resolve({ file_name: file.name, mime_type: file.type, data: dataURI });
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
};

function InputFileUpload({ fileRef, maxFiles }) {
const [files, setFiles] = useState([]);
const handleFileChange = (event) => {
setFiles(Array.from(event.target.files).slice(0, maxFiles));
};

return (
<>
<Button
component="label"
role={undefined}
variant="contained"
tabIndex={-1}
startIcon={<CloudUploadIcon />}
>
Upload up to {maxFiles} files
<input
ref={fileRef}
style={{
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
}}
type="file"
onChange={handleFileChange}
multiple
/>
</Button>
<div style={{ marginTop: 10 }}>
{files &&
Array.from(files)
.slice(0, maxFiles)
.map((file, index) => (
<span
key={index}
style={{
display: "inline-block",
padding: "5px 10px",
margin: "5px",
backgroundColor: "#e0e0e0",
borderRadius: "15px",
}}
>
{file.name}
</span>
))}
</div>
</>
);
}

const UploadFileModal = ({ open, onClose, sheetUuid, selectedGrid }) => {
const fileRef = useRef(null);
const [isProcessing, setIsProcessing] = useState(false);
const maxFilesAllowed = 5;

return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>Upload Files</DialogTitle>
<DialogContent>
<InputFileUpload fileRef={fileRef} maxFiles={maxFilesAllowed} />
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={isProcessing}>
Cancel
</Button>
<LoadingButton
variant="contained"
type="primary"
onClick={() => {
const startCell = selectedGrid[0].split("-")[0];
setIsProcessing(true);
// Convert the file to a data URI
const fileConvertPromises = Array.from(fileRef.current.files)
.slice(0, maxFilesAllowed)
.map(getFileDataURI);
Promise.all(fileConvertPromises).then((dataURIs) => {
axios()
.post(`/api/sheets/${sheetUuid}/upload_assets`, {
files: dataURIs,
})
.then((response) => {
setIsProcessing(false);
onClose({
files: response.data,
startCell: startCell,
});
})
.finally(() => {
setIsProcessing(false);
});
});
}}
disabled={isProcessing}
loading={isProcessing}
>
Upload
</LoadingButton>
</DialogActions>
</Dialog>
);
};

export default UploadFileModal;
Loading

0 comments on commit f3fe209

Please sign in to comment.