Spaces:
Running
Running
| import React, { useState, useCallback } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Progress } from "@/components/ui/progress"; | |
| import { | |
| Card, | |
| CardContent, | |
| CardDescription, | |
| CardHeader, | |
| CardTitle, | |
| } from "@/components/ui/card"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from "@/components/ui/select"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { Label } from "@/components/ui/label"; | |
| import { | |
| Upload, | |
| X, | |
| CheckCircle, | |
| AlertCircle, | |
| FileText, | |
| Trash2, | |
| ArrowUpDown, | |
| Copy, | |
| } from "lucide-react"; | |
| import { useSystemNotifications } from "@/hooks/useSystemNotifications"; | |
| import { useAgentGraph } from "@/context/AgentGraphContext"; | |
| import { api } from "@/lib/api"; | |
| type ConcatenationMethod = "newlines" | "timestamps" | "custom" | "none"; | |
| interface FileUpload { | |
| file: File; | |
| id: string; | |
| preview?: string; | |
| } | |
| export function UploadView() { | |
| const [uploadProgress, setUploadProgress] = useState(0); | |
| const [isUploading, setIsUploading] = useState(false); | |
| const [selectedFiles, setSelectedFiles] = useState<FileUpload[]>([]); | |
| const [uploadComplete, setUploadComplete] = useState(false); | |
| const [concatenationMethod, setConcatenationMethod] = | |
| useState<ConcatenationMethod>("newlines"); | |
| const [customSeparator, setCustomSeparator] = useState("\n---\n"); | |
| const [finalFilename, setFinalFilename] = useState(""); | |
| const [isDragging, setIsDragging] = useState(false); | |
| const { notifySuccess, notifyError } = useSystemNotifications(); | |
| const { actions } = useAgentGraph(); | |
| const handleFileSelect = useCallback( | |
| (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const files = e.target.files; | |
| if (files) { | |
| addFiles(Array.from(files)); | |
| } | |
| }, | |
| [] | |
| ); | |
| const handleDragOver = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| setIsDragging(true); | |
| }, []); | |
| const handleDragLeave = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| setIsDragging(false); | |
| }, []); | |
| const handleDrop = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| setIsDragging(false); | |
| const files = Array.from(e.dataTransfer.files); | |
| if (files.length > 0) { | |
| addFiles(files); | |
| } | |
| }, []); | |
| const addFiles = (files: File[]) => { | |
| const newFiles = files.map((file) => ({ | |
| file, | |
| id: Math.random().toString(36).substr(2, 9), | |
| preview: file.size < 50000 ? "Loading..." : undefined, // Only preview small files | |
| })); | |
| setSelectedFiles((prev) => [...prev, ...newFiles]); | |
| setUploadComplete(false); | |
| // Generate filename suggestion | |
| if (files.length === 1 && selectedFiles.length === 0) { | |
| setFinalFilename(files[0]?.name || "trace"); | |
| } else { | |
| setFinalFilename(`combined_trace_${Date.now()}.${getOutputExtension()}`); | |
| } | |
| }; | |
| const removeFile = (id: string) => { | |
| setSelectedFiles((prev) => prev.filter((f) => f.id !== id)); | |
| }; | |
| const moveFile = (id: string, direction: "up" | "down") => { | |
| setSelectedFiles((prev) => { | |
| const index = prev.findIndex((f) => f.id === id); | |
| if (index === -1) return prev; | |
| const newFiles = [...prev]; | |
| const targetIndex = direction === "up" ? index - 1 : index + 1; | |
| if ( | |
| targetIndex >= 0 && | |
| targetIndex < newFiles.length && | |
| newFiles[index] && | |
| newFiles[targetIndex] | |
| ) { | |
| [newFiles[index], newFiles[targetIndex]] = [ | |
| newFiles[targetIndex], | |
| newFiles[index], | |
| ]; | |
| } | |
| return newFiles; | |
| }); | |
| }; | |
| const getOutputExtension = () => { | |
| const hasJson = selectedFiles.some((f) => f.file.name.endsWith(".json")); | |
| return hasJson ? "json" : "txt"; | |
| }; | |
| const concatenateFiles = async (): Promise<string> => { | |
| if (selectedFiles.length === 1) { | |
| return (await selectedFiles[0]?.file.text()) || ""; | |
| } | |
| const fileContents = await Promise.all( | |
| selectedFiles.map(async (fileUpload) => { | |
| const content = await fileUpload.file.text(); | |
| return { | |
| filename: fileUpload.file.name, | |
| content: content.trim(), | |
| }; | |
| }) | |
| ); | |
| switch (concatenationMethod) { | |
| case "none": | |
| return fileContents.map((f) => f.content).join(""); | |
| case "newlines": | |
| return fileContents.map((f) => f.content).join("\n\n"); | |
| case "timestamps": | |
| return fileContents | |
| .map( | |
| (f) => | |
| `=== File: ${f.filename} (${new Date().toISOString()}) ===\n${ | |
| f.content | |
| }` | |
| ) | |
| .join("\n\n"); | |
| case "custom": | |
| return fileContents.map((f) => f.content).join(customSeparator); | |
| default: | |
| return fileContents.map((f) => f.content).join("\n\n"); | |
| } | |
| }; | |
| const handleUpload = async () => { | |
| if (selectedFiles.length === 0) return; | |
| setIsUploading(true); | |
| setUploadProgress(0); | |
| try { | |
| let fileToUpload: File; | |
| if (selectedFiles.length === 1 && selectedFiles[0]) { | |
| fileToUpload = selectedFiles[0].file; | |
| } else { | |
| // Concatenate multiple files | |
| const combinedContent = await concatenateFiles(); | |
| const blob = new Blob([combinedContent], { type: "text/plain" }); | |
| fileToUpload = new File([blob], finalFilename, { type: blob.type }); | |
| } | |
| const trace = await api.traces.upload(fileToUpload, (progress) => { | |
| setUploadProgress(progress); | |
| }); | |
| notifySuccess( | |
| "Upload Successful", | |
| `${fileToUpload.name} has been uploaded successfully.` | |
| ); | |
| // Refresh traces list | |
| const traces = await api.traces.list(); | |
| actions.setTraces(traces); | |
| // Select the uploaded trace | |
| actions.setSelectedTrace(trace); | |
| // Mark upload as complete | |
| setUploadComplete(true); | |
| setUploadProgress(100); | |
| } catch (error) { | |
| notifyError( | |
| "Upload Failed", | |
| error instanceof Error ? error.message : "Failed to upload file" | |
| ); | |
| } finally { | |
| setIsUploading(false); | |
| } | |
| }; | |
| const handleReset = () => { | |
| setSelectedFiles([]); | |
| setUploadProgress(0); | |
| setUploadComplete(false); | |
| setFinalFilename(""); | |
| }; | |
| const handleViewTrace = () => { | |
| actions.setActiveView("traces"); | |
| }; | |
| const getFileIcon = (fileName: string) => { | |
| const extension = fileName.toLowerCase().split(".").pop(); | |
| switch (extension) { | |
| case "json": | |
| return "📄"; | |
| case "txt": | |
| return "📝"; | |
| case "log": | |
| return "📋"; | |
| default: | |
| return "📄"; | |
| } | |
| }; | |
| const formatFileSize = (bytes: number) => { | |
| if (bytes === 0) return "0 Bytes"; | |
| const k = 1024; | |
| const sizes = ["Bytes", "KB", "MB", "GB"]; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; | |
| }; | |
| return ( | |
| <div className="p-6 space-y-6 max-w-7xl mx-auto"> | |
| {/* Upload Instructions */} | |
| <div | |
| className={`border border-dashed rounded-lg p-8 text-center space-y-4 transition-colors ${ | |
| isDragging | |
| ? "border-primary bg-primary/5 border-2" | |
| : "border-gray-300 hover:border-primary/50" | |
| }`} | |
| onDragOver={handleDragOver} | |
| onDragLeave={handleDragLeave} | |
| onDrop={handleDrop} | |
| > | |
| <div className="space-y-2"> | |
| <Upload className="h-12 w-12 text-muted-foreground mx-auto" /> | |
| <h3 className="text-lg font-semibold"> | |
| Drop files here or click to upload | |
| </h3> | |
| <p className="text-muted-foreground max-w-md mx-auto"> | |
| Select trace files (JSON, TXT, JSONL) to upload. You can select | |
| multiple files at once. | |
| </p> | |
| </div> | |
| <Button size="lg" variant="outline" asChild> | |
| <label className="cursor-pointer"> | |
| <input | |
| type="file" | |
| className="hidden" | |
| accept=".json,.txt,.log" | |
| multiple | |
| onChange={handleFileSelect} | |
| /> | |
| Browse Files | |
| </label> | |
| </Button> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| {/* Left Column - Upload Functionality */} | |
| <div className="space-y-6"> | |
| {/* Selected Files */} | |
| {selectedFiles.length > 0 && ( | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <h4 className="font-medium"> | |
| Selected Files ({selectedFiles.length}) | |
| </h4> | |
| <Button variant="ghost" size="sm" onClick={handleReset}> | |
| <X className="h-4 w-4 mr-1" /> | |
| Clear All | |
| </Button> | |
| </div> | |
| <div className="space-y-2 max-h-60 overflow-y-auto"> | |
| {selectedFiles.map((fileUpload, index) => ( | |
| <div | |
| key={fileUpload.id} | |
| className="flex items-center gap-3 p-3 border rounded-lg bg-muted/20" | |
| > | |
| <div className="text-2xl"> | |
| {getFileIcon(fileUpload.file.name)} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <p | |
| className="font-medium text-sm truncate" | |
| title={fileUpload.file.name} | |
| > | |
| {fileUpload.file.name} | |
| </p> | |
| <div className="flex items-center gap-2 text-xs text-muted-foreground"> | |
| <span>{formatFileSize(fileUpload.file.size)}</span> | |
| <span>•</span> | |
| <span>{fileUpload.file.type || "Unknown type"}</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-1"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => moveFile(fileUpload.id, "up")} | |
| disabled={index === 0} | |
| > | |
| <ArrowUpDown className="h-3 w-3" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => removeFile(fileUpload.id)} | |
| > | |
| <Trash2 className="h-3 w-3" /> | |
| </Button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Multiple Files Configuration */} | |
| {selectedFiles.length > 1 && ( | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Copy className="h-5 w-5" /> | |
| File Concatenation | |
| </CardTitle> | |
| <CardDescription> | |
| Configure how multiple files should be combined | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="concatenation-method"> | |
| Concatenation Method | |
| </Label> | |
| <Select | |
| value={concatenationMethod} | |
| onValueChange={(value: ConcatenationMethod) => | |
| setConcatenationMethod(value) | |
| } | |
| > | |
| <SelectTrigger> | |
| <SelectValue /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="newlines"> | |
| Separate with blank lines | |
| </SelectItem> | |
| <SelectItem value="timestamps"> | |
| Add timestamps and filenames | |
| </SelectItem> | |
| <SelectItem value="custom">Custom separator</SelectItem> | |
| <SelectItem value="none"> | |
| No separation (direct concatenation) | |
| </SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| {concatenationMethod === "custom" && ( | |
| <div className="space-y-2"> | |
| <Label htmlFor="custom-separator">Custom Separator</Label> | |
| <Textarea | |
| id="custom-separator" | |
| value={customSeparator} | |
| onChange={(e) => setCustomSeparator(e.target.value)} | |
| placeholder="Enter custom separator..." | |
| rows={2} | |
| /> | |
| </div> | |
| )} | |
| <div className="space-y-2"> | |
| <Label htmlFor="final-filename">Final Filename</Label> | |
| <input | |
| id="final-filename" | |
| type="text" | |
| value={finalFilename} | |
| onChange={(e) => setFinalFilename(e.target.value)} | |
| className="w-full px-3 py-2 border rounded-lg text-sm" | |
| placeholder="Enter filename for combined file..." | |
| /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| {/* Upload Progress & Actions */} | |
| {selectedFiles.length > 0 && ( | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <FileText className="h-5 w-5" /> | |
| {uploadComplete ? "Upload Complete" : "Ready to Upload"} | |
| </CardTitle> | |
| <CardDescription> | |
| {selectedFiles.length === 1 && selectedFiles[0] | |
| ? `Upload ${selectedFiles[0].file.name}` | |
| : `Combine and upload ${selectedFiles.length} files`} | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| {/* Upload Progress */} | |
| {isUploading && ( | |
| <div className="space-y-3"> | |
| <div className="flex items-center justify-between text-sm"> | |
| <span className="text-muted-foreground"> | |
| Uploading... | |
| </span> | |
| <span className="font-medium"> | |
| {Math.round(uploadProgress)}% | |
| </span> | |
| </div> | |
| <Progress value={uploadProgress} className="h-3" /> | |
| </div> | |
| )} | |
| {/* Success State */} | |
| {uploadComplete && ( | |
| <div className="p-4 bg-green-50 border border-green-200 rounded-lg"> | |
| <div className="flex items-center gap-2 text-green-800 mb-2"> | |
| <CheckCircle className="h-5 w-5" /> | |
| <span className="font-medium">Upload Successful!</span> | |
| </div> | |
| <p className="text-sm text-green-700 mb-4"> | |
| Your trace file has been uploaded and is ready for | |
| analysis. | |
| </p> | |
| <div className="flex gap-2"> | |
| <Button onClick={handleViewTrace} size="sm"> | |
| View in Traces | |
| </Button> | |
| <Button onClick={handleReset} variant="outline" size="sm"> | |
| Upload Another | |
| </Button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Upload Actions */} | |
| {!uploadComplete && ( | |
| <div className="flex gap-3"> | |
| <Button | |
| variant="outline" | |
| onClick={handleReset} | |
| disabled={isUploading} | |
| className="flex-1" | |
| > | |
| Reset | |
| </Button> | |
| <Button | |
| onClick={handleUpload} | |
| disabled={isUploading} | |
| className="flex-1" | |
| size="lg" | |
| > | |
| {isUploading | |
| ? "Uploading..." | |
| : selectedFiles.length > 1 | |
| ? "Combine & Upload" | |
| : "Upload File"} | |
| </Button> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| )} | |
| {/* Minimum Viable Trace Example */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-lg"> | |
| Minimum Viable Trace Example | |
| </CardTitle> | |
| <CardDescription> | |
| AgentGraph can process even basic conversation logs with just | |
| agent identification and input/output pairs | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="bg-slate-50 border rounded-lg p-4"> | |
| <pre className="text-xs text-slate-700 whitespace-pre-wrap"> | |
| {`{ | |
| "agent_name": "assistant", | |
| "input": "user message", | |
| "output": "agent response" | |
| }`} | |
| </pre> | |
| </div> | |
| <p className="text-xs text-muted-foreground mt-3"> | |
| <span className="font-medium">Note:</span> Analysis capabilities | |
| scale significantly with additional structured metadata. | |
| </p> | |
| </CardContent> | |
| </Card> | |
| {/* File Guidelines */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Upload className="h-5 w-5" /> | |
| File Guidelines | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="space-y-4 text-sm"> | |
| <div> | |
| <h4 className="font-medium mb-2">Supported File Types</h4> | |
| <ul className="space-y-1 text-muted-foreground"> | |
| <li>• JSON files (.json) - Structured trace data</li> | |
| <li>• Text files (.txt) - Plain text traces</li> | |
| <li>• Log files (.log) - Application logs</li> | |
| </ul> | |
| </div> | |
| <Separator /> | |
| <div> | |
| <h4 className="font-medium mb-2">File Requirements</h4> | |
| <ul className="space-y-1 text-muted-foreground"> | |
| <li>• Maximum file size: 50MB per file</li> | |
| <li>• UTF-8 encoding recommended</li> | |
| <li>• Well-formed JSON for .json files</li> | |
| <li> | |
| • Multiple files will be concatenated based on your | |
| settings | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Right Column - AgentGraph Trace Requirements */} | |
| <div className="space-y-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <AlertCircle className="h-5 w-5" /> | |
| AgentGraph Trace Requirements | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="space-y-6 text-sm"> | |
| {/* Essential Requirements */} | |
| <div> | |
| <h4 className="font-semibold text-green-700 mb-3"> | |
| Essential (Absolute Minimum): | |
| </h4> | |
| <ul className="space-y-2 text-muted-foreground"> | |
| <li> | |
| •{" "} | |
| <span className="font-medium">Agent Identification</span>{" "} | |
| - Clear agent names/types to distinguish different actors | |
| </li> | |
| <li> | |
| •{" "} | |
| <span className="font-medium">Input/Output Messages</span>{" "} | |
| - Core conversation content (user inputs and agent | |
| responses) | |
| </li> | |
| </ul> | |
| </div> | |
| <Separator /> | |
| {/* Highly Recommended */} | |
| <div> | |
| <h4 className="font-semibold text-blue-700 mb-3"> | |
| Highly Recommended: | |
| </h4> | |
| <ul className="space-y-2 text-muted-foreground"> | |
| <li> | |
| • <span className="font-medium">System Prompts</span> - | |
| Agent instructions and personas (improves entity | |
| classification) | |
| </li> | |
| <li> | |
| •{" "} | |
| <span className="font-medium"> | |
| Hierarchical Structure | |
| </span>{" "} | |
| - Parent-child relationships between spans/steps for | |
| execution flow analysis | |
| </li> | |
| <li> | |
| • <span className="font-medium">Tool Usage Details</span>{" "} | |
| - Function calls with parameters and return values | |
| (critical for multi-step agents) | |
| </li> | |
| <li> | |
| • <span className="font-medium">Error Information</span> - | |
| Exception details and failure modes for anomaly detection | |
| </li> | |
| </ul> | |
| </div> | |
| <Separator /> | |
| {/* Optional */} | |
| <div> | |
| <h4 className="font-semibold text-purple-700 mb-3"> | |
| Optional (Enhanced Analysis): | |
| </h4> | |
| <ul className="space-y-2 text-muted-foreground"> | |
| <li> | |
| •{" "} | |
| <span className="font-medium"> | |
| Intermediate Reasoning | |
| </span>{" "} | |
| - Chain-of-thought processes (enhances causal analysis) | |
| </li> | |
| <li> | |
| •{" "} | |
| <span className="font-medium"> | |
| Unique Trace Identifier | |
| </span>{" "} | |
| - Consistent ID to track complete agent workflows (e.g., | |
| trace_id, session_id) | |
| </li> | |
| <li> | |
| • <span className="font-medium">Timestamps</span> - | |
| Start/end times for temporal analysis and performance | |
| optimization | |
| </li> | |
| <li> | |
| • <span className="font-medium">Basic Status</span> - | |
| Success/failure indication for each step | |
| </li> | |
| <li> | |
| • <span className="font-medium">Model Information</span> - | |
| LLM model names, versions, parameters (enables | |
| model-specific insights) | |
| </li> | |
| <li> | |
| • <span className="font-medium">Token Usage</span> - | |
| Input/output token counts (for cost analysis) | |
| </li> | |
| <li> | |
| • <span className="font-medium">Execution Context</span> - | |
| Environment details, API endpoints, service versions | |
| </li> | |
| </ul> | |
| </div> | |
| <Separator /> | |
| {/* Multi-Agent Specific */} | |
| <div> | |
| <h4 className="font-semibold text-orange-700 mb-3"> | |
| Multi-Agent Specific (Optional): | |
| </h4> | |
| <ul className="space-y-2 text-muted-foreground"> | |
| <li> | |
| • <span className="font-medium">Task Decomposition</span>{" "} | |
| - How tasks are split among agents (Normally in the Agent | |
| Setting yaml file in CrewAI) | |
| </li> | |
| <li> | |
| • <span className="font-medium">Dependency Tracking</span>{" "} | |
| - Inter-agent dependencies (Access level of each agent) | |
| </li> | |
| <li> | |
| •{" "} | |
| <span className="font-medium">Orchestration Events</span>{" "} | |
| - Workflow coordination and handoffs (Sometimes defined in | |
| the parameters or code) | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |