DocumentationField.tsx•7.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>
)
}