By Sagar Shankaran, Founder of CallSphere
Design and build an admin dashboard for AI agents with metric cards, real-time charts, configuration panels, and activity logs using React, TypeScript, and TanStack Query.
Key takeaways
An agent admin dashboard serves two audiences: operations teams monitoring agent health and behavior, and product teams configuring agent behavior. The dashboard must display key metrics at a glance, show conversation activity in real-time, surface errors and escalations, and provide configuration controls for agent parameters.
Use a grid-based layout with a sidebar for navigation and a main content area divided into metric cards at the top and detailed panels below.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
function AgentDashboard() {
return (
<div className="flex h-screen bg-gray-50">
<Sidebar />
<main className="flex-1 overflow-y-auto p-6">
<h1 className="text-2xl font-bold mb-6">Agent Overview</h1>
<MetricCardsRow />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
<ConversationChart />
<RecentActivity />
</div>
<AgentConfigPanel />
</main>
</div>
);
}
Metric cards show the most important numbers: total conversations, average response time, error rate, and user satisfaction score. Fetch these with TanStack Query for automatic background refetching.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
import { useQuery } from "@tanstack/react-query";
interface AgentMetrics {
totalConversations: number;
avgResponseTimeMs: number;
errorRate: number;
satisfactionScore: number;
}
function MetricCardsRow() {
const { data, isLoading } = useQuery<AgentMetrics>({
queryKey: ["agent-metrics"],
queryFn: () =>
fetch("/api/admin/metrics").then((r) => r.json()),
refetchInterval: 30_000, // Refresh every 30 seconds
});
if (isLoading) return <MetricCardsSkeleton />;
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard
label="Conversations"
value={data!.totalConversations.toLocaleString()}
trend="+12%"
trendUp={true}
/>
<MetricCard
label="Avg Response"
value={`${(data!.avgResponseTimeMs / 1000).toFixed(1)}s`}
trend="-8%"
trendUp={true}
/>
<MetricCard
label="Error Rate"
value={`${(data!.errorRate * 100).toFixed(2)}%`}
trend="+0.3%"
trendUp={false}
/>
<MetricCard
label="Satisfaction"
value={`${data!.satisfactionScore.toFixed(1)}/5`}
trend="+0.2"
trendUp={true}
/>
</div>
);
}
Each card displays a label, value, and trend indicator. The trend arrow and color change based on whether the direction is positive or negative.
interface MetricCardProps {
label: string;
value: string;
trend: string;
trendUp: boolean;
}
function MetricCard({ label, value, trend, trendUp }: MetricCardProps) {
return (
<div className="bg-white rounded-xl border p-5">
<p className="text-sm text-gray-500 mb-1">{label}</p>
<p className="text-2xl font-bold text-gray-900">{value}</p>
<p
className={`text-sm mt-2 ${
trendUp ? "text-green-600" : "text-red-600"
}`}
>
{trendUp ? "^" : "v"} {trend} vs last week
</p>
</div>
);
}
Dashboard components should show skeleton placeholders during data loading instead of blank space. This prevents layout shift and communicates that data is on the way.
function MetricCardsSkeleton() {
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="bg-white rounded-xl border p-5">
<div className="h-4 w-20 bg-gray-200 rounded animate-pulse mb-2" />
<div className="h-8 w-24 bg-gray-200 rounded animate-pulse mb-2" />
<div className="h-3 w-28 bg-gray-200 rounded animate-pulse" />
</div>
))}
</div>
);
}
An activity log shows recent conversations, errors, and escalations in chronological order. Use a polling query to keep it up-to-date.
interface ActivityItem {
id: string;
type: "conversation" | "error" | "escalation";
summary: string;
timestamp: string;
}
function RecentActivity() {
const { data } = useQuery<ActivityItem[]>({
queryKey: ["agent-activity"],
queryFn: () =>
fetch("/api/admin/activity?limit=20").then((r) => r.json()),
refetchInterval: 10_000,
});
const typeStyles: Record<ActivityItem["type"], string> = {
conversation: "bg-blue-100 text-blue-700",
error: "bg-red-100 text-red-700",
escalation: "bg-yellow-100 text-yellow-700",
};
return (
<div className="bg-white rounded-xl border p-5">
<h2 className="font-semibold text-lg mb-4">Recent Activity</h2>
<div className="space-y-3 max-h-80 overflow-y-auto">
{data?.map((item) => (
<div key={item.id} className="flex items-start gap-3">
<span
className={`text-xs px-2 py-0.5 rounded-full
font-medium ${typeStyles[item.type]}`}
>
{item.type}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-700 truncate">
{item.summary}
</p>
<p className="text-xs text-gray-400">{item.timestamp}</p>
</div>
</div>
))}
</div>
</div>
);
}
Allow admins to tweak agent parameters like system prompt, temperature, max tokens, and enabled tools without deploying code.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
import { useMutation, useQueryClient } from "@tanstack/react-query";
interface AgentConfig {
systemPrompt: string;
temperature: number;
maxTokens: number;
enabledTools: string[];
}
function AgentConfigPanel() {
const queryClient = useQueryClient();
const { data: config } = useQuery<AgentConfig>({
queryKey: ["agent-config"],
queryFn: () =>
fetch("/api/admin/config").then((r) => r.json()),
});
const mutation = useMutation({
mutationFn: (updated: AgentConfig) =>
fetch("/api/admin/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updated),
}),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["agent-config"] }),
});
if (!config) return null;
return (
<div className="bg-white rounded-xl border p-6 mt-6">
<h2 className="font-semibold text-lg mb-4">
Agent Configuration
</h2>
<label className="block text-sm font-medium mb-1">
System Prompt
</label>
<textarea
defaultValue={config.systemPrompt}
rows={4}
className="w-full border rounded-lg p-3 text-sm mb-4"
/>
<button
onClick={() => mutation.mutate(config)}
className="bg-blue-600 text-white px-4 py-2 rounded-lg
text-sm disabled:opacity-50"
disabled={mutation.isPending}
>
{mutation.isPending ? "Saving..." : "Save Changes"}
</button>
</div>
);
}
Use a charting library like Recharts or Chart.js with a React wrapper. Fetch time-series data from your API (grouped by hour or day) and pass it to a line or bar chart component. Recharts integrates naturally with React because its charts are composed from React components like <LineChart>, <Line>, and <XAxis>.
Polling with TanStack Query's refetchInterval is simpler and works well for dashboards where 10-30 second latency is acceptable. Use WebSockets only if you need sub-second updates, such as live conversation transcripts or real-time error alerts that require immediate operator attention.
Wrap your dashboard routes in an authentication guard that checks the user's role. In Next.js, use middleware to redirect non-admin users before the page loads. On the API side, every admin endpoint should verify the JWT token contains an admin role claim and return 403 if it does not.
#AdminDashboard #React #Monitoring #TypeScript #AIAgentManagement #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
How to actually observe a WebSocket fleet: ping/pong heartbeats, Prometheus metrics that matter, dead-man switches, and the alerts that fire before customers notice.
AI SDK 5 ships fully typed chat for React, Svelte, Vue, and Angular plus first-class agent loop primitives. Here are the patterns that matter for shipping in 2026.
Mastra.ai is becoming the go-to TypeScript agent framework in 2026. Workflows, RAG, evals, and an honest comparison with Vercel AI SDK 5 for serious teams.
How to wire Vercel AI SDK 5 tool calls to a React UI with streaming, partial UI updates, and proper error handling that survives flaky network conditions.
Convex's reactive queries auto-push every transcript update to every subscribed client. Real working code for actions, mutations, and OpenAI Realtime streaming.
The MCP TypeScript SDK 1.0 makes server development straightforward and type-safe. A walkthrough from scaffold to a deployed Cloud Run service ready for production traffic.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.