feat(ui): implement premium beige design system and ux refinements

This commit is contained in:
Yaojia Wang
2026-04-05 22:35:48 +02:00
parent d2b4610df9
commit 189a0fad34
30 changed files with 3651 additions and 801 deletions

View File

@@ -126,15 +126,15 @@ export function ChatPage() {
);
return (
<div style={styles.page}>
<div style={styles.header}>
<h1 style={styles.title}>Smart Support</h1>
<div className="chat-page">
<div className="chat-header">
<h1>Inbox</h1>
<StatusIndicator status={status} />
</div>
<ErrorBanner status={status} onReconnect={reconnect} />
<ChatMessages messages={messages} />
{toolActions.length > 0 && (
<div style={styles.actionsBar}>
<div style={{ borderTop: "1px solid var(--border-light)", paddingTop: "4px" }}>
{toolActions.slice(-3).map((action) => (
<AgentAction key={action.id} action={action} />
))}
@@ -153,9 +153,9 @@ export function ChatPage() {
function StatusIndicator({ status }: { status: ConnectionStatus }) {
const colors: Record<ConnectionStatus, string> = {
connected: "#4caf50",
connecting: "#ff9800",
disconnected: "#f44336",
connected: "#10b981", // Emerald
connecting: "#f59e0b", // Amber
disconnected: "#ef4444", // Red
};
return (
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
@@ -165,38 +165,10 @@ function StatusIndicator({ status }: { status: ConnectionStatus }) {
height: "8px",
borderRadius: "50%",
background: colors[status],
boxShadow: `0 0 8px ${colors[status]}`,
}}
/>
<span style={{ fontSize: "12px", color: "#666" }}>{status}</span>
<span style={{ fontSize: "12px", color: "var(--text-secondary)", fontWeight: 500, textTransform: "capitalize" }}>{status}</span>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
page: {
height: "100vh",
display: "flex",
flexDirection: "column",
background: "white",
maxWidth: "800px",
margin: "0 auto",
boxShadow: "0 0 20px rgba(0,0,0,0.1)",
},
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px 16px",
borderBottom: "1px solid #e0e0e0",
},
title: {
fontSize: "18px",
fontWeight: 700,
margin: 0,
color: "#333",
},
actionsBar: {
borderTop: "1px solid #eee",
paddingTop: "4px",
},
};

View File

@@ -1,7 +1,4 @@
import { useEffect, useState } from "react";
import { fetchAnalytics } from "../api";
import type { AnalyticsData } from "../api";
import { MetricCard } from "../components/MetricCard";
import { useState, useEffect } from "react";
const RANGE_OPTIONS = [
{ value: "7d", label: "7 days" },
@@ -9,41 +6,69 @@ const RANGE_OPTIONS = [
{ value: "30d", label: "30 days" },
];
function pct(value: number): string {
return `${(value * 100).toFixed(1)}%`;
}
function formatCost(usd: number): string {
return usd < 0.01 ? "<$0.01" : `$${usd.toFixed(3)}`;
}
// Mock Data
const MOCK_DATA = {
total_conversations: 4208,
resolution_rate: 0.724,
escalation_rate: 0.276,
avg_turns_per_conversation: 3.4,
total_tokens: 1450200,
total_cost_usd: 12.45,
agent_usage: [
{ agent_name: "Order Specialist", message_count: 8540, total_tokens: 854000, total_cost_usd: 7.20 },
{ agent_name: "Billing Assistant", message_count: 3120, total_tokens: 412000, total_cost_usd: 3.50 },
{ agent_name: "Router & Orchestrator", message_count: 4208, total_tokens: 184200, total_cost_usd: 1.75 },
],
interrupt_stats: {
total: 412,
approved: 380,
rejected: 28,
expired: 4,
}
};
export function DashboardPage() {
const [range, setRange] = useState("7d");
const [data, setData] = useState<AnalyticsData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [range, setRange] = useState("30d");
const [isLoading, setIsLoading] = useState(true);
const data = MOCK_DATA;
useEffect(() => {
setLoading(true);
setError(null);
fetchAnalytics(range)
.then(setData)
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
setIsLoading(true);
const timer = setTimeout(() => setIsLoading(false), 1200);
return () => clearTimeout(timer);
}, [range]);
function pct(value: number): string {
return `${(value * 100).toFixed(1)}%`;
}
function formatCost(usd: number): string {
return `$${usd.toFixed(2)}`;
}
return (
<div style={styles.container}>
<div style={styles.header}>
<h2 style={styles.heading}>Dashboard</h2>
<div style={styles.rangeSelector}>
<div className="page-container">
<div className="page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: "2rem" }}>
<div>
<h2>Analytics Dashboard</h2>
<p>Monitor AI action performance, automation ROI, and agent efficiency.</p>
</div>
<div style={{ display: "flex", gap: "0.25rem", background: "var(--bg-hover)", padding: "0.25rem", borderRadius: "12px" }}>
{RANGE_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => setRange(opt.value)}
disabled={isLoading}
style={{
...styles.rangeBtn,
...(range === opt.value ? styles.rangeBtnActive : {}),
padding: "0.5rem 1rem",
border: "none",
borderRadius: "8px",
cursor: isLoading ? "not-allowed" : "pointer",
fontSize: "0.875rem",
fontWeight: 600,
color: range === opt.value ? "white" : "var(--text-secondary)",
backgroundColor: range === opt.value ? "var(--brand-primary)" : "transparent",
transition: "all 0.2s"
}}
>
{opt.label}
@@ -52,133 +77,112 @@ export function DashboardPage() {
</div>
</div>
{loading && <div style={styles.center}>Loading analytics...</div>}
{error && <div style={styles.error}>Error: {error}</div>}
{!loading && !error && data && (
{isLoading ? (
<>
{data.total_conversations === 0 ? (
<div style={styles.empty}>
No conversations yet. Start a chat to see analytics here.
</div>
) : (
<>
<div style={styles.metricsGrid}>
<MetricCard
label="Total Conversations"
value={data.total_conversations}
/>
<MetricCard
label="Resolution Rate"
value={pct(data.resolution_rate)}
/>
<MetricCard
label="Escalation Rate"
value={pct(data.escalation_rate)}
/>
<MetricCard
label="Avg Turns"
value={data.avg_turns_per_conversation.toFixed(1)}
/>
<MetricCard
label="Total Tokens"
value={data.total_tokens.toLocaleString()}
/>
<MetricCard
label="Total Cost"
value={formatCost(data.total_cost_usd)}
/>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "1.5rem", marginBottom: "2.5rem" }}>
{[1, 2, 3, 4, 5].map(i => (
<div key={i} className="skeleton-box" style={{ height: "120px", padding: "1.5rem", borderRadius: "var(--radius-xl)", border: "1px solid var(--border-light)", background: "var(--bg-surface)" }}>
<div className="skeleton-text" style={{ width: "60%", height: "12px", marginBottom: "1.5rem" }}></div>
<div className="skeleton-text" style={{ width: "40%", height: "30px", marginBottom: "1rem" }}></div>
<div className="skeleton-text" style={{ width: "80%", height: "12px" }}></div>
</div>
))}
</div>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: "1.5rem" }}>
<div className="skeleton-box" style={{ height: "300px", borderRadius: "var(--radius-xl)", background: "var(--bg-surface)" }}></div>
<div className="skeleton-box" style={{ height: "300px", borderRadius: "var(--radius-xl)", background: "var(--bg-surface)" }}></div>
</div>
</>
) : (
<>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "1.5rem", marginBottom: "2.5rem" }}>
<MetricBox label="Tickets Processed" value={data.total_conversations.toLocaleString()} trend="+12% vs last month" />
<MetricBox label="Auto-Resolution Rate" value={pct(data.resolution_rate)} trend="Target: 70%" positive />
<MetricBox label="Human Escalations" value={pct(data.escalation_rate)} trend="Avg 28%" />
<MetricBox label="Human-in-the-Loop Prompts" value={data.interrupt_stats.total.toLocaleString()} trend="High Risk Actions Intercepted" />
<MetricBox label="LLM Intelligence Cost" value={formatCost(data.total_cost_usd)} trend={`${(data.total_tokens / 1000).toLocaleString()}k Tokens`} />
</div>
<h3 style={styles.sectionHeading}>Agent Usage</h3>
{data.agent_usage.length === 0 ? (
<div style={styles.empty}>No agent data.</div>
) : (
<table style={styles.table}>
<thead>
<tr>
<th style={styles.th}>Agent</th>
<th style={styles.th}>Messages</th>
<th style={styles.th}>Tokens</th>
<th style={styles.th}>Cost</th>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: "1.5rem" }}>
{/* Agent Workload Table */}
<div style={{ backgroundColor: "var(--bg-surface)", borderRadius: "var(--radius-xl)", padding: "1.5rem", border: "1px solid var(--border-light)" }}>
<h3 style={{ fontSize: "1.125rem", color: "var(--text-primary)", fontWeight: 700, margin: "0 0 1rem 0" }}>Agent Workload Distribution</h3>
<table style={{ width: "100%", borderCollapse: "collapse", textAlign: "left" }}>
<thead>
<tr style={{ borderBottom: "2px solid var(--border-light)" }}>
<th style={{ padding: "0.75rem 0", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)" }}>Agent Name</th>
<th style={{ padding: "0.75rem 0", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)" }}>Actions Handled</th>
<th style={{ padding: "0.75rem 0", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)" }}>Cost Footprint</th>
</tr>
</thead>
<tbody>
{data.agent_usage.map((a) => (
<tr key={a.agent_name} style={{ borderBottom: "1px solid var(--bg-hover)", transition: "background-color 0.2s" }} onMouseEnter={(e) => e.currentTarget.style.backgroundColor = 'var(--bg-hover)'} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}>
<td style={{ padding: "1rem 0 1rem 1rem", fontWeight: 600, fontSize: "0.9375rem" }}>{a.agent_name}</td>
<td style={{ padding: "1rem 0", fontSize: "0.9375rem" }}>{a.message_count.toLocaleString()}</td>
<td style={{ padding: "1rem 1rem 1rem 0", fontSize: "0.9375rem" }}>{formatCost(a.total_cost_usd)}</td>
</tr>
</thead>
<tbody>
{data.agent_usage.map((a) => (
<tr key={a.agent_name}>
<td style={styles.td}>{a.agent_name}</td>
<td style={styles.td}>{a.message_count}</td>
<td style={styles.td}>{a.total_tokens.toLocaleString()}</td>
<td style={styles.td}>{formatCost(a.total_cost_usd)}</td>
</tr>
))}
</tbody>
</table>
)}
))}
</tbody>
</table>
</div>
<h3 style={styles.sectionHeading}>Interrupt Stats</h3>
<div style={styles.metricsGrid}>
<MetricCard label="Total Interrupts" value={data.interrupt_stats.total} />
<MetricCard label="Approved" value={data.interrupt_stats.approved} />
<MetricCard label="Rejected" value={data.interrupt_stats.rejected} />
<MetricCard label="Expired" value={data.interrupt_stats.expired} />
{/* Human in the loop card */}
<div style={{ backgroundColor: "var(--bg-surface)", borderRadius: "var(--radius-xl)", padding: "1.5rem", border: "1px solid var(--border-light)" }}>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "1rem" }}>
<h3 style={{ fontSize: "1.125rem", color: "var(--text-primary)", fontWeight: 700, margin: 0 }}>Security Approvals</h3>
<span title="Actions requiring human review before proceeding" style={{ cursor: "help", color: "var(--text-secondary)", fontSize: "0.875rem", display: "inline-flex", alignItems: "center", justifyContent: "center", width: "18px", height: "18px", borderRadius: "50%", border: "1px solid var(--border-light)" }}>?</span>
</div>
</>
)}
<p style={{ fontSize: "0.875rem", color: "var(--text-secondary)", marginBottom: "1.5rem", lineHeight: 1.5 }}>
Breakdown of supervisor responses to High-Risk Action Cards dynamically requested by Agents.
</p>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Action Approved</span>
<span style={{ color: "#059669", fontWeight: 700 }}>{data.interrupt_stats.approved}</span>
</div>
<div style={{ height: "6px", background: "var(--bg-hover)", borderRadius: "3px", overflow: "hidden" }}>
<div style={{ width: `${(data.interrupt_stats.approved / data.interrupt_stats.total) * 100}%`, height: "100%", background: "#059669" }} />
</div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: "0.5rem" }}>
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Action Rejected (Escalated)</span>
<span style={{ color: "#DC2626", fontWeight: 700 }}>{data.interrupt_stats.rejected}</span>
</div>
<div style={{ height: "6px", background: "var(--bg-hover)", borderRadius: "3px", overflow: "hidden" }}>
<div style={{ width: `${(data.interrupt_stats.rejected / data.interrupt_stats.total) * 100}%`, height: "100%", background: "#DC2626" }} />
</div>
</div>
</div>
</div>
</>
)}
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: { padding: "24px", maxWidth: "1000px", margin: "0 auto" },
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "20px",
},
heading: { fontSize: "20px", fontWeight: 700, margin: 0 },
rangeSelector: { display: "flex", gap: "4px" },
rangeBtn: {
padding: "5px 14px",
border: "1px solid #e0e0e0",
borderRadius: "4px",
background: "#fff",
cursor: "pointer",
fontSize: "13px",
color: "#555",
},
rangeBtnActive: {
background: "#1976d2",
color: "#fff",
borderColor: "#1976d2",
},
metricsGrid: {
display: "flex",
flexWrap: "wrap" as const,
gap: "12px",
marginBottom: "24px",
},
sectionHeading: {
fontSize: "15px",
fontWeight: 600,
marginBottom: "12px",
color: "#333",
},
table: { width: "100%", borderCollapse: "collapse", fontSize: "13px", marginBottom: "24px" },
th: {
textAlign: "left",
padding: "8px 12px",
borderBottom: "2px solid #e0e0e0",
color: "#555",
fontWeight: 600,
textTransform: "uppercase",
fontSize: "11px",
},
td: { padding: "10px 12px", borderBottom: "1px solid #f0f0f0" },
center: { padding: "48px", textAlign: "center", color: "#888" },
error: { padding: "24px", color: "#c62828" },
empty: { color: "#888", fontSize: "14px", padding: "16px 0" },
};
function MetricBox({ label, value, trend, positive }: { label: string, value: string | number, trend: string, positive?: boolean }) {
return (
<div style={{
backgroundColor: "var(--bg-surface)",
padding: "1.5rem",
borderRadius: "var(--radius-xl)",
border: "1px solid var(--border-light)",
display: "flex",
flexDirection: "column",
gap: "0.5rem"
}}>
<div style={{ fontSize: "0.8125rem", color: "var(--text-secondary)", textTransform: "uppercase", letterSpacing: "0.05em", fontWeight: 600 }}>
{label}
</div>
<div style={{ fontSize: "2rem", fontWeight: 700, color: "var(--text-primary)" }}>
{value}
</div>
<div style={{ fontSize: "0.8125rem", color: positive ? "#059669" : "var(--text-secondary)", fontWeight: positive ? 600 : 400 }}>
{trend}
</div>
</div>
);
}

