feat(FN-1183): add agent reflections tab to dashboard
- Add API routes for agent reflections (GET, POST, DELETE) - Create AgentReflectionsTab component with reflection list and form - Integrate reflections tab into AgentDetailView - Add CSS styles for the reflections tab UI - Write tests for API routes and component
This commit is contained in:
@@ -3,13 +3,14 @@ import {
|
||||
Bot, Heart, Activity, Pause, Play, Square, Trash2, RefreshCw,
|
||||
Settings, FileText, ActivitySquare, X, Copy,
|
||||
ExternalLink, CheckCircle, XCircle, Loader2, GitBranch, ListChecks,
|
||||
ChevronDown, ChevronRight
|
||||
ChevronDown, ChevronRight, BarChart3
|
||||
} from "lucide-react";
|
||||
import type { AgentDetail, AgentState, AgentHeartbeatRun } from "../api";
|
||||
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogs, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentTasks, fetchChainOfCommand } from "../api";
|
||||
import type { Agent } from "../api";
|
||||
import type { AgentLogEntry, Task } from "@fusion/core";
|
||||
import { AgentLogViewer } from "./AgentLogViewer";
|
||||
import { AgentReflectionsTab } from "./AgentReflectionsTab";
|
||||
|
||||
/**
|
||||
* Simple className utility - joins class names conditionally
|
||||
@@ -50,7 +51,7 @@ interface AgentDetailViewProps {
|
||||
onChildClick?: (childId: string) => void;
|
||||
}
|
||||
|
||||
type TabId = "dashboard" | "logs" | "config" | "runs" | "tasks" | "employees" | "soul" | "memory";
|
||||
type TabId = "dashboard" | "logs" | "config" | "runs" | "tasks" | "employees" | "soul" | "memory" | "reflections";
|
||||
|
||||
const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [
|
||||
{ id: "dashboard", label: "Dashboard", icon: ActivitySquare },
|
||||
@@ -60,6 +61,7 @@ const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [
|
||||
{ id: "employees", label: "Employees", icon: GitBranch },
|
||||
{ id: "soul", label: "Soul", icon: Heart },
|
||||
{ id: "memory", label: "Memory", icon: FileText },
|
||||
{ id: "reflections", label: "Reflections", icon: BarChart3 },
|
||||
{ id: "config", label: "Settings", icon: Settings },
|
||||
];
|
||||
|
||||
@@ -441,6 +443,14 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "reflections" && (
|
||||
<AgentReflectionsTab
|
||||
agentId={agent.id}
|
||||
projectId={projectId}
|
||||
addToast={addToast}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "config" && (
|
||||
<ConfigTab
|
||||
agent={agent}
|
||||
|
||||
308
packages/dashboard/app/components/AgentReflectionsTab.test.tsx
Normal file
308
packages/dashboard/app/components/AgentReflectionsTab.test.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { AgentReflectionsTab } from "./AgentReflectionsTab";
|
||||
import {
|
||||
fetchAgentReflections,
|
||||
fetchAgentPerformance,
|
||||
triggerAgentReflection,
|
||||
} from "../api";
|
||||
|
||||
vi.mock("../api", () => ({
|
||||
fetchAgentReflections: vi.fn(),
|
||||
fetchAgentPerformance: vi.fn(),
|
||||
triggerAgentReflection: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockedFetchAgentReflections = vi.mocked(fetchAgentReflections);
|
||||
const mockedFetchAgentPerformance = vi.mocked(fetchAgentPerformance);
|
||||
const mockedTriggerAgentReflection = vi.mocked(triggerAgentReflection);
|
||||
|
||||
describe("AgentReflectionsTab", () => {
|
||||
const mockReflections = [
|
||||
{
|
||||
id: "ref-001",
|
||||
agentId: "agent-001",
|
||||
timestamp: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago
|
||||
trigger: "periodic",
|
||||
metrics: {
|
||||
tasksCompleted: 5,
|
||||
tasksFailed: 1,
|
||||
avgDurationMs: 120000,
|
||||
},
|
||||
insights: ["Insight 1", "Insight 2"],
|
||||
suggestedImprovements: ["Improve X", "Fix Y"],
|
||||
summary: "Test summary for the reflection",
|
||||
},
|
||||
{
|
||||
id: "ref-002",
|
||||
agentId: "agent-001",
|
||||
timestamp: new Date(Date.now() - 86400000).toISOString(), // 1 day ago
|
||||
trigger: "manual",
|
||||
metrics: {
|
||||
tasksCompleted: 3,
|
||||
tasksFailed: 0,
|
||||
avgDurationMs: 90000,
|
||||
},
|
||||
insights: ["Another insight"],
|
||||
suggestedImprovements: ["Another suggestion"],
|
||||
summary: "Another summary",
|
||||
},
|
||||
];
|
||||
|
||||
const mockPerformance = {
|
||||
agentId: "agent-001",
|
||||
totalTasksCompleted: 10,
|
||||
totalTasksFailed: 2,
|
||||
avgDurationMs: 110000,
|
||||
successRate: 0.833,
|
||||
commonErrors: ["Error 1"],
|
||||
strengths: ["Strong point"],
|
||||
weaknesses: ["Weak point"],
|
||||
recentReflectionCount: 3,
|
||||
computedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const addToast = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockedFetchAgentReflections.mockReset();
|
||||
mockedFetchAgentPerformance.mockReset();
|
||||
mockedTriggerAgentReflection.mockReset();
|
||||
mockedFetchAgentReflections.mockResolvedValue(mockReflections);
|
||||
mockedFetchAgentPerformance.mockResolvedValue(mockPerformance);
|
||||
addToast.mockReset();
|
||||
});
|
||||
|
||||
it("renders loading state initially", () => {
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("Loading reflections...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders performance summary cards with data", async () => {
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Tasks Completed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("10")).toBeInTheDocument(); // totalTasksCompleted
|
||||
expect(screen.getByText("2")).toBeInTheDocument(); // totalTasksFailed
|
||||
expect(screen.getByText("83%")).toBeInTheDocument(); // successRate
|
||||
expect(screen.getByText("3")).toBeInTheDocument(); // recentReflectionCount
|
||||
});
|
||||
|
||||
it("renders reflections list with correct timestamps and trigger badges", async () => {
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Reflection History")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check trigger badges
|
||||
expect(screen.getByText("Periodic")).toBeInTheDocument();
|
||||
expect(screen.getByText("Manual")).toBeInTheDocument();
|
||||
|
||||
// Check summaries are shown
|
||||
expect(screen.getByText("Test summary for the reflection")).toBeInTheDocument();
|
||||
expect(screen.getByText("Another summary")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows empty state when no reflections exist", async () => {
|
||||
mockedFetchAgentReflections.mockResolvedValue([]);
|
||||
mockedFetchAgentPerformance.mockResolvedValue({
|
||||
...mockPerformance,
|
||||
totalTasksCompleted: 0,
|
||||
totalTasksFailed: 0,
|
||||
recentReflectionCount: 0,
|
||||
});
|
||||
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No reflections yet")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Trigger a reflection to get started")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'no performance data' when summary has zeros", async () => {
|
||||
mockedFetchAgentPerformance.mockResolvedValue({
|
||||
...mockPerformance,
|
||||
totalTasksCompleted: 0,
|
||||
totalTasksFailed: 0,
|
||||
recentReflectionCount: 0,
|
||||
});
|
||||
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No performance data yet")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("clicking a reflection card expands it to show insights and suggestions", async () => {
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test summary for the reflection")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on the first reflection card
|
||||
const firstCard = screen.getByText("Test summary for the reflection").closest(".reflection-card");
|
||||
expect(firstCard).toBeInTheDocument();
|
||||
fireEvent.click(firstCard!);
|
||||
|
||||
// Check expanded content
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Insights")).toBeInTheDocument();
|
||||
expect(screen.getByText("Suggested Improvements")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Insight 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Improve X")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clicking Reflect Now calls triggerAgentReflection and refreshes data", async () => {
|
||||
mockedTriggerAgentReflection.mockResolvedValue({
|
||||
...mockReflections[0],
|
||||
id: "ref-003",
|
||||
});
|
||||
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Reflect Now")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Reflect Now"));
|
||||
|
||||
// Should call triggerAgentReflection
|
||||
await waitFor(() => {
|
||||
expect(mockedTriggerAgentReflection).toHaveBeenCalledWith("agent-001", "test-project");
|
||||
});
|
||||
|
||||
// Should show success toast
|
||||
await waitFor(() => {
|
||||
expect(addToast).toHaveBeenCalledWith("Reflection generated successfully", "success");
|
||||
});
|
||||
|
||||
// Should refresh data
|
||||
expect(mockedFetchAgentReflections).toHaveBeenCalledTimes(2); // Initial + refresh
|
||||
expect(mockedFetchAgentPerformance).toHaveBeenCalledTimes(2); // Initial + refresh
|
||||
});
|
||||
|
||||
it("shows error toast when Reflect Now fails", async () => {
|
||||
mockedTriggerAgentReflection.mockRejectedValue(new Error("Service unavailable"));
|
||||
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Reflect Now")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Reflect Now"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addToast).toHaveBeenCalledWith(expect.stringContaining("Service unavailable"), "error");
|
||||
});
|
||||
});
|
||||
|
||||
it("disables Reflect Now button while reflecting is in progress", async () => {
|
||||
mockedTriggerAgentReflection.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve(mockReflections[0]), 1000))
|
||||
);
|
||||
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Reflect Now")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Reflect Now"));
|
||||
|
||||
// Should show "Reflecting..." text
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Reflecting...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const button = screen.getByText("Reflecting...").closest("button");
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows error toast when loading data fails", async () => {
|
||||
mockedFetchAgentReflections.mockRejectedValue(new Error("Network error"));
|
||||
mockedFetchAgentPerformance.mockResolvedValue(mockPerformance);
|
||||
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addToast).toHaveBeenCalledWith(expect.stringContaining("Failed to load reflections"), "error");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays metrics when expanded", async () => {
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test summary for the reflection")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const firstCard = screen.getByText("Test summary for the reflection").closest(".reflection-card");
|
||||
fireEvent.click(firstCard!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Metrics")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText(/Tasks:/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Failed:/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("collapses reflection when clicking again", async () => {
|
||||
render(
|
||||
<AgentReflectionsTab agentId="agent-001" projectId="test-project" addToast={addToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test summary for the reflection")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const firstCard = screen.getByText("Test summary for the reflection").closest(".reflection-card");
|
||||
fireEvent.click(firstCard!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Insights")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click again to collapse
|
||||
fireEvent.click(firstCard!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Insights")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
342
packages/dashboard/app/components/AgentReflectionsTab.tsx
Normal file
342
packages/dashboard/app/components/AgentReflectionsTab.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
BarChart3,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Lightbulb,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import type { AgentPerformanceSummary, AgentReflection } from "../api";
|
||||
import {
|
||||
fetchAgentPerformance,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export function AgentReflectionsTab({ agentId, projectId, addToast }: AgentReflectionsTabProps) {
|
||||
const [reflections, setReflections] = useState<AgentReflection[]>([]);
|
||||
const [performance, setPerformance] = useState<AgentPerformanceSummary | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isReflecting, setIsReflecting] = useState(false);
|
||||
const [expandedReflectionId, setExpandedReflectionId] = useState<string | null>(null);
|
||||
|
||||
// Load data on mount
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const [reflectionsData, performanceData] = await Promise.all([
|
||||
fetchAgentReflections(agentId, 20, projectId),
|
||||
fetchAgentPerformance(agentId, undefined, projectId),
|
||||
]);
|
||||
setReflections(reflectionsData);
|
||||
setPerformance(performanceData);
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to load reflections: ${err.message}`, "error");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [agentId, projectId, addToast]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
}, [loadData]);
|
||||
|
||||
// Handle reflect now button
|
||||
const handleReflectNow = async () => {
|
||||
setIsReflecting(true);
|
||||
try {
|
||||
await triggerAgentReflection(agentId, projectId);
|
||||
addToast("Reflection generated successfully", "success");
|
||||
setIsLoading(true);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to generate reflection: ${err.message}`, "error");
|
||||
} finally {
|
||||
setIsReflecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle expanded state
|
||||
const toggleExpanded = (id: string) => {
|
||||
setExpandedReflectionId((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="reflections-tab">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
padding: "24px",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
<span className="text-muted">Loading reflections...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if performance summary has no data
|
||||
const hasNoPerformanceData =
|
||||
performance &&
|
||||
performance.totalTasksCompleted === 0 &&
|
||||
performance.totalTasksFailed === 0 &&
|
||||
performance.recentReflectionCount === 0;
|
||||
|
||||
return (
|
||||
<div className="reflections-tab">
|
||||
{/* Header */}
|
||||
<div className="reflections-header">
|
||||
<h3>
|
||||
<BarChart3 size={16} />
|
||||
Performance & Reflections
|
||||
</h3>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleReflectNow}
|
||||
disabled={isReflecting}
|
||||
title="Generate a manual reflection"
|
||||
>
|
||||
{isReflecting ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
Reflecting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw size={14} />
|
||||
Reflect Now
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Performance Summary Grid */}
|
||||
{performance && !hasNoPerformanceData && (
|
||||
<div className="reflections-stats-grid">
|
||||
<div className="reflections-stat-card">
|
||||
<div className="stat-value">
|
||||
<TrendingUp size={16} style={{ color: "var(--color-success)" }} />
|
||||
{performance.totalTasksCompleted}
|
||||
</div>
|
||||
<div className="stat-label">Tasks Completed</div>
|
||||
</div>
|
||||
|
||||
<div className="reflections-stat-card">
|
||||
<div className="stat-value">
|
||||
<TrendingDown size={16} style={{ color: "var(--color-error)" }} />
|
||||
{performance.totalTasksFailed}
|
||||
</div>
|
||||
<div className="stat-label">Tasks Failed</div>
|
||||
</div>
|
||||
|
||||
<div className="reflections-stat-card">
|
||||
<div className="stat-value">
|
||||
<Zap size={16} style={{ color: "var(--in-progress)" }} />
|
||||
{formatDuration(performance.avgDurationMs)}
|
||||
</div>
|
||||
<div className="stat-label">Avg Duration</div>
|
||||
</div>
|
||||
|
||||
<div className="reflections-stat-card">
|
||||
<div className="stat-value">
|
||||
<BarChart3
|
||||
size={16}
|
||||
style={{
|
||||
color:
|
||||
performance.successRate >= 0.8
|
||||
? "var(--color-success)"
|
||||
: performance.successRate >= 0.5
|
||||
? "var(--color-warning)"
|
||||
: "var(--color-error)",
|
||||
}}
|
||||
/>
|
||||
{formatPercent(performance.successRate)}
|
||||
</div>
|
||||
<div className="stat-label">Success Rate</div>
|
||||
</div>
|
||||
|
||||
<div className="reflections-stat-card">
|
||||
<div className="stat-value">
|
||||
<Lightbulb size={16} style={{ color: "var(--color-info)" }} />
|
||||
{performance.recentReflectionCount}
|
||||
</div>
|
||||
<div className="stat-label">Reflections</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasNoPerformanceData && (
|
||||
<div className="reflections-no-data">
|
||||
<BarChart3 size={24} opacity={0.3} />
|
||||
<p>No performance data yet</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reflections List */}
|
||||
<div className="reflections-list">
|
||||
<h4>Reflection History</h4>
|
||||
|
||||
{reflections.length === 0 ? (
|
||||
<div className="reflection-empty">
|
||||
<Lightbulb size={32} opacity={0.3} />
|
||||
<p>No reflections yet</p>
|
||||
<p className="text-secondary">Trigger a reflection to get started</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="reflection-cards">
|
||||
{reflections.map((reflection) => {
|
||||
const isExpanded = expandedReflectionId === reflection.id;
|
||||
return (
|
||||
<div
|
||||
key={reflection.id}
|
||||
className={`reflection-card ${isExpanded ? "reflection-card--expanded" : ""}`}
|
||||
onClick={() => toggleExpanded(reflection.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === "Enter" && toggleExpanded(reflection.id)}
|
||||
>
|
||||
<div className="reflection-card-header">
|
||||
<span className={`reflection-trigger-badge reflection-trigger-${reflection.trigger}`}>
|
||||
{getTriggerLabel(reflection.trigger)}
|
||||
</span>
|
||||
<span className="reflection-timestamp">{relativeTime(reflection.timestamp)}</span>
|
||||
<span className="reflection-chevron">
|
||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="reflection-summary">{reflection.summary}</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="reflection-details">
|
||||
{reflection.insights.length > 0 && (
|
||||
<div className="reflection-insights">
|
||||
<h5>
|
||||
<Lightbulb size={14} /> Insights
|
||||
</h5>
|
||||
<ul>
|
||||
{reflection.insights.map((insight, i) => (
|
||||
<li key={i}>{insight}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{reflection.suggestedImprovements.length > 0 && (
|
||||
<div className="reflection-suggestions">
|
||||
<h5>
|
||||
<TrendingUp size={14} /> Suggested Improvements
|
||||
</h5>
|
||||
<ul>
|
||||
{reflection.suggestedImprovements.map((suggestion, i) => (
|
||||
<li key={i}>{suggestion}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{reflection.metrics && (
|
||||
<div className="reflection-metrics">
|
||||
<h5>Metrics</h5>
|
||||
<div className="metrics-grid">
|
||||
{reflection.metrics.tasksCompleted !== undefined && (
|
||||
<div className="metric">
|
||||
<span className="metric-label">Tasks:</span>
|
||||
<span className="metric-value">{reflection.metrics.tasksCompleted}</span>
|
||||
</div>
|
||||
)}
|
||||
{reflection.metrics.tasksFailed !== undefined && (
|
||||
<div className="metric">
|
||||
<span className="metric-label">Failed:</span>
|
||||
<span className="metric-value">{reflection.metrics.tasksFailed}</span>
|
||||
</div>
|
||||
)}
|
||||
{reflection.metrics.avgDurationMs !== undefined && (
|
||||
<div className="metric">
|
||||
<span className="metric-label">Avg Duration:</span>
|
||||
<span className="metric-value">{formatDuration(reflection.metrics.avgDurationMs)}</span>
|
||||
</div>
|
||||
)}
|
||||
{reflection.metrics.errorCount !== undefined && (
|
||||
<div className="metric">
|
||||
<span className="metric-label">Errors:</span>
|
||||
<span className="metric-value">{reflection.metrics.errorCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user