Skip to main content
Glama
DocumentationField.tsx7.46 kB
'use client' import { useConfig } from '@/src/app/config-context'; import { Badge } from '@/src/components/ui/badge'; import { Button } from '@/src/components/ui/button'; import { FileChip } from '@/src/components/ui/FileChip'; import { Input } from '@/src/components/ui/input'; import { useToast } from '@/src/hooks/use-toast'; import { ExtendedSuperglueClient } from '@/src/lib/extended-superglue-client'; import { formatBytes, MAX_TOTAL_FILE_SIZE_DOCUMENTATION, processAndExtractFile, sanitizeFileName, type UploadedFileInfo } from '@/src/lib/file-utils'; import { ALLOWED_FILE_EXTENSIONS } from '@superglue/shared'; import { cn } from '@/src/lib/general-utils'; import { tokenRegistry } from '@/src/lib/token-registry'; import { FileQuestion, FileText, Link, Loader2 } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; interface DocumentationFieldProps { url: string content?: string onUrlChange: (url: string) => void onContentChange?: (content: string) => void className?: string placeholder?: string onFileUpload?: (extractedText: string) => void onFileRemove?: () => void hasUploadedFile?: boolean } export function DocumentationField({ url, content, onUrlChange, onContentChange, className, placeholder = "https://docs.example.com/api", onFileUpload, onFileRemove, hasUploadedFile = false }: DocumentationFieldProps) { const [localUrl, setLocalUrl] = useState(url) const [docFile, setDocFile] = useState<UploadedFileInfo | null>(null) const [urlError, setUrlError] = useState(false) const [isUploading, setIsUploading] = useState(false) const { toast } = useToast() const superglueConfig = useConfig() const client = useMemo(() => new ExtendedSuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: tokenRegistry.getToken(), }), [superglueConfig.superglueEndpoint]) // Parse multiple files from file:// URL format const parseFileUrls = (fileUrl: string): UploadedFileInfo[] => { if (!fileUrl.startsWith('file://')) return [] const filesString = fileUrl.replace('file://', '') const filenames = filesString.split(',').map(f => f.trim()).filter(Boolean) // Limit to 5 files for display return filenames.slice(0, 5).map(filename => ({ name: filename, size: null, key: filename, status: 'ready' as const })) } const displayFiles = hasUploadedFile ? (docFile ? [docFile] : parseFileUrls(url)) : [] useEffect(() => { setLocalUrl(url) }, [url]) const activeType = hasUploadedFile ? 'file' : (url ? 'url' : content ? 'content' : 'empty') const handleDocFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0] if (!file) return // Check file size limit if (file.size > MAX_TOTAL_FILE_SIZE_DOCUMENTATION) { toast({ title: 'File too large', description: `Documentation files cannot exceed ${formatBytes(MAX_TOTAL_FILE_SIZE_DOCUMENTATION)}. Current file: ${formatBytes(file.size)}`, variant: 'destructive' }) // Reset file input e.target.value = '' return } const fileInfo: UploadedFileInfo = { name: file.name, size: file.size, key: sanitizeFileName(file.name, { removeExtension: false, lowercase: false }), status: 'processing' } setDocFile(fileInfo) setIsUploading(true) try { const data = await processAndExtractFile(file, client) const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2) setDocFile({ ...fileInfo, status: 'ready' }) if (onContentChange) onContentChange(text) onUrlChange(`file://${fileInfo.key}`) if (typeof onFileUpload === 'function') onFileUpload(text) } catch (error: any) { console.error('Error reading file:', error) setDocFile({ ...fileInfo, status: 'error', error: error.message }) toast({ title: 'Failed to process file', description: error.message, variant: 'destructive' }) } finally { setIsUploading(false) } } const handleRemoveFile = () => { setDocFile(null) if (onContentChange) onContentChange('') onUrlChange('') setLocalUrl('') setUrlError(false) // Reset the file input so it can be used again const fileInput = document.getElementById('doc-file-upload') as HTMLInputElement if (fileInput) { fileInput.value = '' } // Notify parent component that file was removed if (onFileRemove) { onFileRemove() } } const handleUrlChange = useCallback((urlHost: string, urlPath: string, queryParams: Record<string, string>) => { const fullUrl = urlHost + (urlPath || '') setLocalUrl(fullUrl) onUrlChange(fullUrl) }, [onUrlChange]) return ( <div className={className}> {hasUploadedFile && displayFiles.length > 0 ? ( <div className="space-y-2"> {displayFiles.map((file, idx) => ( <FileChip key={file.key} file={file} onRemove={idx === 0 ? handleRemoveFile : undefined} size="large" rounded="sm" showOriginalName={true} showSize={file.size > 0} /> ))} {url.startsWith('file://') && url.replace('file://', '').split(',').length > 5 && ( <p className="text-xs text-muted-foreground pl-2"> + {url.replace('file://', '').split(',').length - 5} more file(s) </p> )} </div> ) : ( // Show URL field when no file is uploaded <div className="flex items-center gap-2"> <div className="relative flex-1"> <Input value={localUrl} onChange={(e) => handleUrlChange(e.target.value, '', {})} onBlur={() => { }} placeholder={placeholder} className={cn( "pr-28", urlError && "border-destructive focus-visible:ring-destructive" )} required={true} /> <Badge variant="outline" className="absolute right-2 top-1/2 -translate-y-1/2 bg-background border"> {activeType === 'url' ? ( <><Link className="h-3 w-3 mr-1" /> URL</> ) : activeType === 'content' ? ( <><FileText className="h-3 w-3 mr-1" /> Manual Content</> ) : ( <><FileQuestion className="h-3 w-3 mr-1" /> None</> )} </Badge> </div> <Button type="button" variant="outline" size="sm" className="shrink-0" onClick={() => document.getElementById('doc-file-upload')?.click()} disabled={isUploading} > {isUploading ? ( <> <Loader2 className="h-4 w-4 mr-2 animate-spin" /> Uploading... </> ) : ( 'Upload' )} </Button> </div> )} <input type="file" id="doc-file-upload" hidden onChange={handleDocFileUpload} accept={ALLOWED_FILE_EXTENSIONS.join(',')} /> {urlError && !hasUploadedFile && ( <p className="text-sm text-destructive mt-1">Please enter a valid URL or upload a file</p> )} </div> ) }

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