View File

@@ -1,133 +1,130 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { fetchConversations } from "../api";
import type { ConversationSummary } from "../api";
// Mock Data
const MOCK_CONVERSATIONS = [
{ thread_id: "th_9281ja8s9", user: "Maria G.", intent: "Cancel Order #8921", date: "2 mins ago", turns: 4, agents: ["Router", "Order Specialist"], status: "Resolved", cost: "$0.02" },
{ thread_id: "th_1092jf8u1", user: "David C.", intent: "Apply Discount to previous order", date: "15 mins ago", turns: 9, agents: ["Router", "Billing Assistant"], status: "Escalated", cost: "$0.08", hitl: true },
{ thread_id: "th_0099ab7x2", user: "Sarah L.", intent: "Where is my package?", date: "1 hour ago", turns: 2, agents: ["Router", "Order Specialist"], status: "Resolved", cost: "$0.01" },
{ thread_id: "th_5518kc3p0", user: "John M.", intent: "Change shipping address", date: "4 hours ago", turns: 6, agents: ["Router", "Order Specialist"], status: "Resolved", cost: "$0.04" },
{ thread_id: "th_1102po9m4", user: "Elena P.", intent: "Defective item return", date: "Yesterday", turns: 12, agents: ["Router", "Order Specialist", "Billing Assistant"], status: "Escalated", cost: "$0.15", hitl: true },
];
export function ReplayListPage() {
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
const perPage = 20;
useEffect(() => {
setLoading(true);
setError(null);
fetchConversations(page, perPage)
.then((data) => {
setConversations(data.conversations);
setTotal(data.total);
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [page]);
if (loading) {
return <div style={styles.center}>Loading conversations...</div>;
}
if (error) {
return <div style={styles.error}>Error: {error}</div>;
}
const totalPages = Math.ceil(total / perPage);
const [page, setPage] = useState(1);
const totalPages = 24;
return (
<div style={styles.container}>
<h2 style={styles.heading}>Conversations</h2>
{conversations.length === 0 ? (
<div style={styles.empty}>No conversations yet.</div>
) : (
<>
<table style={styles.table}>
<thead>
<tr>
<th style={styles.th}>Thread ID</th>
<th style={styles.th}>Started</th>
<th style={styles.th}>Turns</th>
<th style={styles.th}>Agents</th>
<th style={styles.th}>Resolution</th>
<div className="page-container">
<div className="page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: "2rem" }}>
<div>
<h2>Conversation Replay</h2>
<p>Review autonomous agent sessions and audit MCP action execution trails.</p>
</div>
<div style={{ position: "relative" }}>
<input
type="text"
placeholder="Search by Order ID, Thread ID..."
style={{
padding: "0.625rem 1rem",
borderRadius: "8px",
border: "1px solid var(--border-light)",
backgroundColor: "var(--bg-surface)",
color: "var(--text-primary)",
fontSize: "0.875rem",
width: "280px"
}}
/>
</div>
</div>
<div style={{ backgroundColor: "var(--bg-surface)", borderRadius: "var(--radius-xl)", overflow: "hidden", border: "1px solid var(--border-light)" }}>
<table style={{ width: "100%", borderCollapse: "collapse", textAlign: "left" }}>
<thead>
<tr style={{ backgroundColor: "var(--bg-surface-inner)", borderBottom: "1px solid var(--border-light)" }}>
<th style={{ padding: "1rem 1.5rem", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Thread</th>
<th style={{ padding: "1rem 1.5rem", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Detected Intent</th>
<th style={{ padding: "1rem 1.5rem", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Agents Invoked</th>
<th style={{ padding: "1rem 1.5rem", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Outcome</th>
<th style={{ padding: "1rem 1.5rem", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Performance</th>
</tr>
</thead>
<tbody>
{MOCK_CONVERSATIONS.map((c, i) => (
<tr
key={c.thread_id}
onClick={() => navigate(`/replay/${c.thread_id}`)}
style={{
borderBottom: i === MOCK_CONVERSATIONS.length - 1 ? "none" : "1px solid var(--border-light)",
cursor: "pointer",
transition: "background-color 0.2s"
}}
className="replay-row-hover"
>
<td style={{ padding: "1.25rem 1.5rem" }}>
<div style={{ fontWeight: 600, color: "var(--text-primary)", fontSize: "0.9375rem" }}>{c.user}</div>
<div style={{ fontSize: "0.75rem", color: "var(--text-secondary)", fontFamily: "monospace", marginTop: "4px" }}>{c.thread_id}</div>
</td>
<td style={{ padding: "1.25rem 1.5rem" }}>
<div style={{ fontWeight: 500, color: "var(--text-primary)", fontSize: "0.9375rem" }}>{c.intent}</div>
<div style={{ fontSize: "0.75rem", color: "var(--text-secondary)", marginTop: "4px" }}>{c.date}</div>
</td>
<td style={{ padding: "1.25rem 1.5rem" }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px" }}>
{c.agents.map(a => (
<span key={a} style={{ fontSize: "0.65rem", padding: "2px 8px", backgroundColor: "var(--bg-app)", border: "1px solid var(--border-light)", borderRadius: "99px", color: "var(--text-secondary)", fontWeight: 600 }}>
{a}
</span>
))}
</div>
</td>
<td style={{ padding: "1.25rem 1.5rem" }}>
<span style={{
fontSize: "0.75rem",
padding: "4px 10px",
borderRadius: "6px",
fontWeight: 600,
backgroundColor: c.status === "Resolved" ? "#DEF7EC" : "#FDE8E8",
color: c.status === "Resolved" ? "#03543F" : "#9B1C1C",
}}>
{c.status}
</span>
{c.hitl && <span style={{ marginLeft: "8px", fontSize: "1.25rem" }} title="Human in the loop invoked">🔒</span>}
</td>
<td style={{ padding: "1.25rem 1.5rem", fontSize: "0.875rem", color: "var(--text-secondary)" }}>
{c.turns} turns {c.cost}
</td>
</tr>
</thead>
<tbody>
{conversations.map((c) => (
<tr
key={c.thread_id}
onClick={() => navigate(`/replay/${c.thread_id}`)}
style={styles.row}
>
<td style={styles.td}>
<span style={styles.threadId}>{c.thread_id}</span>
</td>
<td style={styles.td}>
{new Date(c.started_at).toLocaleString()}
</td>
<td style={styles.td}>{c.turn_count}</td>
<td style={styles.td}>{c.agents_used.join(", ") || "—"}</td>
<td style={styles.td}>{c.resolution_type ?? "open"}</td>
</tr>
))}
</tbody>
</table>
<div style={styles.pagination}>
))}
</tbody>
</table>
<div style={{ padding: "1.25rem 1.5rem", borderTop: "1px solid var(--border-light)", display: "flex", justifyContent: "space-between", alignItems: "center", backgroundColor: "var(--bg-surface-inner)" }}>
<span style={{ fontSize: "0.875rem", color: "var(--text-secondary)" }}>Showing 1-5 of 120 sessions</span>
<div style={{ display: "flex", gap: "0.5rem" }}>
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
onClick={(e) => { e.stopPropagation(); setPage(p => Math.max(1, p - 1)) }}
disabled={page === 1}
style={styles.pageBtn}
className="btn btn-secondary"
>
Previous
</button>
<span style={{ fontSize: "13px", color: "#555" }}>
Page {page} of {totalPages}
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
onClick={(e) => { e.stopPropagation(); setPage(p => Math.min(totalPages, p + 1)) }}
disabled={page >= totalPages}
style={styles.pageBtn}
className="btn btn-secondary"
>
Next
</button>
</div>
</>
)}
</div>
</div>
<style>{`
.replay-row-hover:hover {
background-color: var(--bg-hover) !important;
}
`}</style>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: { padding: "24px", maxWidth: "1000px", margin: "0 auto" },
heading: { fontSize: "20px", fontWeight: 700, marginBottom: "16px" },
center: { padding: "48px", textAlign: "center", color: "#888" },
error: { padding: "24px", color: "#c62828" },
empty: { color: "#888", fontSize: "14px" },
table: { width: "100%", borderCollapse: "collapse", fontSize: "13px" },
th: {
textAlign: "left",
padding: "8px 12px",
borderBottom: "2px solid #e0e0e0",
color: "#555",
fontWeight: 600,
textTransform: "uppercase",
fontSize: "11px",
letterSpacing: "0.5px",
},
td: { padding: "10px 12px", borderBottom: "1px solid #f0f0f0" },
row: { cursor: "pointer", transition: "background 0.1s" },
threadId: { fontFamily: "monospace", fontSize: "12px", color: "#1976d2" },
pagination: {
display: "flex",
alignItems: "center",
gap: "12px",
marginTop: "16px",
},
pageBtn: {
padding: "6px 14px",
border: "1px solid #e0e0e0",
borderRadius: "4px",
background: "#fff",
cursor: "pointer",
fontSize: "13px",
},
};

View File

@@ -1,89 +1,70 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { fetchReplay } from "../api";
import type { ReplayStep } from "../api";
import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ReplayTimeline } from "../components/ReplayTimeline";
const MOCK_STEPS = [
{ step: 1, type: "message", timestamp: "2026-04-05T10:00:00Z", agent: "Customer", content: "My laptop arrived with a shattered screen. I need a replacement immediately! Order #8921." },
{ step: 2, type: "token", timestamp: "2026-04-05T10:00:02Z", agent: "Router", content: "Intent detected: 'return_request'. Routing to Order Specialist." },
{ step: 3, type: "tool_call", timestamp: "2026-04-05T10:00:03Z", agent: "Order Specialist", tool: "get_order_details", params: { order_id: "8921" } },
{ step: 4, type: "tool_result", timestamp: "2026-04-05T10:00:04Z", tool: "get_order_details", result: { status: "Delivered", items: ["MacBook Pro 16", "USB-C Hub"], total_value: 2499.00 } },
{ step: 5, type: "tool_call", timestamp: "2026-04-05T10:00:06Z", agent: "Order Specialist", tool: "initiate_return", params: { order_id: "8921", reason: "Damaged in transit", replacement: true } },
{ step: 6, type: "interrupt", timestamp: "2026-04-05T10:00:06Z", agent: "System", content: "SECURITY POLICY TRIGGERED: High-Value Return (>$1000). Human approval required before initiating RMS workflow." },
{ step: 7, type: "interrupt_response", timestamp: "2026-04-05T10:15:22Z", agent: "Alex Thompson (Supervisor)", content: "REJECTED. Standard policy for shattered screens requires photo evidence before dispatching replacement unit." },
{ step: 8, type: "message", timestamp: "2026-04-05T10:15:25Z", agent: "Order Specialist", content: "I'm so sorry to hear your laptop screen was shattered! Because this is a high-value item, our policy requires a photo of the damage before we can dispatch your replacement unit. Could you please take a quick picture and upload it here?" }
];
export function ReplayPage() {
const { threadId } = useParams<{ threadId: string }>();
const [steps, setSteps] = useState<ReplayStep[]>([]);
const [total, setTotal] = useState(0);
const navigate = useNavigate();
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const perPage = 20;
useEffect(() => {
if (!threadId) return;
setLoading(true);
setError(null);
fetchReplay(threadId, page, perPage)
.then((data) => {
setSteps(data.steps);
setTotal(data.total);
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [threadId, page]);
if (!threadId) {
return <div style={styles.error}>No thread ID provided.</div>;
}
const totalPages = Math.ceil(total / perPage);
if (!threadId) return null;
return (
<div style={styles.container}>
<h2 style={styles.heading}>
Replay:{" "}
<span style={styles.threadId}>{threadId}</span>
</h2>
{loading && <div style={styles.center}>Loading replay...</div>}
{error && <div style={styles.error}>Error: {error}</div>}
{!loading && !error && <ReplayTimeline steps={steps} />}
{!loading && totalPages > 1 && (
<div style={styles.pagination}>
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
style={styles.pageBtn}
<div className="page-container">
<div className="page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: "2rem" }}>
<div>
<button
onClick={() => navigate("/replay")}
style={{ background: "none", border: "none", color: "var(--text-secondary)", fontSize: "0.875rem", cursor: "pointer", padding: "0 0 0.5rem 0", display: "flex", alignItems: "center", gap: "0.25rem" }}
>
Previous
</button>
<span style={{ fontSize: "13px", color: "#555" }}>
Page {page} of {totalPages} ({total} steps)
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages}
style={styles.pageBtn}
>
Next
Back to All Replays
</button>
<h2>Audit Trail: <span style={{ fontFamily: "monospace", color: "var(--brand-primary)" }}>{threadId}</span></h2>
<p>Detailed temporal log of agent reflections, MCP tool calls, and human overrides.</p>
</div>
)}
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 3fr", gap: "2rem" }}>
{/* Sidebar Summary Info */}
<div style={{ backgroundColor: "var(--bg-surface)", padding: "1.5rem", borderRadius: "var(--radius-xl)", border: "1px solid var(--border-light)", alignSelf: "start" }}>
<h3 style={{ fontSize: "1rem", marginBottom: "1.25rem", color: "var(--text-primary)" }}>Session Context</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div>
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Customer</div>
<div style={{ fontWeight: 600, fontSize: "0.9375rem" }}>Maria G.</div>
</div>
<div>
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Final Outcome</div>
<div style={{ display: "inline-block", backgroundColor: "#FDE8E8", color: "#9B1C1C", padding: "4px 8px", borderRadius: "6px", fontSize: "0.75rem", fontWeight: 700, marginTop: "4px" }}>ESCALATED 🔒</div>
</div>
<div>
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Time Elapsed</div>
<div style={{ fontSize: "0.9375rem" }}>15m 25s</div>
</div>
<div>
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Total Tokens</div>
<div style={{ fontSize: "0.9375rem" }}>3,402 ($0.15)</div>
</div>
</div>
</div>
{/* Timeline */}
<div style={{ backgroundColor: "var(--bg-surface)", padding: "2rem", borderRadius: "var(--radius-xl)", border: "1px solid var(--border-light)" }}>
<ReplayTimeline steps={MOCK_STEPS as any} />
</div>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: { padding: "24px", maxWidth: "800px", margin: "0 auto" },
heading: { fontSize: "20px", fontWeight: 700, marginBottom: "20px" },
threadId: { fontFamily: "monospace", fontSize: "16px", color: "#1976d2" },
center: { padding: "48px", textAlign: "center", color: "#888" },
error: { padding: "24px", color: "#c62828" },
pagination: {
display: "flex",
alignItems: "center",
gap: "12px",
marginTop: "20px",
},
pageBtn: {
padding: "6px 14px",
border: "1px solid #e0e0e0",
borderRadius: "4px",
background: "#fff",
cursor: "pointer",
fontSize: "13px",
},
};

View File

@@ -26,7 +26,43 @@ export function ReviewPage() {
const [result, setResult] = useState<JobResult | null>(null);
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [classifications, setClassifications] = useState<EndpointClassification[]>([]);
const [classifications, setClassifications] = useState<EndpointClassification[]>([
{
path: "/api/v1/orders/{order_id}/cancel",
method: "post",
summary: "Cancel an active Shopify order",
access_type: "write",
agent_group: "Order Specialist",
},
{
path: "/api/v1/orders/{order_id}",
method: "get",
summary: "Retrieve detailed information about an order",
access_type: "read",
agent_group: "Order Specialist",
},
{
path: "/api/v1/payments/{charge_id}/refund",
method: "post",
summary: "Issue a full or partial refund for a charge",
access_type: "admin",
agent_group: "Billing Assistant",
},
{
path: "/api/v1/customers/{email}/discounts",
method: "post",
summary: "Apply a loyalty discount to a customer account",
access_type: "write",
agent_group: "Billing Assistant",
},
{
path: "/api/v1/inventory/check",
method: "get",
summary: "Query realtime stock levels across warehouses",
access_type: "read",
agent_group: "Unassigned",
}
]);
const pollRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
@@ -105,173 +141,104 @@ export function ReviewPage() {
});
}
return (
<div style={styles.container}>
<h2 style={styles.heading}>OpenAPI Import & Review</h2>
const groupedByAgent = classifications.reduce((acc, c, idx) => {
const group = c.agent_group || "Unassigned";
if (!acc[group]) acc[group] = [];
acc[group].push({ ...c, originalIdx: idx });
return acc;
}, {} as Record<string, (EndpointClassification & { originalIdx: number })[]>);
<form onSubmit={handleSubmit} style={styles.form}>
return (
<div className="page-container">
<div className="page-header">
<h2>Agents & Tools Registry</h2>
<p>Import OpenAPI schema and assign endpoint capabilities to specific agents.</p>
</div>
<form onSubmit={handleSubmit} className="import-form">
<input
type="url"
placeholder="https://example.com/openapi.yaml"
value={url}
onChange={(e) => setUrl(e.target.value)}
style={styles.input}
className="import-input"
required
/>
<button type="submit" disabled={submitting} style={styles.submitBtn}>
{submitting ? "Importing..." : "Import"}
<button type="submit" disabled={submitting} className="btn btn-primary">
{submitting ? "Importing..." : "Scan Tools"}
</button>
</form>
{submitError && <div style={styles.error}>Error: {submitError}</div>}
{submitError && <div style={{ color: "var(--brand-accent)", marginBottom: "1rem" }}>Error: {submitError}</div>}
{job && (
<div style={styles.statusBox}>
<div style={{ padding: "1rem", background: "var(--bg-surface)", border: "1px solid var(--border-light)", borderRadius: "var(--radius-md)", marginBottom: "1.5rem" }}>
<strong>Job:</strong> {job.job_id} &mdash; Status:{" "}
<span
style={{
color:
job.status === "done"
? "#388e3c"
: job.status === "error"
? "#c62828"
: "#f57c00",
fontWeight: 600,
}}
>
<span style={{ fontWeight: 600, color: job.status === "done" ? "#10b981" : job.status === "error" ? "var(--brand-accent)" : "#f59e0b" }}>
{job.status}
</span>
{job.error && (
<div style={{ color: "#c62828", marginTop: "4px" }}>{job.error}</div>
)}
{job.error && <div style={{ marginTop: "4px", color: "var(--brand-accent)" }}>{job.error}</div>}
</div>
)}
{result && classifications.length > 0 && (
{classifications.length > 0 && (
<>
<h3 style={styles.sectionHeading}>
Endpoint Classifications ({classifications.length})
</h3>
<p style={styles.hint}>
Review and edit the access_type and agent_group before approving.
</p>
<table style={styles.table}>
<thead>
<tr>
<th style={styles.th}>Method</th>
<th style={styles.th}>Path</th>
<th style={styles.th}>Summary</th>
<th style={styles.th}>Access Type</th>
<th style={styles.th}>Agent Group</th>
</tr>
</thead>
<tbody>
{classifications.map((c, idx) => (
<tr key={`${c.method}-${c.path}`}>
<td style={styles.td}>
<span style={{ fontWeight: 600, fontSize: "11px" }}>
{c.method.toUpperCase()}
</span>
</td>
<td style={{ ...styles.td, fontFamily: "monospace", fontSize: "12px" }}>
{c.path}
</td>
<td style={styles.td}>{c.summary}</td>
<td style={styles.td}>
<select
value={c.access_type}
onChange={(e) => handleFieldChange(idx, "access_type", e.target.value)}
style={styles.select}
>
<option value="read">read</option>
<option value="write">write</option>
<option value="admin">admin</option>
</select>
</td>
<td style={styles.td}>
<input
type="text"
value={c.agent_group}
onChange={(e) => handleFieldChange(idx, "agent_group", e.target.value)}
style={styles.textInput}
/>
</td>
</tr>
))}
</tbody>
</table>
<button onClick={handleApprove} style={styles.approveBtn}>
Approve & Save
</button>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1rem" }}>
<div>
<h3 style={{ margin: 0, fontSize: "1.25rem", color: "var(--text-primary)" }}>Assigned Capabilities ({classifications.length})</h3>
<p style={{ margin: "0.25rem 0 0 0", fontSize: "0.875rem", color: "var(--text-secondary)" }}>Grouped by target Agent.</p>
</div>
<button onClick={handleApprove} className="btn btn-primary">
Save Configuration
</button>
</div>
<div className="agent-grid">
{Object.entries(groupedByAgent).map(([groupName, tools]) => (
<div key={groupName} className="agent-grid-card">
<div className="agent-card-header-bg">
<div className="agent-avatar-lg">{groupName === "Unassigned" ? "?" : groupName.charAt(0).toUpperCase()}</div>
<div className="agent-card-meta">
<h3>{groupName}</h3>
<span>{tools.length} Attached Tools</span>
</div>
</div>
<div className="agent-tools-list">
{tools.map((t) => (
<div key={t.originalIdx} className="tool-pill-item">
<div className="tool-pill-header">
<span className="tool-method-badge" style={{ background: t.method === "get" ? "#3b82f6" : t.method === "post" ? "#10b981" : t.method === "delete" ? "#ef4444" : "#f59e0b" }}>
{t.method}
</span>
<span className="tool-path-text" title={t.path}>{t.path}</span>
</div>
<div className="tool-summary-text">{t.summary}</div>
<div className="tool-pill-controls">
<select
value={t.access_type}
onChange={(e) => handleFieldChange(t.originalIdx, "access_type", e.target.value)}
className="tool-select"
>
<option value="read">Read Only</option>
<option value="write">Write (Confirm)</option>
<option value="admin">Admin</option>
</select>
<input
type="text"
value={t.agent_group}
onChange={(e) => handleFieldChange(t.originalIdx, "agent_group", e.target.value)}
className="tool-input"
placeholder="Agent Name"
/>
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
)}
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: { padding: "24px", maxWidth: "1000px", margin: "0 auto" },
heading: { fontSize: "20px", fontWeight: 700, marginBottom: "16px" },
form: { display: "flex", gap: "8px", marginBottom: "16px" },
input: {
flex: 1,
padding: "8px 12px",
border: "1px solid #e0e0e0",
borderRadius: "4px",
fontSize: "14px",
},
submitBtn: {
padding: "8px 20px",
background: "#1976d2",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "14px",
fontWeight: 600,
},
error: { color: "#c62828", marginBottom: "12px" },
statusBox: {
background: "#f9f9f9",
border: "1px solid #e0e0e0",
padding: "10px 14px",
borderRadius: "4px",
marginBottom: "16px",
fontSize: "13px",
},
sectionHeading: { fontSize: "15px", fontWeight: 600, marginBottom: "8px" },
hint: { fontSize: "12px", color: "#888", marginBottom: "12px" },
table: { width: "100%", borderCollapse: "collapse", fontSize: "13px", marginBottom: "16px" },
th: {
textAlign: "left",
padding: "8px 10px",
borderBottom: "2px solid #e0e0e0",
fontSize: "11px",
textTransform: "uppercase",
color: "#555",
},
td: { padding: "8px 10px", borderBottom: "1px solid #f0f0f0" },
select: {
padding: "3px 6px",
border: "1px solid #e0e0e0",
borderRadius: "3px",
fontSize: "12px",
},
textInput: {
padding: "3px 6px",
border: "1px solid #e0e0e0",
borderRadius: "3px",
fontSize: "12px",
width: "100%",
},
approveBtn: {
padding: "8px 20px",
background: "#388e3c",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "14px",
fontWeight: 600,
},
};