From ae9b797abc4c1584a13d0b80703f5566951fa62c Mon Sep 17 00:00:00 2001 From: Shreya Shankar Date: Mon, 25 Nov 2024 16:42:35 -0800 Subject: [PATCH] fix: small errors in user study (#211) * feat: have global system prompt and decription * fix: don't auto collapse output schema in UI * fix: don't delete the sorting functionality, hide observability cols, and other nits * feat: add documentation * fix: fixing errors from user study --- website/package-lock.json | 25 ++-- website/package.json | 3 +- website/src/app/api/utils.ts | 24 ++-- .../src/app/api/writePipelineConfig/route.ts | 28 +++-- website/src/app/types.ts | 1 + website/src/components/OperationCard.tsx | 110 +++++++++++++----- website/src/components/Output.tsx | 2 +- website/src/components/PipelineGui.tsx | 18 ++- .../src/components/operations/components.tsx | 9 ++ website/src/contexts/PipelineContext.tsx | 18 ++- 10 files changed, 178 insertions(+), 60 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index a2bd553a..e4240d57 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -36,7 +36,7 @@ "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.59.15", "@tanstack/react-table": "^8.20.5", - "@tanstack/react-virtual": "^3.10.8", + "@tanstack/react-virtual": "^3.10.9", "@types/mime-types": "^2.1.4", "@types/react-resizable": "^3.0.8", "ai": "^3.4.29", @@ -52,6 +52,7 @@ "js-yaml": "^4.1.0", "json2csv": "^6.0.0-alpha.2", "lodash": "^4.17.21", + "lodash.throttle": "^4.1.1", "lucide-react": "^0.441.0", "next": "14.2.11", "re-resizable": "^6.10.0", @@ -3741,11 +3742,12 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.10.8", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz", - "integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==", + "version": "3.10.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.9.tgz", + "integrity": "sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==", + "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.10.8" + "@tanstack/virtual-core": "3.10.9" }, "funding": { "type": "github", @@ -3769,9 +3771,10 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.10.8", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz", - "integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==", + "version": "3.10.9", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz", + "integrity": "sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -9061,6 +9064,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", diff --git a/website/package.json b/website/package.json index bdfab8e5..26152b6f 100644 --- a/website/package.json +++ b/website/package.json @@ -37,7 +37,7 @@ "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.59.15", "@tanstack/react-table": "^8.20.5", - "@tanstack/react-virtual": "^3.10.8", + "@tanstack/react-virtual": "^3.10.9", "@types/mime-types": "^2.1.4", "@types/react-resizable": "^3.0.8", "ai": "^3.4.29", @@ -53,6 +53,7 @@ "js-yaml": "^4.1.0", "json2csv": "^6.0.0-alpha.2", "lodash": "^4.17.21", + "lodash.throttle": "^4.1.1", "lucide-react": "^0.441.0", "next": "14.2.11", "re-resizable": "^6.10.0", diff --git a/website/src/app/api/utils.ts b/website/src/app/api/utils.ts index 30bca6dd..d411a1e0 100644 --- a/website/src/app/api/utils.ts +++ b/website/src/app/api/utils.ts @@ -1,6 +1,5 @@ import yaml from "js-yaml"; import path from "path"; -import os from "os"; import { Operation, SchemaItem } from "@/app/types"; export function generatePipelineConfig( @@ -35,9 +34,14 @@ export function generatePipelineConfig( } // Fix the output schema of all operations to ensure correct typing - const updatedOperations: Record = operations.map( - (op: Operation) => { - const newOp: Record = { + const updatedOperations = operations + .map((op: Operation) => { + // Skip if visibility is false + if (!op.visibility) { + return null; + } + + const newOp: Record = { ...op, ...op.otherKwargs, }; @@ -54,6 +58,7 @@ export function generatePipelineConfig( delete newOp.otherKwargs; delete newOp.id; delete newOp.llmType; + delete newOp.visibility; // Convert numeric strings in otherKwargs to numbers Object.entries(newOp).forEach(([key, value]) => { @@ -119,7 +124,7 @@ export function generatePipelineConfig( if (op.type === "sample" && op.otherKwargs?.method === "custom") { try { newOp.samples = JSON.parse(op.otherKwargs.samples); - } catch (e) { + } catch (error) { console.warn( "Failed to parse custom samples as JSON, using raw value" ); @@ -139,8 +144,13 @@ export function generatePipelineConfig( ), }, }; - } - ); + }) + .filter((op) => op !== null); + + // Add check for empty operations + if (updatedOperations.length === 0) { + throw new Error("No valid operations found in pipeline configuration"); + } // Fetch all operations up until and including the operation_id const operationsToRun = operations.slice( diff --git a/website/src/app/api/writePipelineConfig/route.ts b/website/src/app/api/writePipelineConfig/route.ts index 7f32d7c6..20605e26 100644 --- a/website/src/app/api/writePipelineConfig/route.ts +++ b/website/src/app/api/writePipelineConfig/route.ts @@ -51,14 +51,28 @@ export async function POST(request: Request) { const pipelineDir = path.join(homeDir, ".docetl", "pipelines"); const configDir = path.join(pipelineDir, "configs"); const nameDir = path.join(pipelineDir, name, "intermediates"); - await fs.mkdir(configDir, { recursive: true }); - await fs.mkdir(nameDir, { recursive: true }); - const filePath = path.join(configDir, `${name}.yaml`); - await fs.writeFile(filePath, yamlString, "utf8"); - return NextResponse.json({ filePath, inputPath, outputPath }); + try { + await fs.mkdir(configDir, { recursive: true }); + await fs.mkdir(nameDir, { recursive: true }); + const filePath = path.join(configDir, `${name}.yaml`); + await fs.writeFile(filePath, yamlString, "utf8"); + + return NextResponse.json({ filePath, inputPath, outputPath }); + } catch (fsError) { + console.error("File system error:", fsError); + return NextResponse.json( + `Failed to write pipeline configuration: ${fsError.message}`, + { status: 500 } + ); + } } catch (error) { - console.error(error); - return NextResponse.json({ error: error }, { status: 500 }); + console.error("Pipeline configuration error:", error); + return NextResponse.json( + error instanceof Error + ? error.message + : "An unexpected error occurred while creating the pipeline configuration", + { status: 500 } + ); } } diff --git a/website/src/app/types.ts b/website/src/app/types.ts index 2b5ba26f..f3873789 100644 --- a/website/src/app/types.ts +++ b/website/src/app/types.ts @@ -30,6 +30,7 @@ export type Operation = { runIndex?: number; sample?: number; shouldOptimizeResult?: string; + visibility: boolean; }; export type OutputRow = Record; diff --git a/website/src/components/OperationCard.tsx b/website/src/components/OperationCard.tsx index 400827a3..18f3ab29 100644 --- a/website/src/components/OperationCard.tsx +++ b/website/src/components/OperationCard.tsx @@ -25,6 +25,8 @@ import { ListCollapse, Wand2, ChevronDown, + Eye, + EyeOff, } from "lucide-react"; import { Operation, SchemaItem } from "@/app/types"; import { usePipelineContext } from "@/contexts/PipelineContext"; @@ -62,6 +64,7 @@ const OperationHeader: React.FC<{ disabled: boolean; currOp: boolean; expanded: boolean; + visibility: boolean; optimizeResult?: string; onEdit: (name: string) => void; onDelete: () => void; @@ -71,6 +74,7 @@ const OperationHeader: React.FC<{ onOptimize: () => void; onAIEdit: (instruction: string) => void; onToggleExpand: () => void; + onToggleVisibility: () => void; }> = React.memo( ({ name, @@ -79,6 +83,7 @@ const OperationHeader: React.FC<{ disabled, currOp, expanded, + visibility, optimizeResult, onEdit, onDelete, @@ -88,6 +93,7 @@ const OperationHeader: React.FC<{ onOptimize, onAIEdit, onToggleExpand, + onToggleVisibility, }) => { const [isEditing, setIsEditing] = useState(false); const [editedName, setEditedName] = useState(name); @@ -105,7 +111,11 @@ const OperationHeader: React.FC<{ return (
{/* Left side buttons */} -
+
- - - - - - -

Show outputs

-
-
-
+
{/* Centered title */} -
+
{isEditing ? ( - {/* Right side delete button */} - + {/* Right side buttons */} +
+ + +
); } @@ -798,6 +832,18 @@ export const OperationCard: React.FC<{ index: number }> = ({ index }) => { [debouncedUpdate] ); + const handleVisibilityToggle = useCallback(() => { + if (!operation) return; + + const updatedOperation = { + ...operation, + visibility: + operation.visibility === undefined ? false : !operation.visibility, + }; + + handleOperationUpdate(updatedOperation); + }, [operation, handleOperationUpdate]); + if (!operation) { return ; } @@ -822,7 +868,7 @@ export const OperationCard: React.FC<{ index: number }> = ({ index }) => { pipelineOutput?.operationId === operation.id ? "bg-white border-blue-500 border-2" : "bg-white" - }`} + } ${!operation.visibility ? "opacity-50" : ""}`} > {/* Move the drag handle div outside of the ml-5 container */}
= ({ index }) => { disabled={isLoadingOutputs || pipelineOutput === undefined} currOp={operation.id === pipelineOutput?.operationId} expanded={isExpanded} + visibility={operation.visibility} optimizeResult={operation.shouldOptimizeResult} onEdit={(name) => { dispatch({ type: "UPDATE_NAME", payload: name }); @@ -857,8 +904,9 @@ export const OperationCard: React.FC<{ index: number }> = ({ index }) => { onOptimize={onOptimize} onAIEdit={handleAIEdit} onToggleExpand={() => dispatch({ type: "TOGGLE_EXPAND" })} + onToggleVisibility={handleVisibilityToggle} /> - {isExpanded && ( + {isExpanded && operation.visibility !== false && ( <> {createOperationComponent( diff --git a/website/src/components/Output.tsx b/website/src/components/Output.tsx index 5cbc4edf..7e5889b8 100644 --- a/website/src/components/Output.tsx +++ b/website/src/components/Output.tsx @@ -264,7 +264,7 @@ export const Output: React.FC = () => { }; fetchData(); - }, [output, operation, isLoadingOutputs]); + }, [output, isLoadingOutputs]); const columns: ColumnType[] = React.useMemo(() => { const importantColumns = operation?.output?.schema diff --git a/website/src/components/PipelineGui.tsx b/website/src/components/PipelineGui.tsx index 4ab8b70b..17d612bb 100644 --- a/website/src/components/PipelineGui.tsx +++ b/website/src/components/PipelineGui.tsx @@ -411,6 +411,7 @@ const PipelineGUI: React.FC = () => { validate, sample, otherKwargs, + visibility: true, } as Operation; }) ); @@ -614,6 +615,7 @@ const PipelineGUI: React.FC = () => { llmType, type: type as Operation["type"], name: `${name} ${numOpRun}`, + visibility: true, }; setOperations([...operations, newOperation]); }; @@ -689,7 +691,7 @@ const PipelineGUI: React.FC = () => { )} -
+
@@ -697,6 +699,7 @@ const PipelineGUI: React.FC = () => { variant="ghost" size="icon" onClick={() => fileInputRef.current?.click()} + className="h-8 w-8" > @@ -720,6 +723,7 @@ const PipelineGUI: React.FC = () => { size="icon" variant="ghost" onClick={() => handleExport()} + className="h-8 w-8" > @@ -734,14 +738,22 @@ const PipelineGUI: React.FC = () => { size="icon" variant="ghost" onClick={() => setIsSettingsOpen(true)} + className="h-8 w-8" > - diff --git a/website/src/components/operations/components.tsx b/website/src/components/operations/components.tsx index e2b090d1..4771828e 100644 --- a/website/src/components/operations/components.tsx +++ b/website/src/components/operations/components.tsx @@ -1277,6 +1277,15 @@ export const CodeOperationComponent: React.FC = ({ operation, onUpdate, }) => { + useEffect(() => { + if ( + operation.type === "code_reduce" && + !operation.otherKwargs?.reduce_key + ) { + handleReduceKeysChange(["_all"]); + } + }, []); + const handleCodeChange = (newCode: string) => { onUpdate({ ...operation, diff --git a/website/src/contexts/PipelineContext.tsx b/website/src/contexts/PipelineContext.tsx index 6970ea7f..b5c13dfc 100644 --- a/website/src/contexts/PipelineContext.tsx +++ b/website/src/contexts/PipelineContext.tsx @@ -139,10 +139,24 @@ const serializeState = async (state: PipelineState): Promise => { .map((row: Record) => { const sampleRow: Record = {}; + // Helper function to safely stringify values + const safeStringify = (value: unknown): string => { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "object") { + try { + return JSON.stringify(value); + } catch { + return "[Complex Object]"; + } + } + return String(value); + }; + // Prioritize important columns importantColumns.forEach((col) => { if (col in row) { - const value = String(row[col]); + const value = safeStringify(row[col]); if (value.length > 10000) { sampleRow[`**${col}**`] = `**${value.slice(0, 10000)}` + @@ -157,7 +171,7 @@ const serializeState = async (state: PipelineState): Promise => { Object.keys(row).forEach((key) => { if (!(key in sampleRow)) { // Only add if not already added - const value = String(row[key]); + const value = safeStringify(row[key]); if (value.length > 10000) { sampleRow[key] = value.slice(0, 10000) +