Skip to main content
Glama
PayloadCard.tsx12.6 kB
import JsonSchemaEditor from '@/src/components/editors/JsonSchemaEditor'; import { Button } from '@/src/components/ui/button'; import { Card } from '@/src/components/ui/card'; import { FileChip } from '@/src/components/ui/FileChip'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/src/components/ui/tabs'; import { HelpTooltip } from '@/src/components/utils/HelpTooltip'; import { formatBytes, isAllowedFileType, MAX_TOTAL_FILE_SIZE_TOOLS, type UploadedFileInfo } from '@/src/lib/file-utils'; import { ALLOWED_FILE_EXTENSIONS } from '@superglue/shared'; import { FileBraces, FileBracesCorner, FileJson, Upload } from 'lucide-react'; import React, { useEffect, useRef, useState } from 'react'; import { JsonCodeEditor } from '../../editors/JsonCodeEditor'; export const PayloadSpotlight = ({ payloadText, inputSchema, onChange, onInputSchemaChange, readOnly, onFilesUpload, uploadedFiles = [], onFileRemove, isProcessingFiles = false, totalFileSize = 0, onUserEdit, isPayloadValid }: { payloadText: string; inputSchema?: string | null; onChange?: (value: string) => void; onInputSchemaChange?: (value: string | null) => void; readOnly?: boolean; onFilesUpload?: (files: File[]) => Promise<void>; uploadedFiles?: UploadedFileInfo[]; onFileRemove?: (fileName: string) => void; isProcessingFiles?: boolean; totalFileSize?: number; onUserEdit?: () => void; isPayloadValid?: boolean; }) => { const [activeTab, setActiveTab] = useState('payload'); const [localPayload, setLocalPayload] = useState<string>(payloadText || ''); const [localInputSchema, setLocalInputSchema] = useState(inputSchema || null); const [error, setError] = useState<string | null>(null); const fileInputRef = useRef<HTMLInputElement>(null); // Simple sync with parent payload (parent is source of truth) useEffect(() => { setLocalPayload(payloadText || ''); }, [payloadText]); useEffect(() => { setLocalInputSchema(inputSchema || null); }, [inputSchema]); const handlePayloadChange = (value: string) => { setLocalPayload(value); if (onUserEdit) { onUserEdit(); } const trimmed = (value || '').trim(); if (trimmed === '') { setError(null); if (onChange) onChange(value); return; } try { JSON.parse(value); setError(null); if (onChange) onChange(value); } catch { setError('Invalid JSON - will not be saved. Navigating away will revert to last valid JSON.'); } }; const handleSchemaChange = (value: string | null) => { setLocalInputSchema(value); if (onInputSchemaChange) onInputSchemaChange(value); }; // Extract payload schema from full input schema for display in schema tab const extractPayloadSchemaForDisplay = (fullInputSchema: string | null): any | null => { if (!fullInputSchema || fullInputSchema.trim() === '') { return null; } try { const parsed = JSON.parse(fullInputSchema); if (parsed?.properties?.payload) { return parsed.properties.payload; } return parsed; } catch { return null; } }; const handleFileInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const files = Array.from(e.target.files || []); if (files.length === 0) return; const invalidFiles = files.filter(f => !isAllowedFileType(f.name)); if (invalidFiles.length > 0) { setError(`Unsupported file types: ${invalidFiles.map(f => f.name).join(', ')}`); return; } if (onFilesUpload) { await onFilesUpload(files); } if (fileInputRef.current) { fileInputRef.current.value = ''; } }; return ( <> <input ref={fileInputRef} type="file" multiple accept={ALLOWED_FILE_EXTENSIONS.join(',')} onChange={handleFileInputChange} className="hidden" /> <Tabs value={activeTab} onValueChange={setActiveTab}> <TabsList className="h-9 p-1 rounded-md mb-3"> <TabsTrigger value="payload" 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" /> Payload </TabsTrigger> {isPayloadValid && ( <TabsTrigger value="schema" className="h-full px-3 text-xs flex items-center gap-1 rounded-sm data-[state=active]:rounded-sm"> <FileBracesCorner className="h-4 w-4" /> Input Schema </TabsTrigger> )} {!isPayloadValid && ( <TabsTrigger value="schema" className="h-full px-3 text-xs flex items-center gap-1 rounded-sm data-[state=active]:rounded-sm"> <FileBraces color="#FFA500" className="h-4 w-4" /> Input Schema </TabsTrigger> )} </TabsList> <TabsContent value="payload" className="mt-1 space-y-3"> {!readOnly && onFilesUpload && uploadedFiles.length > 0 && ( <div className="space-y-1.5"> {uploadedFiles.map(file => ( <FileChip key={file.key} file={file} onRemove={onFileRemove} size="default" rounded="md" showOriginalName={true} showKey={true} /> ))} </div> )} <span className="text-xs text-muted-foreground"> Enter your inputs here manually, or upload files to autofill missing JSON fields. </span> <div> <JsonCodeEditor value={localPayload} onChange={(val) => handlePayloadChange(val || '')} readOnly={!!readOnly} maxHeight="300px" resizable={true} showValidation={true} /> </div> {!readOnly && onFilesUpload && ( <div className="pt-3 border-t border-border/50 space-y-3"> <div className="flex flex-col items-center gap-2"> <Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()} disabled={isProcessingFiles || totalFileSize >= MAX_TOTAL_FILE_SIZE_TOOLS} className="h-9 px-4" > {isProcessingFiles ? ( <> <div className="h-3 w-3 animate-spin rounded-full border-2 border-primary border-t-transparent mr-2" /> Processing Files... </> ) : ( <> <Upload className="h-3.5 w-3.5 mr-2" /> Upload Files </> )} </Button> <div className="flex items-center gap-2 text-xs text-muted-foreground"> <span>{formatBytes(totalFileSize)} / {formatBytes(MAX_TOTAL_FILE_SIZE_TOOLS)}</span> <HelpTooltip text="Upload CSV, JSON, XML, or Excel files. Files will be automatically parsed to JSON and merged with the manual payload when the tool executes." /> </div> </div> </div> )} </TabsContent> <TabsContent value="schema" className="mt-3"> <JsonSchemaEditor value={localInputSchema ? JSON.stringify(extractPayloadSchemaForDisplay(localInputSchema), null, 2) : localInputSchema} onChange={(value) => { if (value && value.trim() !== '') { try { const payloadSchema = JSON.parse(value); const fullSchema = { type: 'object', properties: { payload: payloadSchema } }; handleSchemaChange(JSON.stringify(fullSchema, null, 2)); } catch (e) { handleSchemaChange(value); } } else { handleSchemaChange(value); } }} isOptional={true} showModeToggle={true} /> <div className="mt-2 text-[10px] text-muted-foreground"> <HelpTooltip text="Input Schema is optional documentation/validation describing expected payload shape. The payload JSON is what runs; schema does not inject credentials nor drive payload. Leave disabled if not needed." /> </div> </TabsContent> </Tabs> </> ); }; export const PayloadMiniStepCard = React.memo(({ payloadText, inputSchema, onChange, onInputSchemaChange, readOnly, onFilesUpload, uploadedFiles, onFileRemove, isProcessingFiles, totalFileSize, onUserEdit, isPayloadValid }: { payloadText: string; inputSchema?: string | null; onChange?: (value: string) => void; onInputSchemaChange?: (value: string | null) => void; readOnly?: boolean; onFilesUpload?: (files: File[]) => Promise<void>; uploadedFiles?: UploadedFileInfo[]; onFileRemove?: (key: string) => void; isProcessingFiles?: boolean; totalFileSize?: number; onUserEdit?: () => void; isPayloadValid?: boolean; }) => { return ( <Card className="w-full max-w-6xl mx-auto shadow-md border dark:border-border/50"> <div className="p-3"> <div className="flex items-center justify-between mb-3"> <div className="flex items-center gap-2"> <FileJson className="h-4 w-4 text-muted-foreground" /> <h3 className="text-lg font-semibold">Tool Input</h3> </div> <HelpTooltip text="Payload is the JSON input to tool execution. Editing here does NOT save values to the tool; it only affects this session/run. Use Input Schema to optionally describe the expected structure for validation and tooling." /> </div> <PayloadSpotlight payloadText={payloadText} inputSchema={inputSchema} onChange={onChange} onInputSchemaChange={onInputSchemaChange} readOnly={readOnly} onFilesUpload={onFilesUpload} uploadedFiles={uploadedFiles} onFileRemove={onFileRemove} isProcessingFiles={isProcessingFiles} totalFileSize={totalFileSize} onUserEdit={onUserEdit} isPayloadValid={isPayloadValid} /> </div> </Card> ); });

Latest Blog Posts

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