import "./AgentReflectionsTab.css"; import { useCallback, useEffect, useState } from "react"; import { BarChart3, ChevronDown, ChevronRight, Lightbulb, Loader2, RefreshCw, Star, Trash2, TrendingDown, TrendingUp, Zap, } from "lucide-react"; import type { AgentPerformanceSummary, AgentReflection, } from "../api"; import type { AgentRating, AgentRatingSummary } from "@fusion/core"; import { addAgentRating, deleteAgentRating, fetchAgentPerformance, fetchAgentRatings, fetchAgentRatingSummary, fetchAgentReflections, triggerAgentReflection, } from "../api"; interface AgentReflectionsTabProps { agentId: string; projectId?: string; addToast: (msg: string, type?: "success" | "error") => void; } /** Format a number in milliseconds to a human-readable duration string */ function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`; return `${(ms / 3_600_000).toFixed(1)}h`; } /** Format a percentage value (0-1) to a percentage string */ function formatPercent(rate: number): string { return `${Math.round(rate * 100)}%`; } /** Format an ISO timestamp to a relative time string */ function relativeTime(iso: string): string { const now = Date.now(); const then = new Date(iso).getTime(); const diffMs = now - then; if (diffMs < 0) { const absDiff = Math.abs(diffMs); if (absDiff < 60_000) return "in a moment"; if (absDiff < 3_600_000) return `in ${Math.floor(absDiff / 60_000)}m`; if (absDiff < 86_400_000) return `in ${Math.floor(absDiff / 3_600_000)}h`; return `in ${Math.floor(absDiff / 86_400_000)}d`; } if (diffMs < 60_000) return "just now"; if (diffMs < 3_600_000) return `${Math.floor(diffMs / 60_000)}m ago`; if (diffMs < 86_400_000) return `${Math.floor(diffMs / 3_600_000)}h ago`; return `${Math.floor(diffMs / 86_400_000)}d ago`; } /** Get display label for a trigger type */ function getTriggerLabel(trigger: string): string { switch (trigger) { case "periodic": return "Periodic"; case "post-task": return "Post-Task"; case "manual": return "Manual"; case "user-requested": return "User Requested"; default: return trigger; } } function getErrorMessage(err: unknown): string { if (err instanceof Error && err.message) { return err.message; } return String(err); } function getTrendLabel(trend: string): string { switch (trend) { case "improving": return "↑ Improving"; case "declining": return "↓ Declining"; case "stable": return "→ Stable"; default: return "Insufficient data"; } } function getTrendClass(trend: string): string { switch (trend) { case "improving": return "trend-improving"; case "declining": return "trend-declining"; case "stable": return "trend-stable"; default: return "trend-insufficient"; } } function renderStars(score: number, maxScore: number = 5) { return ( {Array.from({ length: maxScore }, (_, i) => ( ))} ); } export function AgentReflectionsTab({ agentId, projectId, addToast }: AgentReflectionsTabProps) { const [reflections, setReflections] = useState([]); const [performance, setPerformance] = useState(null); const [ratingSummary, setRatingSummary] = useState(null); const [ratings, setRatings] = useState([]); const [isLoadingReflections, setIsLoadingReflections] = useState(true); const [isLoadingRatings, setIsLoadingRatings] = useState(true); const [isReflecting, setIsReflecting] = useState(false); const [isSubmittingRating, setIsSubmittingRating] = useState(false); const [expandedReflectionId, setExpandedReflectionId] = useState(null); const [newScore, setNewScore] = useState(0); const [newCategory, setNewCategory] = useState(""); const [newComment, setNewComment] = useState(""); const loadReflectionData = useCallback(async () => { try { const [reflectionsData, performanceData] = await Promise.all([ fetchAgentReflections(agentId, 20, projectId), fetchAgentPerformance(agentId, undefined, projectId), ]); setReflections(reflectionsData); setPerformance(performanceData); } catch (err) { addToast(`Failed to load reflections: ${getErrorMessage(err)}`, "error"); } finally { setIsLoadingReflections(false); } }, [agentId, projectId, addToast]); const loadRatingsData = useCallback(async () => { try { const [summaryData, ratingsData] = await Promise.all([ fetchAgentRatingSummary(agentId, projectId), fetchAgentRatings(agentId, { limit: 50 }, projectId), ]); setRatingSummary(summaryData); setRatings(ratingsData); } catch (err) { addToast(`Failed to load ratings: ${getErrorMessage(err)}`, "error"); } finally { setIsLoadingRatings(false); } }, [agentId, projectId, addToast]); useEffect(() => { void loadReflectionData(); void loadRatingsData(); }, [loadReflectionData, loadRatingsData]); const handleReflectNow = async () => { setIsReflecting(true); try { const reflection = await triggerAgentReflection(agentId, projectId); if (!reflection) { addToast("Not enough history to generate a reflection yet", "error"); return; } addToast("Reflection generated successfully", "success"); setIsLoadingReflections(true); await loadReflectionData(); } catch (err: unknown) { const message = getErrorMessage(err); const normalizedMessage = message.toLowerCase(); if (normalizedMessage.includes("agent not found") || normalizedMessage.includes("not found")) { addToast("This agent is no longer available. It may have been deleted.", "error"); } else if (normalizedMessage.includes("insufficient history")) { addToast("Not enough history to generate a reflection yet", "error"); } else { addToast(`Failed to generate reflection: ${message}`, "error"); } } finally { setIsReflecting(false); } }; const handleSubmitRating = async (e: React.FormEvent) => { e.preventDefault(); if (newScore === 0) return; setIsSubmittingRating(true); try { await addAgentRating(agentId, { score: newScore, category: newCategory || undefined, comment: newComment || undefined, raterType: "user", }, projectId); setNewScore(0); setNewCategory(""); setNewComment(""); addToast("Rating added", "success"); await loadRatingsData(); } catch (err) { addToast(`Failed to add rating: ${getErrorMessage(err)}`, "error"); } finally { setIsSubmittingRating(false); } }; const handleDeleteRating = async (ratingId: string) => { try { await deleteAgentRating(agentId, ratingId, projectId); addToast("Rating deleted", "success"); await loadRatingsData(); } catch (err) { addToast(`Failed to delete rating: ${getErrorMessage(err)}`, "error"); } }; const toggleExpanded = (id: string) => { setExpandedReflectionId((prev) => (prev === id ? null : id)); }; if (isLoadingReflections && isLoadingRatings) { return (
Loading evaluation...
); } const hasNoPerformanceData = performance && performance.totalTasksCompleted === 0 && performance.totalTasksFailed === 0 && performance.recentReflectionCount === 0; return (

Performance, Reflections & Ratings

{performance && !hasNoPerformanceData && (
{performance.totalTasksCompleted}
Tasks Completed
{performance.totalTasksFailed}
Tasks Failed
{formatDuration(performance.avgDurationMs)}
Avg Duration
= 0.8 ? "var(--color-success)" : performance.successRate >= 0.5 ? "var(--color-warning)" : "var(--color-error)", }} /> {formatPercent(performance.successRate)}
Success Rate
{performance.recentReflectionCount}
Reflections
)} {hasNoPerformanceData && (

No performance data yet

)}

User Ratings

{isLoadingRatings ? (
Loading ratings...
) : ( <> {ratingSummary && (
{ratingSummary.averageScore.toFixed(1)} {renderStars(Math.round(ratingSummary.averageScore))}
{ratingSummary.totalRatings} ratings {getTrendLabel(ratingSummary.trend)}
)} {ratingSummary && Object.keys(ratingSummary.categoryAverages).length > 0 && (

Category Averages

{Object.entries(ratingSummary.categoryAverages as Record).map(([category, avg]) => (
{category} {avg.toFixed(1)}
))}
)}

Add Rating

{[1, 2, 3, 4, 5].map((score) => ( ))}