Skip to content

Commit 0516f71

Browse files
committed
Allow downloading run sheet
1 parent f75ac5e commit 0516f71

File tree

3 files changed

+99
-1
lines changed

3 files changed

+99
-1
lines changed

llmstack/client/src/components/sheets/Sheet.jsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { enqueueSnackbar } from "notistack";
2222
import SaveIcon from "@mui/icons-material/Save";
2323
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
2424
import PauseIcon from "@mui/icons-material/Pause";
25+
import DownloadIcon from "@mui/icons-material/Download";
2526

2627
import "@glideapps/glide-data-grid/dist/index.css";
2728

@@ -51,7 +52,14 @@ const gridCellToCellId = (gridCell, columns) => {
5152
return `${colLetter}${rowIndex + 1}`;
5253
};
5354

54-
const SheetHeader = ({ sheet, setRunId, hasChanges, onSave, sheetRunning }) => {
55+
const SheetHeader = ({
56+
sheet,
57+
setRunId,
58+
hasChanges,
59+
onSave,
60+
sheetRunning,
61+
runId,
62+
}) => {
5563
const navigate = useNavigate();
5664

5765
const saveSheet = () => {
@@ -83,6 +91,10 @@ const SheetHeader = ({ sheet, setRunId, hasChanges, onSave, sheetRunning }) => {
8391
}
8492
};
8593

94+
const downloadSheet = () => {
95+
window.open(`/api/sheets/${sheet.uuid}/download`, "_blank");
96+
};
97+
8698
return (
8799
<Stack>
88100
<Typography variant="h5" className="section-header">
@@ -124,6 +136,18 @@ const SheetHeader = ({ sheet, setRunId, hasChanges, onSave, sheetRunning }) => {
124136
<SaveIcon />
125137
</Button>
126138
</Tooltip>
139+
{!sheetRunning && (
140+
<Tooltip title="Download CSV">
141+
<Button
142+
onClick={downloadSheet}
143+
color="primary"
144+
variant="outlined"
145+
sx={{ minWidth: "40px", padding: "5px", borderRadius: "4px" }}
146+
>
147+
<DownloadIcon />
148+
</Button>
149+
</Tooltip>
150+
)}
127151
<Tooltip
128152
title={
129153
sheetRunning ? "Sheet is already running" : "Run the sheet"
@@ -425,6 +449,7 @@ function Sheet(props) {
425449
hasChanges={hasChanges()}
426450
onSave={saveSheet}
427451
sheetRunning={sheetRunning}
452+
runId={runId}
428453
/>
429454
<Box>
430455
<DataEditor

llmstack/sheets/apis.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import csv
2+
import io
13
import logging
24
import uuid
35

6+
from django.http import StreamingHttpResponse
47
from rest_framework import status, viewsets
58
from rest_framework.permissions import IsAuthenticated
69
from rest_framework.response import Response as DRFResponse
@@ -18,6 +21,32 @@
1821
logger = logging.getLogger(__name__)
1922

2023

24+
def _get_sheet_csv(columns, cells, total_rows):
25+
output = io.StringIO()
26+
writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
27+
28+
# Write header
29+
writer.writerow([col.title for col in columns])
30+
yield output.getvalue()
31+
output.seek(0)
32+
output.truncate(0)
33+
34+
for row in range(1, total_rows + 1):
35+
# Create a dict of cell_id to cell from this row
36+
row_cells = {cell.cell_id: cell for cell in cells.values() if cell.row == row}
37+
38+
# Create a list of cell values for this row, using an empty string if the cell doesn't exist
39+
row_values = []
40+
for column in columns:
41+
cell = row_cells.get(f"{column.col}{row}")
42+
row_values.append(cell.display_data if cell else "")
43+
44+
writer.writerow(row_values)
45+
yield output.getvalue()
46+
output.seek(0)
47+
output.truncate(0)
48+
49+
2150
class PromptlySheetAppExecuteJob(ProcessingJob):
2251
@classmethod
2352
def generate_job_id(cls):
@@ -141,3 +170,39 @@ def run_async(self, request, sheet_uuid=None):
141170
).add_to_queue()
142171

143172
return DRFResponse({"job_id": job.id, "run_id": run_entry.uuid}, status=202)
173+
174+
def download(self, request, sheet_uuid=None):
175+
if not sheet_uuid:
176+
return DRFResponse(status=status.HTTP_400_BAD_REQUEST)
177+
178+
profile = Profile.objects.get(user=request.user)
179+
180+
sheet = PromptlySheet.objects.get(uuid=sheet_uuid, profile_uuid=profile.uuid)
181+
if not sheet:
182+
return DRFResponse(status=status.HTTP_404_NOT_FOUND)
183+
184+
response = StreamingHttpResponse(
185+
streaming_content=_get_sheet_csv(sheet.columns, sheet.cells, sheet.data.get("total_rows", 0)),
186+
content_type="text/csv",
187+
)
188+
response["Content-Disposition"] = f'attachment; filename="sheet_{sheet_uuid}.csv"'
189+
return response
190+
191+
def download_run(self, request, sheet_uuid=None, run_id=None):
192+
if not sheet_uuid or not run_id:
193+
return DRFResponse(status=status.HTTP_400_BAD_REQUEST)
194+
195+
profile = Profile.objects.get(user=request.user)
196+
197+
sheet = PromptlySheet.objects.get(uuid=sheet_uuid, profile_uuid=profile.uuid)
198+
run_entry = PromptlySheetRunEntry.objects.get(uuid=run_id, sheet_uuid=sheet.uuid)
199+
200+
if not run_entry:
201+
return DRFResponse(status=status.HTTP_404_NOT_FOUND)
202+
203+
response = StreamingHttpResponse(
204+
streaming_content=_get_sheet_csv(sheet.columns, sheet.cells, sheet.data.get("total_rows", 0)),
205+
content_type="text/csv",
206+
)
207+
response["Content-Disposition"] = f'attachment; filename="sheet_{sheet_uuid}_{run_id}.csv"'
208+
return response

llmstack/sheets/urls.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,12 @@
1212
"api/sheets/<str:sheet_uuid>/run",
1313
PromptlySheetViewSet.as_view({"post": "run_async"}),
1414
),
15+
path(
16+
"api/sheets/<str:sheet_uuid>/download",
17+
PromptlySheetViewSet.as_view({"get": "download"}),
18+
),
19+
path(
20+
"api/sheets/<str:sheet_uuid>/<str:run_id>/download",
21+
PromptlySheetViewSet.as_view({"get": "download_run"}),
22+
),
1523
]

0 commit comments

Comments
 (0)