wu981526092's picture
Fix 429 Rate Limit Error and TypeScript Issues
df14457
import React, { useEffect, useCallback, useState, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
FileText,
GitBranch,
Search,
Filter,
X,
ArrowUpDown,
Eye,
Trash2,
Info,
HelpCircle,
} from "lucide-react";
import { useAgentGraph } from "@/context/AgentGraphContext";
import { useModal } from "@/context/ModalContext";
import { EmptyState } from "@/components/shared/EmptyState";
import { api } from "@/lib/api";
// Types for filtering and sorting
type SortField = "name" | "date" | "content" | "type" | "status";
type SortDirection = "asc" | "desc";
type StatusFilter = "all" | "processed" | "ready";
type TypeFilter = "all" | string;
export function TracesView() {
const { state, actions } = useAgentGraph();
const { openModal } = useModal();
const { traces, isLoading } = state;
// Search and filter states
const [searchQuery, setSearchQuery] = useState("");
const [typeFilter, setTypeFilter] = useState<TypeFilter>("all");
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
const [sortField, setSortField] = useState<SortField>("date");
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
// Simple bulk selection state
const [selectedTraces, setSelectedTraces] = useState<string[]>([]);
// Format trace type function
const formatTraceType = (type?: string) => {
if (!type) return "Unknown";
return type.replace(/[-_]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
};
// Format date function
const formatDate = (dateString?: string) => {
if (!dateString) return "N/A";
try {
const date = new Date(dateString);
return date.toLocaleDateString();
} catch {
return "N/A";
}
};
// Get unique trace types for filter dropdown
const uniqueTypes = useMemo(() => {
const types = new Set<string>();
traces.forEach((trace) => {
if (trace.trace_type) {
types.add(trace.trace_type);
}
});
return Array.from(types).sort();
}, [traces]);
// Filter and sort traces
const filteredAndSortedTraces = useMemo(() => {
let filtered = traces;
// Apply search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(
(trace) =>
trace.filename.toLowerCase().includes(query) ||
trace.description?.toLowerCase().includes(query) ||
trace.title?.toLowerCase().includes(query)
);
}
// Apply type filter
if (typeFilter !== "all") {
filtered = filtered.filter((trace) => trace.trace_type === typeFilter);
}
// Apply status filter
if (statusFilter !== "all") {
filtered = filtered.filter((trace) => {
const hasGraphs =
trace.knowledge_graphs && trace.knowledge_graphs.length > 0;
if (statusFilter === "processed") return hasGraphs;
if (statusFilter === "ready") return !hasGraphs;
return true;
});
}
// Apply sorting
filtered.sort((a, b) => {
let valueA: any, valueB: any;
switch (sortField) {
case "name":
valueA = a.filename.toLowerCase();
valueB = b.filename.toLowerCase();
break;
case "date":
// Sort by upload timestamp (added date)
valueA = new Date(a.upload_timestamp || a.timestamp || 0).getTime();
valueB = new Date(b.upload_timestamp || b.timestamp || 0).getTime();
break;
case "content":
valueA = a.character_count || 0;
valueB = b.character_count || 0;
break;
case "type":
valueA = a.trace_type || "";
valueB = b.trace_type || "";
break;
case "status":
valueA = (a.knowledge_graphs?.length || 0) > 0 ? 1 : 0;
valueB = (b.knowledge_graphs?.length || 0) > 0 ? 1 : 0;
break;
default:
valueA = a.filename;
valueB = b.filename;
}
if (valueA < valueB) return sortDirection === "asc" ? -1 : 1;
if (valueA > valueB) return sortDirection === "asc" ? 1 : -1;
return 0;
});
return filtered;
}, [traces, searchQuery, typeFilter, statusFilter, sortField, sortDirection]);
// Clear all filters
const clearFilters = () => {
setSearchQuery("");
setTypeFilter("all");
setStatusFilter("all");
setSortField("date");
setSortDirection("desc");
};
// Check if any filters are active
const hasActiveFilters =
searchQuery.trim() || typeFilter !== "all" || statusFilter !== "all";
// Simplified bulk operation handlers
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedTraces(
filteredAndSortedTraces.map((trace) => trace.id.toString())
);
} else {
setSelectedTraces([]);
}
};
const handleSelectTrace = (traceId: string, checked: boolean) => {
if (checked) {
setSelectedTraces((prev) => [...prev, traceId]);
} else {
setSelectedTraces((prev) => prev.filter((id) => id !== traceId));
}
};
const handleBulkDelete = () => {
if (selectedTraces.length === 0) return;
const confirmed = window.confirm(
`Are you sure you want to delete ${selectedTraces.length} trace${
selectedTraces.length !== 1 ? "s" : ""
}? This action cannot be undone.`
);
if (confirmed) {
// Simple deletion without complex loading states
selectedTraces.forEach(async (traceId) => {
try {
await api.traces.delete(traceId);
} catch (error) {
console.error("Error deleting trace:", error);
}
});
// Refresh traces and clear selection
setTimeout(() => {
loadTraces();
setSelectedTraces([]);
}, 500);
}
};
const handleViewTrace = (trace: any, e: React.MouseEvent) => {
e.stopPropagation();
actions.setSelectedTrace(trace);
actions.setActiveView("trace-kg");
};
const handleViewTraceDetails = async (trace: any, e: React.MouseEvent) => {
e.stopPropagation();
openModal(
"trace-details",
`Trace Details - ${trace.filename}`,
{
trace: trace,
knowledgeGraphs: trace.knowledge_graphs || [],
},
{
size: "xl",
closable: true,
}
);
};
const handleDeleteSingleTrace = async (
traceId: string,
e: React.MouseEvent
) => {
e.stopPropagation();
if (window.confirm("Are you sure you want to delete this trace?")) {
try {
await api.traces.delete(traceId);
await loadTraces();
} catch (error) {
console.error("Error deleting trace:", error);
alert("Error deleting trace. Please try again.");
}
}
};
const isAllSelected =
filteredAndSortedTraces.length > 0 &&
filteredAndSortedTraces.every((trace) =>
selectedTraces.includes(trace.id.toString())
);
const isPartiallySelected = selectedTraces.length > 0 && !isAllSelected;
const loadTraces = useCallback(async () => {
actions.setLoading(true);
try {
const tracesData = await api.traces.list();
actions.setTraces(Array.isArray(tracesData) ? tracesData : []);
} catch (error) {
actions.setError(
error instanceof Error ? error.message : "Failed to load traces"
);
actions.setTraces([]);
} finally {
actions.setLoading(false);
}
}, [actions]);
useEffect(() => {
loadTraces();
}, [loadTraces]);
// Auto-refresh traces every 60 seconds (increased for HF Spaces compatibility)
useEffect(() => {
const interval = setInterval(() => {
if (!isLoading) {
console.log("🔄 Auto-refreshing traces...");
loadTraces();
}
}, 60000); // 60 seconds (increased from 12s to avoid 429 errors)
return () => clearInterval(interval);
}, [loadTraces, isLoading]);
const handleUploadTrace = () => {
actions.setActiveView("upload");
};
return (
<TooltipProvider>
<div className="p-6 space-y-6">
{/* Main Content */}
<div className="space-y-6">
{/* Search and Filter Toolbar */}
<div className="space-y-4">
{/* Search Bar */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search traces by name or description..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
{/* Filters Row */}
<div className="flex flex-wrap items-center gap-4">
{/* Type Filter */}
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-muted-foreground" />
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="All Types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
{uniqueTypes.map((type) => (
<SelectItem key={type} value={type}>
{formatTraceType(type)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Status Filter */}
<Select
value={statusFilter}
onValueChange={(value: string) =>
setStatusFilter(value as StatusFilter)
}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="All Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="processed">Processed</SelectItem>
<SelectItem value="ready">Ready</SelectItem>
</SelectContent>
</Select>
{/* Sort Options */}
<div className="flex items-center gap-2">
<ArrowUpDown className="h-4 w-4 text-muted-foreground" />
<Select
value={sortField}
onValueChange={(value: SortField) => setSortField(value)}
>
<SelectTrigger className="w-32">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="date">Date</SelectItem>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="type">Type</SelectItem>
<SelectItem value="content">Content</SelectItem>
<SelectItem value="status">Status</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() =>
setSortDirection((prev) =>
prev === "asc" ? "desc" : "asc"
)
}
className="px-3"
>
{sortDirection === "asc" ? "↑" : "↓"}
</Button>
</div>
{/* Clear Filters */}
{hasActiveFilters && (
<Button
variant="outline"
size="sm"
onClick={clearFilters}
className="gap-2"
>
<X className="h-4 w-4" />
Clear
</Button>
)}
{/* Results Count */}
<div className="ml-auto">
<Badge variant="outline" className="text-xs">
{filteredAndSortedTraces.length} of {traces.length} traces
</Badge>
</div>
</div>
</div>
{/* Simple Bulk Operations */}
{selectedTraces.length > 0 && (
<div className="bg-muted/50 border rounded-lg p-3 flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{selectedTraces.length} trace
{selectedTraces.length !== 1 ? "s" : ""} selected
</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setSelectedTraces([])}
>
Clear
</Button>
<Button
variant="destructive"
size="sm"
onClick={handleBulkDelete}
>
Delete Selected
</Button>
</div>
</div>
)}
{/* Content Area */}
{isLoading ? (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="space-y-2 p-4 border rounded-lg">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-3 w-1/4" />
</div>
))}
</div>
) : traces.length === 0 ? (
<div className="py-12">
<EmptyState
icon={FileText}
title="No traces yet"
description="Upload your first trace to start analyzing AI agent behavior patterns and generate insights."
action={{
label: "Upload Trace",
onClick: handleUploadTrace,
}}
>
<div className="mt-6 text-center">
<p className="text-sm text-muted-foreground">
Supported formats: JSON, TXT, LOG
</p>
</div>
</EmptyState>
</div>
) : (
<div className="border rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b bg-muted/50">
<th className="text-left p-4 font-medium w-12">
<input
type="checkbox"
checked={isAllSelected}
ref={(checkbox) => {
if (checkbox)
checkbox.indeterminate = isPartiallySelected;
}}
onChange={(e) => handleSelectAll(e.target.checked)}
className="rounded"
/>
</th>
<th className="text-left p-4 font-medium">
<div className="flex items-center gap-1">
Name
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Name and description of the uploaded trace file
</p>
</TooltipContent>
</Tooltip>
</div>
</th>
<th className="text-left p-4 font-medium">
<div className="flex items-center gap-1">
Type
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Source or format of the trace data</p>
</TooltipContent>
</Tooltip>
</div>
</th>
<th className="text-left p-4 font-medium">
<div className="flex items-center gap-1">
Date
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>When the trace was uploaded to the system</p>
</TooltipContent>
</Tooltip>
</div>
</th>
<th className="text-left p-4 font-medium">
<div className="flex items-center gap-1">
Content
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Character count and content size information
</p>
</TooltipContent>
</Tooltip>
</div>
</th>
<th className="text-left p-4 font-medium">
<div className="flex items-center gap-1">
Graphs
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Number of generated agent graphs for this trace
</p>
</TooltipContent>
</Tooltip>
</div>
</th>
<th className="text-right p-4 font-medium">Actions</th>
</tr>
</thead>
<tbody>
{filteredAndSortedTraces.map((trace) => (
<tr
key={trace.id}
className="border-b hover:bg-primary/5 active:bg-primary/10 cursor-pointer transition-all duration-200 hover:shadow-sm group"
onClick={() => {
actions.setSelectedTrace(trace);
actions.setActiveView("trace-kg");
}}
onMouseDown={(e) => {
// Add pressed effect
e.currentTarget.style.transform = "scale(0.995)";
e.currentTarget.style.boxShadow =
"inset 0 2px 4px rgba(0,0,0,0.1)";
}}
onMouseUp={(e) => {
// Remove pressed effect
e.currentTarget.style.transform = "scale(1)";
e.currentTarget.style.boxShadow = "none";
}}
onMouseLeave={(e) => {
// Reset on mouse leave
e.currentTarget.style.transform = "scale(1)";
e.currentTarget.style.boxShadow = "none";
}}
>
<td
className="p-4 w-12"
onClick={(e) => e.stopPropagation()}
>
<input
type="checkbox"
checked={selectedTraces.includes(
trace.id.toString()
)}
onChange={(e) =>
handleSelectTrace(
trace.id.toString(),
e.target.checked
)
}
className="rounded"
/>
</td>
<td className="p-4">
<div className="flex items-center gap-3">
<div className="min-w-0 flex-1">
<div
className="font-medium text-sm truncate group-hover:text-primary transition-colors"
title={trace.filename}
>
{trace.filename}
</div>
{trace.description && (
<div className="text-xs text-muted-foreground truncate mt-1 max-w-md">
{trace.description}
</div>
)}
</div>
</div>
</td>
<td className="p-4">
<div className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 group-hover:bg-blue-200 transition-colors">
{formatTraceType(trace.trace_type)}
</div>
</td>
<td className="p-4">
<div className="text-sm font-medium group-hover:text-primary transition-colors">
{formatDate(
trace.upload_timestamp || trace.timestamp
)}
</div>
<div className="text-xs text-muted-foreground">
{trace.upload_timestamp || trace.timestamp
? new Date(
trace.upload_timestamp || trace.timestamp!
).toLocaleTimeString()
: "No time available"}
</div>
</td>
<td className="p-4">
<div className="text-sm font-medium group-hover:text-primary transition-colors">
{trace.character_count
? `${(trace.character_count / 1000).toFixed(0)}K`
: "N/A"}
</div>
<div className="text-xs text-muted-foreground">
{trace.character_count
? `${trace.character_count.toLocaleString()} chars`
: "No content"}
</div>
</td>
<td className="p-4">
<div className="flex items-center gap-1">
{trace.knowledge_graphs &&
trace.knowledge_graphs.length > 0 ? (
<>
<GitBranch className="h-3 w-3 text-muted-foreground group-hover:text-primary transition-colors" />
<span className="text-sm font-medium group-hover:text-primary transition-colors">
{
trace.knowledge_graphs.filter(
(kg) =>
kg.is_final === true ||
(kg.window_index === null &&
kg.window_total !== null)
).length
}
</span>
</>
) : (
<span className="text-sm text-muted-foreground">
None
</span>
)}
</div>
</td>
<td className="p-4">
<div className="flex items-center justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={(e) => handleViewTrace(trace, e)}
className="text-primary hover:text-primary/90"
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => handleViewTraceDetails(trace, e)}
className="text-info hover:text-info/90"
>
<Info className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) =>
handleDeleteSingleTrace(trace.id.toString(), e)
}
className="text-red-500 hover:text-red-600"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</TooltipProvider>
);
}