Spaces:
Running
Running
| /** | |
| * Context Upload Dialog Component | |
| * | |
| * Dialog for uploading files or pasting text as context documents | |
| */ | |
| import React, { useState, useCallback } from "react"; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogHeader, | |
| DialogTitle, | |
| } from "@/components/ui/dialog"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from "@/components/ui/select"; | |
| import { Upload, FileText, AlertCircle, Check } from "lucide-react"; | |
| import { ContextDocumentType, CONTEXT_DOCUMENT_TYPES } from "@/types/context"; | |
| import { useContextDocuments } from "@/hooks/useContextDocuments"; | |
| interface ContextUploadDialogProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| traceId: string; | |
| onSuccess: () => void; | |
| } | |
| export function ContextUploadDialog({ | |
| isOpen, | |
| onClose, | |
| traceId, | |
| onSuccess, | |
| }: ContextUploadDialogProps) { | |
| const { createDocument, uploadFile, loading, error } = useContextDocuments(); | |
| // Form state | |
| const [title, setTitle] = useState(""); | |
| const [documentType, setDocumentType] = | |
| useState<ContextDocumentType>("domain_knowledge"); | |
| const [content, setContent] = useState(""); | |
| const [selectedFile, setSelectedFile] = useState<File | null>(null); | |
| const [dragActive, setDragActive] = useState(false); | |
| const resetForm = () => { | |
| setTitle(""); | |
| setDocumentType("domain_knowledge"); | |
| setContent(""); | |
| setSelectedFile(null); | |
| setDragActive(false); | |
| }; | |
| const handleClose = () => { | |
| resetForm(); | |
| onClose(); | |
| }; | |
| const handleCreateFromText = async () => { | |
| if (!title.trim() || !content.trim()) { | |
| return; | |
| } | |
| const result = await createDocument(traceId, { | |
| title: title.trim(), | |
| document_type: documentType, | |
| content: content.trim(), | |
| }); | |
| if (result) { | |
| resetForm(); | |
| onSuccess(); | |
| } | |
| }; | |
| const handleFileUpload = async () => { | |
| if (!selectedFile || !title.trim()) { | |
| return; | |
| } | |
| const result = await uploadFile( | |
| traceId, | |
| selectedFile, | |
| title.trim(), | |
| documentType | |
| ); | |
| if (result) { | |
| resetForm(); | |
| onSuccess(); | |
| } | |
| }; | |
| const handleFileSelect = (file: File) => { | |
| setSelectedFile(file); | |
| if (!title) { | |
| setTitle(file.name.replace(/\.[^/.]+$/, "")); | |
| } | |
| }; | |
| const handleDrag = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (e.type === "dragenter" || e.type === "dragover") { | |
| setDragActive(true); | |
| } else if (e.type === "dragleave") { | |
| setDragActive(false); | |
| } | |
| }, []); | |
| const handleDrop = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setDragActive(false); | |
| if (e.dataTransfer.files && e.dataTransfer.files[0]) { | |
| handleFileSelect(e.dataTransfer.files[0]); | |
| } | |
| }, []); | |
| const isValidFile = (file: File) => { | |
| const allowedTypes = [".txt", ".md", ".json", ".csv"]; | |
| return allowedTypes.some((type) => file.name.toLowerCase().endsWith(type)); | |
| }; | |
| const formatFileSize = (bytes: number) => { | |
| if (bytes === 0) return "0 Bytes"; | |
| const k = 1024; | |
| const sizes = ["Bytes", "KB", "MB"]; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; | |
| }; | |
| return ( | |
| <Dialog open={isOpen} onOpenChange={handleClose}> | |
| <DialogContent className="max-w-2xl"> | |
| <DialogHeader> | |
| <DialogTitle>Add Context Document</DialogTitle> | |
| </DialogHeader> | |
| <Tabs defaultValue="paste" className="w-full"> | |
| <TabsList className="grid w-full grid-cols-2"> | |
| <TabsTrigger value="paste"> | |
| <FileText className="h-4 w-4 mr-2" /> | |
| Paste Text | |
| </TabsTrigger> | |
| <TabsTrigger value="upload"> | |
| <Upload className="h-4 w-4 mr-2" /> | |
| Upload File | |
| </TabsTrigger> | |
| </TabsList> | |
| {/* Common Fields */} | |
| <div className="space-y-4 mt-6"> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="title">Title *</Label> | |
| <Input | |
| id="title" | |
| value={title} | |
| onChange={(e) => setTitle(e.target.value)} | |
| placeholder="Enter document title" | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="document-type">Document Type *</Label> | |
| <Select | |
| value={documentType} | |
| onValueChange={(value: ContextDocumentType) => | |
| setDocumentType(value) | |
| } | |
| > | |
| <SelectTrigger> | |
| <SelectValue placeholder="Select document type" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {CONTEXT_DOCUMENT_TYPES.map((type) => ( | |
| <SelectItem key={type.value} value={type.value}> | |
| <div> | |
| <div className="font-medium">{type.label}</div> | |
| <div className="text-xs text-muted-foreground"> | |
| {type.description} | |
| </div> | |
| </div> | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Tab Content */} | |
| <TabsContent value="paste" className="space-y-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="content">Content *</Label> | |
| <Textarea | |
| id="content" | |
| value={content} | |
| onChange={(e) => setContent(e.target.value)} | |
| placeholder="Paste or type your context document content here..." | |
| className="min-h-[200px] font-mono text-sm" | |
| required | |
| /> | |
| <div className="text-xs text-muted-foreground text-right"> | |
| {content.length.toLocaleString()} / 100,000 characters | |
| </div> | |
| </div> | |
| <div className="flex justify-end gap-2"> | |
| <Button variant="outline" onClick={handleClose}> | |
| Cancel | |
| </Button> | |
| <Button | |
| onClick={handleCreateFromText} | |
| disabled={ | |
| loading || | |
| !title.trim() || | |
| !content.trim() || | |
| content.length > 100000 | |
| } | |
| > | |
| {loading ? "Creating..." : "Create Document"} | |
| </Button> | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="upload" className="space-y-4"> | |
| <div className="space-y-4"> | |
| {/* File Drop Zone */} | |
| <div | |
| className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${ | |
| dragActive | |
| ? "border-primary bg-primary/5" | |
| : "border-muted-foreground/25 hover:border-muted-foreground/50" | |
| }`} | |
| onDragEnter={handleDrag} | |
| onDragLeave={handleDrag} | |
| onDragOver={handleDrag} | |
| onDrop={handleDrop} | |
| > | |
| {selectedFile ? ( | |
| <div className="space-y-2"> | |
| <Check className="h-8 w-8 text-green-500 mx-auto" /> | |
| <div className="font-medium">{selectedFile.name}</div> | |
| <div className="text-sm text-muted-foreground"> | |
| {formatFileSize(selectedFile.size)} | |
| </div> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => setSelectedFile(null)} | |
| > | |
| Choose Different File | |
| </Button> | |
| </div> | |
| ) : ( | |
| <div className="space-y-2"> | |
| <Upload className="h-8 w-8 text-muted-foreground mx-auto" /> | |
| <div className="font-medium"> | |
| Drop your file here or click to browse | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Supported formats: .txt, .md, .json, .csv (max 1MB) | |
| </div> | |
| <Input | |
| type="file" | |
| accept=".txt,.md,.json,.csv" | |
| onChange={(e) => { | |
| const file = e.target.files?.[0]; | |
| if (file && isValidFile(file)) { | |
| handleFileSelect(file); | |
| } | |
| }} | |
| className="hidden" | |
| id="file-upload" | |
| /> | |
| <Button | |
| variant="outline" | |
| onClick={() => | |
| document.getElementById("file-upload")?.click() | |
| } | |
| > | |
| Browse Files | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| {selectedFile && !isValidFile(selectedFile) && ( | |
| <div className="flex items-center gap-2 text-destructive text-sm"> | |
| <AlertCircle className="h-4 w-4" /> | |
| Unsupported file type. Please select a .txt, .md, .json, or | |
| .csv file. | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex justify-end gap-2"> | |
| <Button variant="outline" onClick={handleClose}> | |
| Cancel | |
| </Button> | |
| <Button | |
| onClick={handleFileUpload} | |
| disabled={ | |
| loading || | |
| !selectedFile || | |
| !title.trim() || | |
| !isValidFile(selectedFile) | |
| } | |
| > | |
| {loading ? "Uploading..." : "Upload Document"} | |
| </Button> | |
| </div> | |
| </TabsContent> | |
| </Tabs> | |
| {error && ( | |
| <div className="flex items-center gap-2 text-destructive text-sm mt-4 p-3 bg-destructive/10 rounded"> | |
| <AlertCircle className="h-4 w-4" /> | |
| {error} | |
| </div> | |
| )} | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| } | |