Skip to main content
Glama

Superglue MCP

Official
by superglue-ai
ToolStepGallery.tsx53.9 kB
import { Button } from '@/src/components/ui/button'; import { Card } from '@/src/components/ui/card'; import { Input } from '@/src/components/ui/input'; import { Tabs, TabsList, TabsTrigger } from '@/src/components/ui/tabs'; import { HelpTooltip } from '@/src/components/utils/HelpTooltip'; import { canExecuteStep } from '@/src/lib/client-utils'; import { downloadJson } from '@/src/lib/download-utils'; import { type UploadedFileInfo } from '@/src/lib/file-utils'; import { buildEvolvingPayload, cn, isEmptyData, MAX_DISPLAY_LINES, MAX_DISPLAY_SIZE, truncateForDisplay, truncateLines } from '@/src/lib/utils'; import { Integration } from "@superglue/client"; import { inferJsonSchema } from '@superglue/shared'; import { ChevronLeft, ChevronRight, Database, Download, FileJson, Package, Play, Plus, Settings, Trash2 } from 'lucide-react'; import React, { useEffect, useRef, useState } from 'react'; import { AddStepDialog } from './AddStepDialog'; import { CopyButton, FinalResultsCard, FinalTransformMiniStepCard, InstructionDisplay, JsonCodeEditor, MiniStepCard, PayloadMiniStepCard } from './ToolMiniStepCards'; import { ToolStepConfigurator } from './ToolStepConfigurator'; interface ToolStepGalleryProps { steps: any[]; stepResults?: Record<string, any>; finalTransform?: string; finalResult?: any; responseSchema?: string; toolId?: string; instruction?: string; onStepsChange?: (steps: any[]) => void; onStepEdit?: (stepId: string, updatedStep: any, isUserInitiated?: boolean) => void; onFinalTransformChange?: (transform: string) => void; onResponseSchemaChange?: (schema: string) => void; onPayloadChange?: (payload: string) => void; onToolIdChange?: (id: string) => void; onInstructionEdit?: () => void; onExecuteStep?: (stepIndex: number) => Promise<void>; onExecuteAllSteps?: () => Promise<void>; onExecuteTransform?: (schema: string, transform: string) => Promise<void>; completedSteps?: string[]; failedSteps?: string[]; integrations?: Integration[]; isExecuting?: boolean; isExecutingStep?: number; isExecutingTransform?: boolean; currentExecutingStepIndex?: number; transformResult?: any; readOnly?: boolean; payloadText?: string; inputSchema?: string | null; onInputSchemaChange?: (schema: string | null) => void; headerActions?: React.ReactNode; navigateToFinalSignal?: number; showStepOutputSignal?: number; focusStepId?: string | null; uploadedFiles?: UploadedFileInfo[]; onFilesUpload?: (files: File[]) => Promise<void>; onFileRemove?: (key: string) => void; isProcessingFiles?: boolean; totalFileSize?: number; filePayloads?: Record<string, any>; } const SpotlightStepCard = ({ step, stepIndex, evolvingPayload, stepResult, onEdit, onRemove, onExecuteStep, canExecute, isExecuting, isGlobalExecuting, currentExecutingStepIndex, integrations, readOnly, failedSteps = [], showOutputSignal, onConfigEditingChange }: { step: any; stepIndex: number; evolvingPayload: any; stepResult?: any; onEdit?: (stepId: string, updatedStep: any, isUserInitiated?: boolean) => void; onRemove?: (stepId: string) => void; onExecuteStep?: () => Promise<void>; canExecute?: boolean; isExecuting?: boolean; isGlobalExecuting?: boolean; currentExecutingStepIndex?: number; integrations?: Integration[]; readOnly?: boolean; failedSteps?: string[]; stepResultsMap?: Record<string, any>; showOutputSignal?: number; onConfigEditingChange?: (editing: boolean) => void; }) => { const [activePanel, setActivePanel] = useState<'input' | 'config' | 'output'>('config'); const [inputViewMode, setInputViewMode] = useState<'preview' | 'schema'>('preview'); const [outputViewMode, setOutputViewMode] = useState<'preview' | 'schema'>('preview'); // Switch to output tab when signal changes useEffect(() => { if (showOutputSignal) { setActivePanel('output'); } }, [showOutputSignal]); return ( <Card className="w-full max-w-6xl mx-auto shadow-md bg-accent/10 dark:bg-accent/5 border border-accent/30 dark:border-accent/20 overflow-hidden" style={{ scrollbarGutter: 'stable both-edges' }}> <div className="p-3"> <div className="flex items-center justify-between mb-4"> <div className="flex items-center gap-2"> <Database className="h-4 w-4 text-muted-foreground" /> <h3 className="text-lg font-semibold"> {step.id || `Step ${stepIndex + 1}`} </h3> {step.name && step.name !== step.id && ( <span className="text-sm text-muted-foreground">({step.name})</span> )} </div> <div className="flex items-center gap-2"> {!readOnly && onExecuteStep && ( <> <span title={!canExecute ? "To enable this button, execute previous steps first" : isExecuting ? "Step is currently executing" : "Test this step (no self-healing)"}> <Button size="sm" onClick={onExecuteStep} disabled={!canExecute || isExecuting} > {isExecuting ? ( <> <div className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent mr-2" /> Running... </> ) : ( <> <Play className="h-3 w-3 mr-1" /> Run Step </> )} </Button> </span> <HelpTooltip text="Executes this step configuration directly without instruction validation or self-healing. Only works if all previous steps have completed successfully." /> </> )} {!readOnly && onRemove && ( <Button variant="ghost" size="icon" onClick={() => onRemove(step.id)} className="text-destructive hover:text-destructive hover:bg-destructive/10" > <Trash2 className="h-4 w-4" /> </Button> )} </div> </div> <div className="space-y-3" style={{ scrollbarGutter: 'stable both-edges' }}> <div className="flex items-center justify-between"> <Tabs value={activePanel} onValueChange={(v) => setActivePanel(v as 'input' | 'config' | 'output')}> <TabsList className="h-9 p-1 rounded-md"> <TabsTrigger value="input" className="h-full px-3 text-xs flex items-center gap-1 rounded-sm data-[state=active]:rounded-sm"> <FileJson className="h-4 w-4" /> Step Input </TabsTrigger> <TabsTrigger value="config" className="h-full px-3 text-xs flex items-center gap-1 rounded-sm data-[state=active]:rounded-sm"> <Settings className="h-4 w-4" /> Step Config </TabsTrigger> <TabsTrigger value="output" className="h-full px-3 text-xs flex items-center gap-1 rounded-sm data-[state=active]:rounded-sm"> <Package className="h-4 w-4" /> Step Output </TabsTrigger> </TabsList> </Tabs> </div> <div className="mt-1" style={{ scrollbarGutter: 'stable both-edges' }}> {activePanel === 'input' && ( <div> {(() => { const noInputYet = stepIndex > 0 && isEmptyData(evolvingPayload || {}); if (noInputYet) { return ( <div className="flex flex-col items-center justify-center py-8 text-muted-foreground border rounded-md bg-muted/5"> <div className="text-xs mb-1">No input yet</div> <p className="text-[10px]">Run previous step to see inputs</p> </div> ); } let inputString = ''; let isTruncated = false; if (inputViewMode === 'schema') { const schemaObj = inferJsonSchema(evolvingPayload || {}); inputString = truncateLines(JSON.stringify(schemaObj, null, 2), MAX_DISPLAY_LINES); } else { const displayData = truncateForDisplay(evolvingPayload); inputString = displayData.value; isTruncated = displayData.truncated; } return ( <> <JsonCodeEditor value={inputString} readOnly={true} minHeight="300px" maxHeight="600px" resizable={true} overlay={ <div className="flex items-center gap-1"> <Tabs value={inputViewMode} onValueChange={(v) => setInputViewMode(v as 'preview' | 'schema')} className="w-auto"> <TabsList className="h-6 p-0.5 rounded-md"> <TabsTrigger value="preview" className="h-full px-2 text-[11px] rounded-sm data-[state=active]:rounded-sm">Preview</TabsTrigger> <TabsTrigger value="schema" className="h-full px-2 text-[11px] rounded-sm data-[state=active]:rounded-sm">Schema</TabsTrigger> </TabsList> </Tabs> <CopyButton text={inputString} /> <Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => downloadJson(evolvingPayload, `step_${step.id}_input.json`)} title="Download step input as JSON" > <Download className="h-3 w-3" /> </Button> </div> } /> {isTruncated && inputViewMode === 'preview' && ( <div className="mt-1 text-[10px] text-amber-600 dark:text-amber-300 px-2"> Preview truncated for display performance </div> )} </> ); })()} </div> )} {activePanel === 'config' && ( <div className="mt-1"> <ToolStepConfigurator step={step} isLast={true} onEdit={onEdit} onRemove={() => { }} integrations={integrations} onEditingChange={onConfigEditingChange} disabled={!!(isExecuting || isGlobalExecuting)} stepInput={evolvingPayload} /> </div> )} {activePanel === 'output' && ( <div> {(() => { // Check if step has failed and we should show error const stepFailed = failedSteps?.includes(step.id); const errorResult = stepFailed && (!stepResult || typeof stepResult === 'string'); // Check if result is pending (no output yet) const isPending = !stepFailed && stepResult === undefined; // Running if either single-step run or global run is currently on this step const isActivelyRunning = !!(isExecuting || (isGlobalExecuting && currentExecutingStepIndex === stepIndex)); let outputString = ''; let isTruncated = false; if (!isPending) { if (errorResult) { // Show error message if step failed if (stepResult) { if (typeof stepResult === 'string') { // Truncate long error strings outputString = stepResult.length > MAX_DISPLAY_SIZE ? stepResult.substring(0, MAX_DISPLAY_SIZE) + '\n... [Error message truncated]' : stepResult; } else { const displayData = truncateForDisplay(stepResult); outputString = displayData.value; } } else { outputString = '{\n "error": "Step execution failed"\n}'; } } else if (outputViewMode === 'schema') { const schemaObj = inferJsonSchema(stepResult || {}); outputString = truncateLines(JSON.stringify(schemaObj, null, 2), MAX_DISPLAY_LINES); } else { const displayData = truncateForDisplay(stepResult); outputString = displayData.value; isTruncated = displayData.truncated; } } const showEmptyWarning = !stepFailed && !isPending && !errorResult && outputViewMode === 'preview' && isEmptyData(outputString || ''); return ( <> {stepFailed && ( <div className="mb-2 p-2 bg-destructive/10 border border-destructive/20 rounded-md"> <p className="text-xs text-destructive">Step execution failed</p> </div> )} {isPending ? ( isActivelyRunning ? ( <div className="flex flex-col items-center justify-center py-8 text-muted-foreground border rounded-md bg-muted/5"> <div className="flex items-center gap-2 mb-1"> <div className="h-3 w-3 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" /> <span className="text-xs">Currently running...</span> </div> <p className="text-[10px]">Step outputs will be shown shortly</p> </div> ) : ( <div className="flex flex-col items-center justify-center py-8 text-muted-foreground border rounded-md bg-muted/5"> <div className="text-xs mb-1">No output yet</div> <p className="text-[10px]">Run this step to see outputs</p> </div> ) ) : ( <> <JsonCodeEditor value={outputString} readOnly={true} minHeight="300px" maxHeight="600px" resizable={true} overlay={ <div className="flex items-center gap-1"> {!errorResult && ( <Tabs value={outputViewMode} onValueChange={(v) => setOutputViewMode(v as 'preview' | 'schema')} className="w-auto"> <TabsList className="h-6 p-0.5 rounded-md"> <TabsTrigger value="preview" className="h-full px-2 text-[11px] rounded-sm data-[state=active]:rounded-sm">Preview</TabsTrigger> <TabsTrigger value="schema" className="h-full px-2 text-[11px] rounded-sm data-[state=active]:rounded-sm">Schema</TabsTrigger> </TabsList> </Tabs> )} <CopyButton text={outputString} /> {!errorResult && ( <Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => downloadJson(stepResult, `step_${step.id}_output.json`)} title="Download step output as JSON" > <Download className="h-3 w-3" /> </Button> )} </div> } /> {showEmptyWarning && ( <div className="mt-1 text-[11px] text-amber-700 dark:text-amber-300 px-2"> ⚠ No data returned. Is this expected? </div> )} {isTruncated && outputViewMode === 'preview' && ( <div className="mt-1 text-[10px] text-amber-600 dark:text-amber-300 px-2"> Preview truncated for display performance </div> )} </> )} </> ); })()} </div> )} </div> </div> </div> </Card> ); }; export function ToolStepGallery({ steps, stepResults = {}, finalTransform, finalResult, responseSchema, toolId, instruction, onStepsChange, onStepEdit: originalOnStepEdit, onFinalTransformChange, onResponseSchemaChange, onPayloadChange, onToolIdChange, onInstructionEdit, onExecuteStep, onExecuteAllSteps, onExecuteTransform, completedSteps = [], failedSteps = [], integrations, isExecuting, isExecutingStep, isExecutingTransform, currentExecutingStepIndex, transformResult, readOnly = false, payloadText, inputSchema, onInputSchemaChange, headerActions, navigateToFinalSignal, showStepOutputSignal, focusStepId, uploadedFiles, onFilesUpload, onFileRemove, isProcessingFiles, totalFileSize, filePayloads }: ToolStepGalleryProps) { const [activeIndex, setActiveIndex] = useState(1); // Default to first tool step, not payload const [windowWidth, setWindowWidth] = useState(1200); const [containerWidth, setContainerWidth] = useState<number>(1200); const [isHydrated, setIsHydrated] = useState(false); const listRef = useRef<HTMLDivElement | null>(null); const [isConfiguratorEditing, setIsConfiguratorEditing] = useState<boolean>(false); const [isAddStepDialogOpen, setIsAddStepDialogOpen] = useState(false); const [defaultStepId, setDefaultStepId] = useState(''); const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null); const isConfiguratorEditingRef = useRef<boolean>(false); useEffect(() => { isConfiguratorEditingRef.current = isConfiguratorEditing; }, [isConfiguratorEditing]); const isNavigatingRef = useRef<boolean>(false); const navigationTimeoutRef = useRef<NodeJS.Timeout | null>(null); const NAV_SUPPRESS_MS = 300; const NAV_DELAY_MS = 50; const navigateToIndex = (nextIndex: number) => { isNavigatingRef.current = true; if (navigationTimeoutRef.current) clearTimeout(navigationTimeoutRef.current); navigationTimeoutRef.current = setTimeout(() => { isNavigatingRef.current = false; }, NAV_SUPPRESS_MS); setTimeout(() => { setActiveIndex(nextIndex); const container = listRef.current; const card = container?.children?.[nextIndex] as HTMLElement | undefined; if (container && card) { card.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); } }, NAV_DELAY_MS); }; // Local toolId editor state to reduce re-renders const [localToolId, setLocalToolId] = useState<string>(toolId ?? ''); const [isEditingToolId, setIsEditingToolId] = useState<boolean>(false); const toolIdInputRef = useRef<HTMLInputElement | null>(null); const liveToolIdRef = useRef<string>(toolId ?? ''); useEffect(() => { if (!isEditingToolId) { setLocalToolId(toolId ?? ''); } }, [toolId, isEditingToolId]); const commitToolIdIfChanged = () => { const nextVal = liveToolIdRef.current ?? localToolId; if (onToolIdChange && nextVal !== (toolId ?? '')) { onToolIdChange(nextVal); setLocalToolId(nextVal); } setIsEditingToolId(false); }; // Hydration effect useEffect(() => { setIsHydrated(true); setWindowWidth(window.innerWidth); setContainerWidth(window.innerWidth); }, []); // Update window width on resize useEffect(() => { if (!isHydrated) return; const handleResize = () => { setWindowWidth(window.innerWidth); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [isHydrated]); // Observe container width (e.g., when logs panel opens/closes) to responsively adjust cards useEffect(() => { if (!isHydrated) return; const container = listRef.current?.parentElement?.parentElement as HTMLElement | null; if (!container || typeof ResizeObserver === 'undefined') return; const ro = new ResizeObserver((entries) => { for (const entry of entries) { const w = (entry.contentRect?.width || container.getBoundingClientRect().width); if (w && Math.abs(w - containerWidth) > 1) setContainerWidth(w); } }); ro.observe(container); return () => ro.disconnect(); }, [isHydrated, listRef.current, containerWidth]); const [rawPayloadText, setRawPayloadText] = useState<string>(payloadText || ''); // Initialize workingPayload with filePayloads if available const [workingPayload, setWorkingPayload] = useState<any>(() => { const trimmed = (payloadText || '').trim(); if (trimmed === '') { return filePayloads || {}; } try { const parsed = JSON.parse(payloadText); return filePayloads ? { ...parsed, ...filePayloads } : parsed; } catch { return filePayloads || {}; } }); useEffect(() => { const trimmed = (rawPayloadText || '').trim(); if (trimmed === '') { // Even with empty manual payload, include file payloads setWorkingPayload(filePayloads || {}); return; } try { const parsed = JSON.parse(rawPayloadText); // Merge manual payload with file payloads const merged = filePayloads ? { ...parsed, ...filePayloads } : parsed; setWorkingPayload(merged); } catch { // On parse error, still include file payloads setWorkingPayload(filePayloads || {}); } }, [rawPayloadText, filePayloads]); // Keep local payload text in sync with external prop so uploads reflect immediately useEffect(() => { if (typeof payloadText === 'string') { setRawPayloadText(payloadText); } }, [payloadText]); const handlePayloadJsonChange = (jsonString: string) => { setRawPayloadText(jsonString); onPayloadChange?.(jsonString); try { const parsed = JSON.parse(jsonString); // Always merge with file payloads when manually editing const merged = filePayloads ? { ...parsed, ...filePayloads } : parsed; setWorkingPayload(merged); } catch { // On parse error, still include file payloads setWorkingPayload(filePayloads || {}); } }; const stepResultsMap = Array.isArray(stepResults) ? stepResults.reduce((acc: Record<string, any>, result: any) => { if (result.stepId) { acc[result.stepId] = result.data || result.transformedData || result; } return acc; }, {}) : stepResults; const toolItems = [ { type: 'payload', data: { payloadText: rawPayloadText, inputSchema }, stepResult: undefined, evolvingPayload: workingPayload || {} }, ...steps.map((step, index) => ({ type: 'step', data: step, stepResult: stepResultsMap[step.id], evolvingPayload: buildEvolvingPayload(workingPayload || {}, steps, stepResultsMap, index - 1) })), ...(finalTransform !== undefined ? [{ type: 'transform', data: { transform: finalTransform, responseSchema }, stepResult: finalResult, evolvingPayload: buildEvolvingPayload(workingPayload || {}, steps, stepResultsMap, steps.length - 1) }] : []), { type: 'final', data: { result: transformResult || finalResult }, stepResult: transformResult || finalResult, evolvingPayload: buildEvolvingPayload(workingPayload || {}, steps, stepResultsMap, steps.length) } ]; const currentItem = toolItems[activeIndex]; const indicatorIndices = toolItems.map((_, idx) => idx); const handleNavigation = (direction: 'prev' | 'next') => { if (isConfiguratorEditing) return; const newIndex = direction === 'prev' ? Math.max(0, activeIndex - 1) : Math.min(toolItems.length - 1, activeIndex + 1); if (newIndex === activeIndex) return; navigateToIndex(newIndex); }; const handleCardClick = (globalIndex: number) => { if (isConfiguratorEditing) return; navigateToIndex(globalIndex); }; const handleRemoveStep = (stepId: string) => { if (!onStepsChange) return; const newSteps = steps.filter(step => step.id !== stepId); onStepsChange(newSteps); // Adjust active index if needed if (activeIndex >= toolItems.length - 1) { setActiveIndex(Math.max(0, activeIndex - 1)); } }; const handleInsertStep = (afterIndex: number) => { if (!onStepsChange || readOnly) return; const defaultId = `step_${Date.now()}`; setDefaultStepId(defaultId); setPendingInsertIndex(afterIndex); setIsAddStepDialogOpen(true); }; const handleConfirmInsertStep = (stepId: string) => { if (pendingInsertIndex === null || !onStepsChange) return; const newStep = { id: stepId, name: '', apiConfig: { id: stepId, instruction: '', urlHost: '', urlPath: '', method: 'GET', headers: {}, queryParams: {}, body: '', authentication: 'NONE' }, executionMode: 'DIRECT' }; const newSteps = [...steps]; newSteps.splice(pendingInsertIndex, 0, newStep); onStepsChange(newSteps); const insertedIndex = pendingInsertIndex; setIsAddStepDialogOpen(false); setPendingInsertIndex(null); // Navigate to the newly inserted step (+1 for payload card, +1 because we insert after) setTimeout(() => navigateToIndex(insertedIndex + 1), 100); }; const handleConfirmInsertTool = (toolSteps: any[]) => { if (pendingInsertIndex === null || !onStepsChange) return; const newSteps = [...steps]; newSteps.splice(pendingInsertIndex, 0, ...toolSteps); onStepsChange(newSteps); const insertedIndex = pendingInsertIndex; setIsAddStepDialogOpen(false); setPendingInsertIndex(null); // Navigate to the first newly inserted step setTimeout(() => navigateToIndex(insertedIndex + 1), 100); }; const onStepEdit = (stepId: string, updatedStep: any, isUserInitiated: boolean = false) => { // Suppress user-initiated edits during navigation to prevent spurious resets if (isNavigatingRef.current && isUserInitiated) { if (originalOnStepEdit) { originalOnStepEdit(stepId, updatedStep, false); } } else if (originalOnStepEdit) { originalOnStepEdit(stepId, updatedStep, isUserInitiated); } }; useEffect(() => { setActiveIndex(steps.length > 0 ? 1 : 0); }, []); useEffect(() => { if (navigateToFinalSignal) { navigateToIndex(toolItems.length - 1); } }, [navigateToFinalSignal]); useEffect(() => { if (!showStepOutputSignal || !focusStepId) return; const idx = steps.findIndex((s: any) => s.id === focusStepId); if (idx >= 0) { navigateToIndex(idx + 1); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [showStepOutputSignal, focusStepId]); return ( <div className="space-y-6"> <div className="space-y-3"> <div className="flex items-center justify-center gap-3 flex-wrap"> <div className="flex items-center gap-3 min-w-0 w-full"> {(onToolIdChange || typeof toolId !== 'undefined') && ( <div className="flex w-full items-center justify-between gap-3"> <div className="flex items-center gap-3 px-3 py-1.5 bg-muted/50 rounded-md border h-[36px]"> <span className="text-sm text-muted-foreground">Tool ID:</span> <Input key={isEditingToolId ? 'editing' : localToolId} ref={toolIdInputRef} defaultValue={localToolId} onFocus={() => { setIsEditingToolId(true); liveToolIdRef.current = toolIdInputRef.current?.value ?? localToolId; }} onChange={(e) => { liveToolIdRef.current = e.target.value; }} onBlur={commitToolIdIfChanged} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); commitToolIdIfChanged(); } else if (e.key === 'Escape') { if (toolIdInputRef.current) { toolIdInputRef.current.value = toolId ?? ''; } liveToolIdRef.current = toolId ?? ''; setLocalToolId(toolId ?? ''); setIsEditingToolId(false); } }} className="h-5 font-mono text-sm w-[200px] md:w-[280px] border-0 bg-transparent p-0 focus:ring-0" readOnly={readOnly || !onToolIdChange} /> </div> <div className="flex items-center gap-2"> {headerActions ?? null} </div> </div> )} </div> </div> {instruction && ( <div className="w-full"> <InstructionDisplay instruction={instruction} onEdit={onInstructionEdit} showEditButton={!readOnly && !!onInstructionEdit} /> </div> )} <div className="flex items-center gap-0"> <Button variant="outline" size="icon" onClick={() => handleNavigation('prev')} disabled={activeIndex === 0} className="shrink-0 h-9 w-9" title="Previous" > <ChevronLeft className="h-4 w-4" /> </Button> <div className="flex-1 overflow-hidden px-0"> <div className="relative"> <div ref={listRef} className="flex justify-center items-center overflow-visible py-3" style={{ minHeight: '150px' }} > {!isHydrated ? ( // Show a simple loading state during hydration <div className="flex items-center justify-center"> <div className="w-48 h-24 bg-muted/20 rounded-md animate-pulse" /> </div> ) : (() => { const totalCards = toolItems.length; let startIdx = 0; let endIdx = totalCards; const CARD_WIDTH = 228; // px (matches card classes above) const ARROW_WIDTH = 24; // px (ChevronRight ~20px, add buffer) const GUTTER = 16; // px (doubled spacing) const SAFE_MARGIN = 12; // px extra space to avoid clipping const available = Math.max(0, (containerWidth || windowWidth) - SAFE_MARGIN); let cardsToShow = 1; const maxCandidates = Math.min(toolItems.length, 12); for (let c = 1; c <= maxCandidates; c++) { const needed = (c * CARD_WIDTH) + ((c - 1) * (ARROW_WIDTH + GUTTER)); if (needed <= available) { cardsToShow = c; } else { break; } } // Always be slightly conservative to avoid cramping at any breakpoint cardsToShow = Math.max(1, cardsToShow - 1); if (totalCards <= cardsToShow) { // Show all cards if we have fewer than cardsToShow startIdx = 0; endIdx = totalCards; } else { const halfWindow = Math.floor(cardsToShow / 2); startIdx = Math.max(0, Math.min(activeIndex - halfWindow, totalCards - cardsToShow)); endIdx = startIdx + cardsToShow; } const visibleItems = toolItems.slice(startIdx, endIdx); const visibleIndices = visibleItems.map((_, i) => startIdx + i); const hasHiddenLeft = startIdx > 0; const hasHiddenRight = endIdx < totalCards; const sepWidth = ARROW_WIDTH + GUTTER; const edgeWidth = sepWidth; const count = Math.max(1, visibleItems.length); const innerAvailable = Math.max(0, (containerWidth || windowWidth) - SAFE_MARGIN - (2 * edgeWidth) - ((count - 1) * sepWidth)); const baseCardWidth = Math.floor(innerAvailable / count); const widthRemainder = innerAvailable - (baseCardWidth * count); return ( <> {hasHiddenLeft && null} {visibleItems.length > 0 && ( <div style={{ flex: `0 0 ${sepWidth}px`, width: `${sepWidth}px` }} /> )} {visibleItems.map((item, idx) => { const globalIdx = visibleIndices[idx]; const showArrow = idx < visibleItems.length - 1; return ( <React.Fragment key={globalIdx}> <div className="flex items-center justify-center" style={{ flex: `0 0 ${baseCardWidth + (idx < widthRemainder ? 1 : 0)}px`, width: `${baseCardWidth + (idx < widthRemainder ? 1 : 0)}px`, maxWidth: `${baseCardWidth + (idx < widthRemainder ? 1 : 0)}px` }} > <MiniStepCard step={item.data} index={globalIdx} isActive={globalIdx === activeIndex} onClick={() => handleCardClick(globalIdx)} stepId={item.type === 'step' ? item.data.id : undefined} isPayload={item.type === 'payload'} isTransform={item.type === 'transform'} isFinal={item.type === 'final'} isRunningAll={isExecuting && currentExecutingStepIndex === (globalIdx - 1)} isTesting={ item.type === 'step' ? isExecutingStep === (globalIdx - 1) : item.type === 'transform' ? isExecutingTransform : false } completedSteps={completedSteps} failedSteps={failedSteps} /> </div> {showArrow && ( <div style={{ flex: `0 0 ${sepWidth}px`, width: `${sepWidth}px` }} className="flex items-center justify-center"> {!readOnly && onStepsChange && ( <button onClick={(e) => { e.stopPropagation(); handleInsertStep(globalIdx); }} className="group relative flex items-center justify-center h-8 w-8 rounded-full hover:bg-primary/10 transition-colors" title="Add step here" > <ChevronRight className="h-5 w-5 text-muted-foreground group-hover:opacity-0 transition-opacity" /> <Plus className="h-4 w-4 text-primary absolute opacity-0 group-hover:opacity-100 transition-opacity" /> </button> )} {(readOnly || !onStepsChange) && ( <ChevronRight className="h-5 w-5 text-muted-foreground/50" /> )} </div> )} </React.Fragment> ); })} {visibleItems.length > 0 && ( <div style={{ flex: `0 0 ${sepWidth}px`, width: `${sepWidth}px` }} /> )} {hasHiddenRight && null} </> ); })()} </div> </div> </div> <Button variant="outline" size="icon" onClick={() => handleNavigation('next')} disabled={activeIndex === toolItems.length - 1} className="shrink-0 h-9 w-9" title="Next" > <ChevronRight className="h-4 w-4" /> </Button> </div> <div className="flex justify-center items-center gap-2"> <div className="flex gap-1"> {indicatorIndices.map((globalIdx) => ( <button key={`dot-${globalIdx}`} onClick={() => { if (isConfiguratorEditing) return; navigateToIndex(globalIdx); }} className={cn( "w-1.5 h-1.5 rounded-full transition-colors", globalIdx === activeIndex ? "bg-primary" : "bg-muted" )} aria-label={`Go to item ${globalIdx + 1}`} title={`Go to item ${globalIdx + 1}`} /> ))} </div> </div> <div className="min-h-[220px] max-w-6xl mx-auto"> {currentItem && ( currentItem.type === 'payload' ? ( <PayloadMiniStepCard payloadText={currentItem.data.payloadText} inputSchema={currentItem.data.inputSchema} onChange={handlePayloadJsonChange} onInputSchemaChange={onInputSchemaChange} readOnly={readOnly} onFilesUpload={onFilesUpload} uploadedFiles={uploadedFiles} onFileRemove={onFileRemove} isProcessingFiles={isProcessingFiles} totalFileSize={totalFileSize} /> ) : currentItem.type === 'transform' ? ( <FinalTransformMiniStepCard transform={currentItem.data.transform} responseSchema={currentItem.data.responseSchema} onTransformChange={onFinalTransformChange} onResponseSchemaChange={onResponseSchemaChange} readOnly={readOnly} onExecuteTransform={onExecuteTransform} isExecutingTransform={isExecutingTransform} canExecute={steps.every((s: any) => completedSteps.includes(s.id))} transformResult={transformResult || finalResult} stepInputs={currentItem.evolvingPayload} /> ) : currentItem.type === 'final' ? ( <FinalResultsCard result={currentItem.data.result} /> ) : ( <SpotlightStepCard step={currentItem.data} stepIndex={activeIndex - 1} // Adjust for payload card evolvingPayload={currentItem.evolvingPayload || {}} stepResult={currentItem.stepResult} onEdit={!readOnly ? onStepEdit : undefined} onRemove={!readOnly && currentItem.type === 'step' ? handleRemoveStep : undefined} onExecuteStep={onExecuteStep ? () => onExecuteStep(activeIndex - 1) : undefined} canExecute={canExecuteStep(activeIndex - 1, completedSteps, { steps } as any, stepResultsMap)} isExecuting={isExecutingStep === activeIndex - 1} isGlobalExecuting={!!(isExecuting || isExecutingTransform)} currentExecutingStepIndex={currentExecutingStepIndex} integrations={integrations} readOnly={readOnly} failedSteps={failedSteps} showOutputSignal={showStepOutputSignal} onConfigEditingChange={setIsConfiguratorEditing} /> ) )} </div> </div> <AddStepDialog open={isAddStepDialogOpen} onOpenChange={setIsAddStepDialogOpen} onConfirm={handleConfirmInsertStep} onConfirmTool={handleConfirmInsertTool} existingStepIds={steps.map((s: any) => s.id)} defaultId={defaultStepId} /> </div> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/superglue-ai/superglue'

If you have feedback or need assistance with the MCP directory API, please join our Discord server