Skip to content

Commit

Permalink
cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyphilemon committed Jan 30, 2025
1 parent 796c9c0 commit ad6b2be
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 179 deletions.
44 changes: 29 additions & 15 deletions blocks/sheet.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Block } from '@/components/create-block';
import {
DownloadIcon,
MessageIcon,
CopyIcon,
LineChartIcon,
RedoIcon,
SparklesIcon,
UndoIcon,
} from '@/components/icons';
import { SpreadsheetEditor } from '@/components/sheet-editor';
import { exportToCSV } from '@/lib/spreadsheet';
import { parse, unparse } from 'papaparse';
import { toast } from 'sonner';

interface Metadata {}
Expand Down Expand Up @@ -73,29 +73,43 @@ export const sheetBlock = new Block<'sheet', Metadata>({
},
},
{
icon: <DownloadIcon />,
description: 'Export',
icon: <CopyIcon />,
description: 'Copy as .csv',
onClick: ({ content }) => {
try {
exportToCSV(content);
toast.success('CSV file downloaded!');
} catch (error) {
console.error(error);
toast.error('Failed to export CSV');
}
const parsed = parse<string[]>(content, { skipEmptyLines: true });

const nonEmptyRows = parsed.data.filter((row) =>
row.some((cell) => cell.trim() !== ''),
);

const cleanedCsv = unparse(nonEmptyRows);

navigator.clipboard.writeText(cleanedCsv);
toast.success('Copied csv to clipboard!');
},
},
],
toolbar: [
{
onClick: () => {},
description: 'Format and clean data',
icon: <SparklesIcon />,
onClick: ({ appendMessage }) => {
appendMessage({
role: 'user',
content: 'Can you please format and clean the data?',
});
},
},
{
onClick: () => {},
description: 'Analyze and visualize data',
icon: <MessageIcon />,
icon: <LineChartIcon />,
onClick: ({ appendMessage }) => {
appendMessage({
role: 'user',
content:
'Can you please analyze and visualize the data by creating a new code block in python?',
});
},
},
],
});
2 changes: 1 addition & 1 deletion components/document-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ const DocumentContent = ({ document }: { document: Document }) => {
</div>
</div>
) : document.kind === 'sheet' ? (
<div className="flex flex-1 relative w-full p-4">
<div className="flex flex-1 relative size-full p-4">
<div className="absolute inset-0">
<SpreadsheetEditor {...commonProps} />
</div>
Expand Down
17 changes: 17 additions & 0 deletions components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1136,3 +1136,20 @@ export const DownloadIcon = ({ size = 16 }: { size?: number }) => (
></path>
</svg>
);

export const LineChartIcon = ({ size = 16 }: { size?: number }) => (
<svg
height={size}
strokeLinejoin="round"
viewBox="0 0 16 16"
width={size}
style={{ color: 'currentcolor' }}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1v11.75A2.25 2.25 0 0 0 3.25 15H15v-1.5H3.25a.75.75 0 0 1-.75-.75V1H1Zm13.297 5.013.513-.547-1.094-1.026-.513.547-3.22 3.434-2.276-2.275a1 1 0 0 0-1.414 0L4.22 8.22l-.53.53 1.06 1.06.53-.53L7 7.56l2.287 2.287a1 1 0 0 0 1.437-.023l3.573-3.811Z"
clipRule="evenodd"
></path>
</svg>
);
141 changes: 87 additions & 54 deletions components/sheet-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,105 +1,138 @@
'use client';

import React, { memo, useMemo } from 'react';
import DataGrid from 'react-data-grid';
import React, { memo, useEffect, useMemo, useState } from 'react';
import DataGrid, { textEditor } from 'react-data-grid';
import 'react-data-grid/lib/styles.css';
import { parse, unparse } from 'papaparse';
import { useTheme } from 'next-themes';
import { cn } from '@/lib/utils';

interface SheetEditorProps {
type SheetEditorProps = {
content: string;
saveContent: (updatedContent: string, debounce: boolean) => void;
status: 'streaming' | 'idle';
saveContent: (content: string, isCurrentVersion: boolean) => void;
status: string;
isCurrentVersion: boolean;
currentVersionIndex: number;
}
};

const MIN_ROWS = 50;
const MIN_COLS = 26;

const PureSpreadsheetEditor = ({
content,
saveContent,
status,
isCurrentVersion,
}: SheetEditorProps) => {
const parseData = (csvContent: string) => {
if (!csvContent) return null;
const result = parse<string[]>(csvContent, { skipEmptyLines: true });
return result.data;
};
const { theme } = useTheme();

const parseData = useMemo(() => {
if (!content) return Array(MIN_ROWS).fill(Array(MIN_COLS).fill(''));
const result = parse<string[]>(content, { skipEmptyLines: true });

const paddedData = result.data.map((row) => {
const paddedRow = [...row];
while (paddedRow.length < MIN_COLS) {
paddedRow.push('');
}
return paddedRow;
});

const generateCsv = (data: any[][]) => {
return unparse(data);
};
while (paddedData.length < MIN_ROWS) {
paddedData.push(Array(MIN_COLS).fill(''));
}

const rawData = parseData(content);
return paddedData;
}, [content]);

const columns = useMemo(() => {
if (!rawData || rawData.length === 0) return [];

const columnCount = Math.max(...rawData.map((row) => row.length));
return Array.from({ length: columnCount }, (_, i) => ({
const rowNumberColumn = {
key: 'rowNumber',
name: '',
frozen: true,
width: 50,
renderCell: ({ rowIdx }: { rowIdx: number }) => rowIdx + 1,
cellClass: 'border-t border-r dark:bg-zinc-950 dark:text-zinc-50',
headerCellClass: 'border-t border-r dark:bg-zinc-900 dark:text-zinc-50',
};

const dataColumns = Array.from({ length: MIN_COLS }, (_, i) => ({
key: i.toString(),
name: String.fromCharCode(65 + i),
editor: 'textEditor',
editable: true,
renderEditCell: textEditor,
width: 120,
cellClass: cn(`border-t dark:bg-zinc-950 dark:text-zinc-50`, {
'border-l': i !== 0,
}),
headerCellClass: cn(`border-t dark:bg-zinc-900 dark:text-zinc-50`, {
'border-l': i !== 0,
}),
}));
}, [rawData]);

const rows = useMemo(() => {
if (!rawData) return [];
return [rowNumberColumn, ...dataColumns];
}, []);

return rawData.map((row, rowIndex) => {
const rowData: any = { id: rowIndex };
const initialRows = useMemo(() => {
return parseData.map((row, rowIndex) => {
const rowData: any = {
id: rowIndex,
rowNumber: rowIndex + 1,
};

columns.forEach((col, colIndex) => {
columns.slice(1).forEach((col, colIndex) => {
rowData[col.key] = row[colIndex] || '';
});

return rowData;
});
}, [rawData, columns]);
}, [parseData, columns]);

function onCellEdit(rowIndex: number, columnKey: string, newValue: string) {
if (!isCurrentVersion) return;
const [localRows, setLocalRows] = useState(initialRows);

const newRows = [...rows];
newRows[rowIndex] = { ...newRows[rowIndex], [columnKey]: newValue };
useEffect(() => {
setLocalRows(initialRows);
}, [initialRows]);

// Convert the rows back to 2D array format
const newData = newRows.map((row) =>
columns.map((col) => row[col.key] || ''),
);
const generateCsv = (data: any[][]) => {
return unparse(data);
};

const handleRowsChange = (newRows: any[]) => {
// Immediately update local state
setLocalRows(newRows);

const updatedCsv = generateCsv(newData);
saveContent(updatedCsv, true);
}
// Still trigger the save operation
const updatedData = newRows.map((row) => {
return columns.slice(1).map((col) => row[col.key] || '');
});

const newCsvContent = generateCsv(updatedData);
saveContent(newCsvContent, true);
};

return rawData ? (
return (
<div style={{ height: '100%', width: '100%' }}>
<DataGrid
className={theme === 'dark' ? 'rdg-dark' : 'rdg-light'}
columns={columns}
rows={rows}
rows={localRows}
enableVirtualization
onRowsChange={handleRowsChange}
onCellClick={(args) => {
args.selectCell();
}}
onCellKeyDown={(args) => {
if (args.mode !== 'EDIT' && args.row.id !== undefined) {
args.enableEditMode();
}
}}
onRowsChange={(newRows, { indexes, column }) => {
if (indexes.length === 1) {
const rowIndex = indexes[0];
const newValue = newRows[rowIndex][column.key];
onCellEdit(rowIndex, column.key, newValue);
if (args.column.key !== 'rowNumber') {
args.selectCell(true);
}
}}
style={{ height: '100%' }}
defaultColumnOptions={{
resizable: true,
sortable: true,
}}
rowHeight={35}
headerRowHeight={35}
/>
</div>
) : null;
);
};

function areEqual(prevProps: SheetEditorProps, nextProps: SheetEditorProps) {
Expand Down
8 changes: 7 additions & 1 deletion lib/ai/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,10 @@ Improve the following code snippet based on the given prompt.
${currentContent}
`
: '';
: type === 'sheet'
? `\
Improve the following spreadsheet based on the given prompt.
${currentContent}
`
: '';
2 changes: 1 addition & 1 deletion lib/ai/tools/create-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const createDocument = ({
}: CreateDocumentProps) =>
tool({
description:
'Create a document for a writing or content creation activities like image generation. This tool will call other functions that will generate the contents of the document based on the title and kind.',
'Create a document for a writing or content creation activities. This tool will call other functions that will generate the contents of the document based on the title and kind.',
parameters: z.object({
title: z.string(),
kind: z.enum(['text', 'code', 'image', 'sheet']),
Expand Down
60 changes: 8 additions & 52 deletions lib/ai/tools/update-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,73 +125,29 @@ export const updateDocument = ({

dataStream.writeData({ type: 'finish', content: '' });
} else if (document.kind === 'sheet') {
// Parse the current content as spreadsheet data
let currentSpreadsheetData = { headers: [], rows: [] };
try {
if (currentContent) {
currentSpreadsheetData = JSON.parse(currentContent);
}
} catch {
// Keep default empty structure
}

const { fullStream } = streamObject({
model: customModel(model.apiIdentifier),
system: `You are a spreadsheet manipulation assistant. The current spreadsheet has the following structure:
Headers: ${JSON.stringify(currentSpreadsheetData.headers)}
Current rows: ${JSON.stringify(currentSpreadsheetData.rows)}
When modifying the spreadsheet:
1. You can add, remove, or modify columns (headers)
2. When adding columns, add empty values to existing rows for the new columns
3. When removing columns, remove the corresponding values from all rows
4. Return the COMPLETE spreadsheet data including ALL headers and rows
5. Format response as valid JSON with 'headers' and 'rows' arrays
Example response format:
{"headers":["Name","Email","Phone"],"rows":[["John","[email protected]","123-456-7890"],["Jane","[email protected]","098-765-4321"]]}`,
system: updateDocumentPrompt(currentContent, 'sheet'),
prompt: description,
schema: z.object({
headers: z
.array(z.string())
.describe('Column headers for the spreadsheet'),
rows: z.array(z.array(z.string())).describe('Sample data rows'),
csv: z.string(),
}),
});

draftText = JSON.stringify(currentSpreadsheetData);

for await (const delta of fullStream) {
const { type } = delta;

if (type === 'object') {
const { object } = delta;
if (
object &&
Array.isArray(object.headers) &&
Array.isArray(object.rows)
) {
// Validate and normalize the data
const headers = object.headers.map((h: any) => String(h || ''));
const rows = object.rows.map(
(row: (string | undefined)[] | undefined) => {
const normalizedRow = (row || []).map((cell: any) =>
String(cell || ''),
);
// Ensure row length matches new headers length
while (normalizedRow.length < headers.length) {
normalizedRow.push('');
}
return normalizedRow.slice(0, headers.length);
},
);

const newData = { headers, rows };
draftText = JSON.stringify(newData);
const { csv } = object;

if (csv) {
dataStream.writeData({
type: 'sheet-delta',
content: draftText,
content: csv,
});

draftText = csv;
}
}
}
Expand Down
Loading

0 comments on commit ad6b2be

Please sign in to comment.