diff --git a/website/src/app/api/downloadTutorialDataset/route.ts b/website/src/app/api/downloadTutorialDataset/route.ts new file mode 100644 index 00000000..25703ec8 --- /dev/null +++ b/website/src/app/api/downloadTutorialDataset/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from "next/server"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const fileId = searchParams.get("fileId"); + + if (!fileId) { + return new NextResponse("File ID is required", { status: 400 }); + } + + try { + const driveUrl = `https://drive.google.com/uc?export=download&id=${fileId}`; + const response = await fetch(driveUrl); + + if (!response.ok) { + throw new Error("Failed to download file from Google Drive"); + } + + const data = await response.blob(); + return new NextResponse(data); + } catch (error) { + console.error("Error downloading tutorial dataset:", error); + return new NextResponse("Failed to download tutorial dataset", { + status: 500, + }); + } +} diff --git a/website/src/app/api/utils.ts b/website/src/app/api/utils.ts index 291f013c..261dd4ee 100644 --- a/website/src/app/api/utils.ts +++ b/website/src/app/api/utils.ts @@ -193,27 +193,14 @@ export function generatePipelineConfig( ); // Fix type errors by asserting the pipeline config type - const pipelineConfig: { - datasets: any; - default_model: string; - optimizer_config: any; - operations: any[]; - pipeline: { - steps: any[]; - output: { - type: string; - path: string; - intermediate_dir: string; - }; - }; - system_prompt: Record; - llm_api_keys?: Record; - } = { + const pipelineConfig: any = { datasets, default_model, - optimizer_config: { - force_decompose: true, - }, + ...(enable_observability && { + optimizer_config: { + force_decompose: true, + }, + }), operations: updatedOperations, pipeline: { steps: [ diff --git a/website/src/app/globals.css b/website/src/app/globals.css index 745c64ad..308516fd 100644 --- a/website/src/app/globals.css +++ b/website/src/app/globals.css @@ -190,3 +190,31 @@ .news-ticker:hover { cursor: pointer; } + +/* Add these styles */ +[data-panel] { + transition: none; /* Remove transition during resize */ +} + +[data-panel-group] { + transition: width 50ms ease, height 50ms ease; +} + +/* Replace the resize handle styles with these cleaner versions */ +[data-resize-handle] { + position: relative; +} + +[data-resize-handle][data-dragging="true"] { + background-color: rgb(96 165 250); /* blue-400 */ +} + +[data-dragging="true"] { + pointer-events: none; + user-select: none; +} + +/* Remove the previous ::before pseudo-element styles */ +[data-dragging="true"]::before { + display: none; +} diff --git a/website/src/app/playground/page.tsx b/website/src/app/playground/page.tsx index 22db7ee8..22e08b39 100644 --- a/website/src/app/playground/page.tsx +++ b/website/src/app/playground/page.tsx @@ -1,7 +1,7 @@ "use client"; import dynamic from "next/dynamic"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef, Suspense } from "react"; import { Scroll, Info, Save, Monitor, AlertCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -30,6 +30,11 @@ const DatasetView = dynamic( () => import("@/components/DatasetView").then((mod) => mod.default), { ssr: false, + loading: () => ( +
+
+
+ ), } ); const PipelineGUI = dynamic( @@ -99,6 +104,7 @@ const NamespaceDialog = dynamic( ); import { ThemeProvider, useTheme, Theme } from "@/contexts/ThemeContext"; import { APIKeysDialog } from "@/components/APIKeysDialog"; +import { TutorialsDialog, TUTORIALS } from "@/components/TutorialsDialog"; const LeftPanelIcon: React.FC<{ isActive: boolean }> = ({ isActive }) => ( (
); +const PerformanceWrapper: React.FC<{ + children: React.ReactNode; + className?: string; +}> = ({ children, className }) => { + const [isDragging, setIsDragging] = useState(false); + const [size, setSize] = useState<{ width: number; height: number }>(); + const ref = useRef(null); + + // Capture size on mount and resize + useEffect(() => { + if (ref.current) { + const observer = new ResizeObserver((entries) => { + if (!isDragging) { + const { width, height } = entries[0].contentRect; + setSize({ width, height }); + } + }); + + observer.observe(ref.current); + return () => observer.disconnect(); + } + }, [isDragging]); + + return ( +
+ {children} +
+ ); +}; + const CodeEditorPipelineApp: React.FC = () => { const [isLoading, setIsLoading] = useState(true); const [isMobileView, setIsMobileView] = useState(false); @@ -216,6 +259,9 @@ const CodeEditorPipelineApp: React.FC = () => { const [showChat, setShowChat] = useState(false); const [showNamespaceDialog, setShowNamespaceDialog] = useState(false); const [showAPIKeysDialog, setShowAPIKeysDialog] = useState(false); + const [showTutorialsDialog, setShowTutorialsDialog] = useState(false); + const [selectedTutorial, setSelectedTutorial] = + useState<(typeof TUTORIALS)[0]>(); const { theme, setTheme } = useTheme(); const { @@ -229,6 +275,10 @@ const CodeEditorPipelineApp: React.FC = () => { unsavedChanges, namespace, setNamespace, + setOperations, + setPipelineName, + setSampleSize, + setDefaultModel, } = usePipelineContext(); useEffect(() => { @@ -358,8 +408,10 @@ const CodeEditorPipelineApp: React.FC = () => { const panelToggleStyles = "flex items-center gap-2 px-3 py-1.5 rounded-md transition-colors duration-200"; const mainContentStyles = "flex-grow overflow-hidden bg-gray-50"; - const resizeHandleStyles = - "w-2 bg-gray-100 hover:bg-blue-200 transition-colors duration-200"; + const resizeHandleStyles = ` + w-2 bg-gray-100 hover:bg-blue-200 transition-colors duration-200 + data-[dragging=true]:bg-blue-400 + `; return ( @@ -446,6 +498,22 @@ const CodeEditorPipelineApp: React.FC = () => { > Show Documentation + + Tutorials + + {TUTORIALS.map((tutorial) => ( + { + setSelectedTutorial(tutorial); + setShowTutorialsDialog(true); + }} + > + {tutorial.title} + + ))} + + setShowChat(!showChat)}> Show Chat @@ -576,10 +644,17 @@ const CodeEditorPipelineApp: React.FC = () => { (document.body.style.cursor = "col-resize")} + onDragEnd={() => (document.body.style.cursor = "default")} > {showFileExplorer && ( - + (document.body.style.cursor = "row-resize")} + onDragEnd={() => (document.body.style.cursor = "default")} + > { )} - + (document.body.style.cursor = "row-resize")} + onDragEnd={() => (document.body.style.cursor = "default")} + > - + + + {showOutput && ( @@ -637,7 +719,9 @@ const CodeEditorPipelineApp: React.FC = () => { minSize={20} className="overflow-auto" > - + + + )} @@ -651,7 +735,17 @@ const CodeEditorPipelineApp: React.FC = () => { minSize={10} className="h-full overflow-auto" > - + + +
+
+ } + > + +
+
)} @@ -670,6 +764,23 @@ const CodeEditorPipelineApp: React.FC = () => { open={showAPIKeysDialog} onOpenChange={setShowAPIKeysDialog} /> + + setFiles((prevFiles) => [...prevFiles, file]) + } + setCurrentFile={setCurrentFile} + setOperations={setOperations} + setPipelineName={setPipelineName} + setSampleSize={setSampleSize} + setDefaultModel={setDefaultModel} + setFiles={setFiles} + currentFile={currentFile} + files={files} + />
); diff --git a/website/src/app/types.ts b/website/src/app/types.ts index b7f85f2c..fd6768d7 100644 --- a/website/src/app/types.ts +++ b/website/src/app/types.ts @@ -1,8 +1,9 @@ export type File = { name: string; path: string; - type: "json" | "document"; + type: "json" | "document" | "pipeline-yaml"; parentFolder?: string; + blob?: Blob; }; export type Operation = { diff --git a/website/src/components/DatasetView.tsx b/website/src/components/DatasetView.tsx index f793b8cc..f67724cb 100644 --- a/website/src/components/DatasetView.tsx +++ b/website/src/components/DatasetView.tsx @@ -21,6 +21,7 @@ import { import { ChevronRight } from "lucide-react"; import { Database } from "lucide-react"; import { File } from "@/app/types"; +import { cn } from "@/lib/utils"; interface FileChunk { content: string; @@ -408,7 +409,11 @@ const DatasetView: React.FC<{ file: File | null }> = ({ file }) => {
{keys.map((key) => ( - + {key} ))} diff --git a/website/src/components/FileExplorer.tsx b/website/src/components/FileExplorer.tsx index d1b9015f..6026a910 100644 --- a/website/src/components/FileExplorer.tsx +++ b/website/src/components/FileExplorer.tsx @@ -54,6 +54,7 @@ import { TooltipTrigger, } from "./ui/tooltip"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { useDatasetUpload } from "@/hooks/useDatasetUpload"; interface FileExplorerProps { files: File[]; @@ -211,12 +212,17 @@ export const FileExplorer: React.FC = ({ const [draggedFiles, setDraggedFiles] = useState(0); const [viewingDocument, setViewingDocument] = useState(null); const [folderToDelete, setFolderToDelete] = useState(null); - const [uploadingFiles, setUploadingFiles] = useState>(new Set()); const [conversionMethod, setConversionMethod] = useState("local"); const [azureEndpoint, setAzureEndpoint] = useState(""); const [azureKey, setAzureKey] = useState(""); + const { uploadingFiles, uploadDataset } = useDatasetUpload({ + namespace, + onFileUpload, + setCurrentFile, + }); + // Group files by folder const groupedFiles = files.reduce((acc: { [key: string]: File[] }, file) => { const folder = file.parentFolder || "root"; @@ -243,66 +249,13 @@ export const FileExplorer: React.FC = ({ console.log("No file selected"); return; } - - if (!uploadedFile.name.toLowerCase().endsWith(".json")) { - toast({ - variant: "destructive", - title: "Error", - description: "Please upload a JSON file", - }); - return; - } - - setUploadingFiles((prev) => new Set(prev).add(uploadedFile.name)); - - try { - // Validate JSON structure before uploading - await validateJsonDataset(uploadedFile); - - const formData = new FormData(); - formData.append("file", uploadedFile); - formData.append("namespace", namespace); - - const response = await fetch("/api/uploadFile", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - throw new Error("Upload failed"); - } - - const data = await response.json(); - - const newFile = { - name: uploadedFile.name, - path: data.path, - type: "json" as const, - parentFolder: "root", - }; - - onFileUpload(newFile); - setCurrentFile(newFile); - - toast({ - title: "Success", - description: "Dataset uploaded successfully", - }); - } catch (error) { - console.error(error); - toast({ - variant: "destructive", - title: "Error", - description: - error instanceof Error ? error.message : "Failed to upload file", - }); - } finally { - setUploadingFiles((prev) => { - const next = new Set(prev); - next.delete(uploadedFile.name); - return next; - }); - } + const fileToUpload: File = { + name: uploadedFile.name, + path: uploadedFile.name, + type: "json", + blob: uploadedFile, + }; + await uploadDataset(fileToUpload); }; const handleFileSelection = (file: File) => { @@ -567,13 +520,26 @@ export const FileExplorer: React.FC = ({ }} className="hidden" id="file-upload" + disabled={uploadingFiles.size > 0} />
diff --git a/website/src/components/OperationCard.tsx b/website/src/components/OperationCard.tsx index cc4033e4..ddb60ff7 100644 --- a/website/src/components/OperationCard.tsx +++ b/website/src/components/OperationCard.tsx @@ -138,12 +138,18 @@ const OperationHeader: React.FC = React.memo( const [editedModel, setEditedModel] = useState(model); return ( -
- {/* Operation Type Badge and Optimization Status (The "Noun") */} +
+ {/* Left side - Operation info */}
{type} + {/* Add help button for LLM operations */} + {llmType === "LLM" && + (type === "map" || type === "reduce" || type === "filter") && ( + + )} + {canBeOptimized(type) && optimizeResult !== undefined && ( @@ -253,28 +259,48 @@ const OperationHeader: React.FC = React.memo( )}
- {/* Action Menu (The "Verb") */} -
- {/* Add help button for LLM operations */} - {llmType === "LLM" && - (type === "map" || type === "reduce" || type === "filter") && ( - - )} + {/* Action Bar - Keep only the most essential actions */} +
+ {/* Show Outputs Button */} + + + {/* LLM-specific Actions */} + {llmType === "LLM" && ( + + )} + {/* More Options Menu */} -
- {/* Add Move Up/Down buttons before other actions */} + {/* Move operation actions */} {!isFirst && ( - - {/* LLM-specific Actions */} + {/* LLM-specific menu items */} {llmType === "LLM" && ( <> )} - - +
)} - {/* Operation-specific Actions */} - + {/* Optimization in menu for supported types */} {canBeOptimized(type) && ( )} + {/* Settings */} -
- {/* Visibility Toggle */} +
+ {/* Delete Operation */}
-
- {/* Expand/Collapse Button */} - + {/* Expand/Collapse Button */} + +
); } @@ -1147,10 +1152,10 @@ export const OperationCard: React.FC = ({ index, id }) => { return (
= ({ index, id }) => { }; const SkeletonCard: React.FC = () => ( - + diff --git a/website/src/components/OperationHelpButton.tsx b/website/src/components/OperationHelpButton.tsx index 6f77a402..6c409f36 100644 --- a/website/src/components/OperationHelpButton.tsx +++ b/website/src/components/OperationHelpButton.tsx @@ -1,7 +1,7 @@ import React from "react"; import { HelpCircle, Copy, Check } from "lucide-react"; import { Button } from "./ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "./ui/hover-card"; interface OperationHelpButtonProps { type: string; @@ -57,7 +57,7 @@ export const OperationHelpButton: React.FC = ({ />

Or reference specific fields (e.g., if your document has a - "text" field): + “text” field):

= ({ />

Or reference specific fields (e.g., if your document has a - "content" field): + “content” field):

= ({

- The prompt runs once per group of documents. Each document in - the group is available in the{" "} + The reduce operation groups documents by a “reduce + key” (like a SQL GROUP BY), then runs the prompt once per + group. The reduce key can be one or more columns - documents + with the same values for these columns will be processed + together. +

+

+ Use “_all” as the reduce key to process all + documents in a single group. +

+

+ Each document in the group is available in the{" "} inputs list, and you can reference specific fields with dot notation:

@@ -156,7 +166,7 @@ export const OperationHelpButton: React.FC = ({ />

Or reference specific fields (e.g., if your documents have a - "title" field): + “title” field):

= ({

The schema defines the columns for a new row that represents - the entire group: + the entire group. Each group (determined by the reduce key) + will produce one new row containing these output columns:

Column: combined_analysis @@ -181,8 +192,9 @@ export const OperationHelpButton: React.FC = ({ Type: string

- Each group will produce one new row containing just these - output columns. + For example, if you group by “category”, + you'll get one output row for each unique category value, + summarizing all documents in that category.

@@ -194,15 +206,15 @@ export const OperationHelpButton: React.FC = ({ }; return ( - - - - - + +
-

Example Prompts

+

Operator Guide

{getExamplePrompt()}

@@ -223,7 +235,7 @@ export const OperationHelpButton: React.FC = ({

-
-
+ + ); }; diff --git a/website/src/components/Output.tsx b/website/src/components/Output.tsx index ef130414..032cfbba 100644 --- a/website/src/components/Output.tsx +++ b/website/src/components/Output.tsx @@ -84,6 +84,8 @@ const useOutputContext = () => { terminalOutput, setTerminalOutput, optimizerProgress, + sampleSize, + operations, } = usePipelineContext(); return { @@ -92,6 +94,8 @@ const useOutputContext = () => { terminalOutput, setTerminalOutput, optimizerProgress, + sampleSize, + operations, }; }; @@ -474,7 +478,8 @@ ConsoleContent.displayName = "ConsoleContent"; // Main Output component export const Output = memo(() => { - const { output, isLoadingOutputs } = useOutputContext(); + const { output, isLoadingOutputs, sampleSize, operations } = + useOutputContext(); const operation = useOperation(output?.operationId); const [outputs, setOutputs] = useState([]); @@ -561,7 +566,7 @@ export const Output = memo(() => { [inputCount, outputCount] ); - // Add back the data fetching effect + // Update the data fetching effect useEffect(() => { const fetchData = async () => { if (output && !isLoadingOutputs) { @@ -612,8 +617,13 @@ export const Output = memo(() => { setOutputs(parsedOutputs); - // Fetch input data if inputPath exists - if (output.inputPath) { + // Check if this is the first operation + const isFirstOperation = operation?.id === operations[0]?.id; + + // Set input count based on whether it's the first operation + if (isFirstOperation && sampleSize !== null) { + setInputCount(sampleSize); + } else if (output.inputPath) { const inputResponse = await fetch( `/api/readFile?path=${output.inputPath}` ); @@ -635,7 +645,14 @@ export const Output = memo(() => { }; fetchData(); - }, [output, operation?.otherKwargs?.prompts, isLoadingOutputs]); + }, [ + output, + operation?.otherKwargs?.prompts, + isLoadingOutputs, + operations, + operation?.id, + sampleSize, + ]); return (
diff --git a/website/src/components/PipelineGui.tsx b/website/src/components/PipelineGui.tsx index 31e6d888..5c7fdaa2 100644 --- a/website/src/components/PipelineGui.tsx +++ b/website/src/components/PipelineGui.tsx @@ -31,6 +31,8 @@ import { Pencil, Plus, AlertCircle, + ChevronRight, + ChevronLeft, } from "lucide-react"; import { usePipelineContext } from "@/contexts/PipelineContext"; import { @@ -75,6 +77,7 @@ import { HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; +import { useRestorePipeline } from "@/hooks/useRestorePipeline"; interface OperationMenuItemProps { name: string; @@ -144,8 +147,110 @@ const PREDEFINED_MODELS = [ "gemini/gemini-pro", ] as const; +interface AddOperationDropdownProps { + onAddOperation: ( + llmType: "LLM" | "non-LLM", + type: string, + name: string + ) => void; + trigger: React.ReactNode; +} + +const AddOperationDropdown: React.FC = ({ + onAddOperation, + trigger, +}) => { + return ( + + {trigger} + + + Add LLM Operation + + onAddOperation("LLM", "map", "Untitled Map")} + /> + onAddOperation("LLM", "reduce", "Untitled Reduce")} + /> + onAddOperation("LLM", "resolve", "Untitled Resolve")} + /> + onAddOperation("LLM", "filter", "Untitled Filter")} + /> + + onAddOperation("LLM", "parallel_map", "Untitled Parallel Map") + } + /> + + + Add Non-LLM Operation + + onAddOperation("non-LLM", "unnest", "Untitled Unnest")} + /> + onAddOperation("non-LLM", "split", "Untitled Split")} + /> + onAddOperation("non-LLM", "gather", "Untitled Gather")} + /> + onAddOperation("non-LLM", "sample", "Untitled Sample")} + /> + + + Code Operations + + + onAddOperation("non-LLM", "code_map", "Untitled Code Map") + } + /> + + onAddOperation("non-LLM", "code_reduce", "Untitled Code Reduce") + } + /> + + onAddOperation("non-LLM", "code_filter", "Untitled Code Filter") + } + /> + + + ); +}; + const PipelineGUI: React.FC = () => { const fileInputRef = useRef(null); + const headerRef = useRef(null); const { operations, setOperations, @@ -206,6 +311,7 @@ const PipelineGUI: React.FC = () => { const [editedPipelineName, setEditedPipelineName] = useState(pipelineName); const [isLocalMode, setIsLocalMode] = useState(false); const [isModelInputFocused, setIsModelInputFocused] = useState(false); + const [isLeftSideCollapsed, setIsLeftSideCollapsed] = useState(false); const hasOpenAIKey = useMemo(() => { return apiKeys.some((key) => key.name === "OPENAI_API_KEY"); @@ -260,6 +366,17 @@ const PipelineGUI: React.FC = () => { }, }); + const { restoreFromYAML } = useRestorePipeline({ + setOperations, + setPipelineName, + setSampleSize, + setDefaultModel, + setFiles, + setCurrentFile, + currentFile, + files, + }); + useEffect(() => { if (lastMessage) { if (lastMessage.type === "output") { @@ -401,127 +518,42 @@ const PipelineGUI: React.FC = () => { } }, [optimizerModel]); + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.contentRect.width < 1100) { + setIsLeftSideCollapsed(true); + } else { + setIsLeftSideCollapsed(false); + } + } + }); + + if (headerRef.current) { + resizeObserver.observe(headerRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, []); + const handleFileUpload = async ( event: React.ChangeEvent ) => { const file = event.target.files?.[0]; if (file) { - const reader = new FileReader(); - reader.onload = async (e) => { - const content = e.target?.result; - if (typeof content === "string") { - try { - const yamlFileName = file.name.split("/").pop()?.split(".")[0]; - const yamlContent = yaml.load(content) as YAMLContent; - setOperations([]); - - // Update PipelineContext with the loaded YAML data - setOperations( - (yamlContent.operations || []).map((op) => { - const { - id, - type, - name, - prompt, - output, - validate, - sample, - ...otherKwargs - } = op; - - // If the operation type is 'reduce', ensure reduce_key is a list - if (type === "reduce" && otherKwargs.reduce_key) { - otherKwargs.reduce_key = Array.isArray(otherKwargs.reduce_key) - ? otherKwargs.reduce_key - : [otherKwargs.reduce_key]; - } - - return { - id: id || uuidv4(), - llmType: - type === "map" || - type === "reduce" || - type === "resolve" || - type === "filter" || - type === "parallel_map" - ? "LLM" - : "non-LLM", - type: type as Operation["type"], - name: name || "Untitled Operation", - prompt, - output: output - ? { - schema: schemaDictToItemSet( - output.schema as Record - ), - } - : undefined, - validate, - sample, - otherKwargs, - visibility: true, - } as Operation; - }) - ); - setPipelineName(yamlFileName || "Untitled Pipeline"); - setSampleSize( - (yamlContent.operations?.[0]?.sample as number) || null - ); - setDefaultModel(yamlContent.default_model || "gpt-4o-mini"); - - // Set current file if it exists in the YAML - // Look for paths in all datasets - const datasetPaths = Object.values(yamlContent.datasets || {}) - .filter( - (dataset: Dataset) => dataset.type === "file" && dataset.path - ) - .map((dataset: Dataset) => dataset.path); - - if (datasetPaths.length > 0) { - const newFiles = datasetPaths.map((filePath) => ({ - name: path.basename(filePath), - path: filePath, - type: "json", - })); - - setFiles((prevFiles: File[]) => { - const uniqueNewFiles = newFiles - .filter( - (newFile) => - !prevFiles.some( - (prevFile) => prevFile.path === newFile.path - ) - ) - .map((file) => ({ - ...file, - type: "json" as const, // Explicitly type as literal "json" - })); - return [...prevFiles, ...uniqueNewFiles]; - }); - - // Set the first file as current if no current file exists - if (!currentFile) { - setCurrentFile({ ...newFiles[0], type: "json" }); - } - } - - toast({ - title: "Pipeline Loaded", - description: - "Your pipeline configuration has been loaded successfully.", - duration: 3000, - }); - } catch (error) { - console.error("Error parsing YAML:", error); - toast({ - title: "Error", - description: "Failed to parse the uploaded YAML file.", - variant: "destructive", - }); - } - } - }; - reader.readAsText(file); + try { + const fileToUpload: File = { + name: file.name, + path: file.name, + type: "pipeline-yaml", + blob: file, + }; + await restoreFromYAML(fileToUpload); + } catch (error) { + console.error("Error handling file upload:", error); + } } }; @@ -754,267 +786,292 @@ const PipelineGUI: React.FC = () => { return (
-
-
-
-
- {isEditingName ? ( - setEditedPipelineName(e.target.value)} - onBlur={() => { - setIsEditingName(false); - setPipelineName(editedPipelineName); - }} - onKeyPress={(e) => { - if (e.key === "Enter") { +
+
+
+
+
+ {isEditingName ? ( + setEditedPipelineName(e.target.value)} + onBlur={() => { setIsEditingName(false); setPipelineName(editedPipelineName); - } - }} - className="max-w-[200px] h-7 text-sm font-bold" - autoFocus - /> - ) : ( - - - -

setIsEditingName(true)} - > - {pipelineName} - -

-
- -

Click to rename pipeline

-
-
-
- )} + }} + onKeyPress={(e) => { + if (e.key === "Enter") { + setIsEditingName(false); + setPipelineName(editedPipelineName); + } + }} + className="max-w-[200px] h-7 text-sm font-bold" + autoFocus + /> + ) : ( + + + +

setIsEditingName(true)} + > + {pipelineName} + +

+
+ +

Click to rename pipeline

+
+
+
+ )} -
- - - +
+ +
+
+ + + + + - - Overview - - - -
-
-

Pipeline Flow

- - {operations.filter((op) => op.visibility).length}{" "} - operations - -
-
- {operations.length > 0 ? ( - operations - .filter((op) => op.visibility) - .map((op, index, arr) => ( -
-
-
{op.name}
-
- {op.type} +
+
+

Pipeline Flow

+ + {operations.filter((op) => op.visibility).length}{" "} + operations + +
+
+ {operations.length > 0 ? ( + operations + .filter((op) => op.visibility) + .map((op, index, arr) => ( +
+
+
{op.name}
+
+ {op.type} +
+ {index < arr.length - 1 && ( +
+ ↓ +
+ )}
- {index < arr.length - 1 && ( -
- ↓ -
- )} -
- )) - ) : ( -
- No operations in the pipeline -
- )} + )) + ) : ( +
+ No operations in the pipeline +
+ )} +
-
- - + + - - - - - -
-
-

- System Configuration -

-

- This will be in the system prompt for every{" "} - operation! -

-
+ + + + +
- -