Spaces:
Running
Running
| import React, { useState } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogFooter, | |
| DialogHeader, | |
| DialogTitle, | |
| } from "@/components/ui/dialog"; | |
| import { GitBranch, Trash2 } from "lucide-react"; | |
| import { Trace } from "@/types"; | |
| import { useAgentGraph } from "@/context/AgentGraphContext"; | |
| import { api } from "@/lib/api"; | |
| interface TraceItemProps { | |
| trace: Trace; | |
| } | |
| export function TraceItem({ trace }: TraceItemProps) { | |
| const { state, actions } = useAgentGraph(); | |
| const { selectedTrace } = state; | |
| const [isDeleting, setIsDeleting] = useState(false); | |
| const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); | |
| const isSelected = selectedTrace?.trace_id === trace.trace_id; | |
| const handleSelect = () => { | |
| // Set trace as selected and switch to trace-kg view | |
| actions.setSelectedTrace(trace); | |
| actions.setActiveView("trace-kg"); | |
| }; | |
| const handleDeleteClick = (e: React.MouseEvent) => { | |
| e.stopPropagation(); // Prevent trace selection | |
| setDeleteDialogOpen(true); | |
| }; | |
| const handleDeleteConfirm = async () => { | |
| setIsDeleting(true); | |
| try { | |
| await api.traces.delete(trace.trace_id); | |
| // Refresh traces list by reloading | |
| window.location.reload(); | |
| } catch (error) { | |
| console.error("Error deleting trace:", error); | |
| alert( | |
| `Error deleting trace: ${ | |
| error instanceof Error ? error.message : String(error) | |
| }` | |
| ); | |
| } finally { | |
| setIsDeleting(false); | |
| setDeleteDialogOpen(false); | |
| } | |
| }; | |
| const handleDeleteCancel = () => { | |
| setDeleteDialogOpen(false); | |
| }; | |
| const hasKnowledgeGraphs = () => { | |
| return trace.knowledge_graphs && trace.knowledge_graphs.length > 0; | |
| }; | |
| const formatDate = (dateString?: string) => { | |
| if (!dateString) return ""; | |
| return new Date(dateString).toLocaleDateString(); | |
| }; | |
| const formatSize = (bytes?: number) => { | |
| if (!bytes) return ""; | |
| const mb = bytes / (1024 * 1024); | |
| return `${mb.toFixed(1)} MB`; | |
| }; | |
| const kgCount = trace.knowledge_graphs?.length || 0; | |
| return ( | |
| <> | |
| <div className={`group relative`}> | |
| {/* Delete button - appears on hover */} | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity h-8 w-8 p-0 bg-background/80 hover:bg-destructive hover:text-destructive-foreground" | |
| onClick={handleDeleteClick} | |
| disabled={isDeleting} | |
| title="Delete trace" | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant={isSelected ? "secondary" : "ghost"} | |
| className="w-full justify-start p-3 h-auto min-h-[4rem] overflow-hidden" | |
| onClick={handleSelect} | |
| disabled={isDeleting} | |
| > | |
| <div className="flex items-start gap-3 w-full"> | |
| <div className="flex-1 min-w-0 text-left"> | |
| <div className="flex items-center gap-2 mb-1"> | |
| <span className="font-medium truncate">{trace.filename}</span> | |
| {hasKnowledgeGraphs() && ( | |
| <div className="flex items-center gap-1 text-xs text-muted-foreground"> | |
| <GitBranch className="h-3 w-3" /> | |
| {(() => { | |
| // Count only final knowledge graphs (same logic as main content) | |
| const finalKGs = trace.knowledge_graphs!.filter( | |
| (kg) => | |
| kg.is_final === true || | |
| (kg.window_index === null && kg.window_total !== null) | |
| ); | |
| const finalCount = finalKGs.length; | |
| return finalCount; | |
| })()} | |
| {/* Show total window count across all final KGs */} | |
| {(() => { | |
| const totalWindows = trace.knowledge_graphs!.reduce( | |
| (acc, kg) => | |
| acc + (kg.window_knowledge_graphs?.length || 0), | |
| 0 | |
| ); | |
| return totalWindows > 0 ? ( | |
| <span className="text-[10px]">+{totalWindows}W</span> | |
| ) : null; | |
| })()} | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-4 text-xs text-muted-foreground"> | |
| <span>{formatDate(trace.upload_timestamp)}</span> | |
| {trace.character_count && ( | |
| <span>{trace.character_count.toLocaleString()} chars</span> | |
| )} | |
| {trace.size && <span>{formatSize(trace.size)}</span>} | |
| </div> | |
| </div> | |
| </div> | |
| </Button> | |
| </div> | |
| {/* Delete Confirmation Dialog */} | |
| <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> | |
| <DialogContent className="sm:max-w-md max-w-[90vw] w-full"> | |
| <DialogHeader> | |
| <DialogTitle>Delete Trace</DialogTitle> | |
| <DialogDescription className="space-y-2"> | |
| <div className="break-words"> | |
| Are you sure you want to delete the trace "{trace.filename}"? | |
| </div> | |
| <div className="space-y-1 text-sm"> | |
| <div>This will permanently remove:</div> | |
| <div>• The trace and all its content</div> | |
| <div>• All associated knowledge graphs ({kgCount} found)</div> | |
| <div>• All entities and relationships</div> | |
| <div>• All prompt reconstruction data</div> | |
| <div>• All perturbation test results</div> | |
| <div>• All causal analysis results</div> | |
| </div> | |
| <div className="font-medium text-destructive"> | |
| This action cannot be undone. | |
| </div> | |
| </DialogDescription> | |
| </DialogHeader> | |
| <DialogFooter> | |
| <Button variant="outline" onClick={handleDeleteCancel}> | |
| Cancel | |
| </Button> | |
| <Button | |
| variant="destructive" | |
| onClick={handleDeleteConfirm} | |
| disabled={isDeleting} | |
| > | |
| {isDeleting ? "Deleting..." : "Delete"} | |
| </Button> | |
| </DialogFooter> | |
| </DialogContent> | |
| </Dialog> | |
| </> | |
| ); | |
| } | |