refactor: formalize safety rules, extract shared styles, reconcile docs (P2)
- Add backend/app/safety.py with explicit confirmation policy, multi-intent semantics, and MCP error taxonomy with retry classification - Add 26 unit tests for safety module (confirmation rules, error taxonomy) - Extract repeated inline styles into shared CSS classes in index.css (section-card, stat-label, status-badge, data-table, empty/error-state, pagination-bar) - Refactor DashboardPage, ReplayListPage, ReplayPage to use shared classes - Update README: add missing API endpoints, document safety/confirmation rules - Use proper HTML entities for arrow/dash characters to fix encoding glitches
This commit is contained in:
@@ -658,6 +658,140 @@ body {
|
||||
border-color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* --- Shared Data Display Components --- */
|
||||
|
||||
.section-card {
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge--resolved {
|
||||
background-color: #DEF7EC;
|
||||
color: #03543F;
|
||||
}
|
||||
|
||||
.status-badge--escalated {
|
||||
background-color: #FDE8E8;
|
||||
color: #9B1C1C;
|
||||
}
|
||||
|
||||
.status-badge--active {
|
||||
background-color: var(--bg-hover);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 1.25rem 1.5rem;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.data-table thead tr {
|
||||
border-bottom: 2px solid var(--border-light);
|
||||
}
|
||||
|
||||
.data-table tbody tr {
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.data-table tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state__title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty-state__description {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.error-state__title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--brand-accent);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-state__description {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-top: 1px solid var(--border-light);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--bg-surface-inner);
|
||||
}
|
||||
|
||||
.pagination-bar__info {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.pagination-bar__controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* --- Skeleton Loading Animation --- */
|
||||
@keyframes pulse-skeleton {
|
||||
0% { opacity: 0.5; background-color: var(--bg-hover); }
|
||||
|
||||
@@ -65,7 +65,7 @@ export function DashboardPage() {
|
||||
<>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "1.5rem", marginBottom: "2.5rem" }}>
|
||||
{[1, 2, 3, 4].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 key={i} className="skeleton-box section-card" style={{ height: "120px" }}>
|
||||
<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>
|
||||
@@ -78,15 +78,15 @@ export function DashboardPage() {
|
||||
</div>
|
||||
</>
|
||||
) : error ? (
|
||||
<div style={{ padding: "3rem", textAlign: "center", color: "var(--text-secondary)" }}>
|
||||
<p style={{ fontSize: "1.125rem", fontWeight: 600, color: "var(--brand-accent)" }}>Failed to load analytics</p>
|
||||
<p style={{ marginTop: "0.5rem" }}>{error}</p>
|
||||
<div className="error-state">
|
||||
<p className="error-state__title">Failed to load analytics</p>
|
||||
<p className="error-state__description">{error}</p>
|
||||
<button onClick={() => setRange(range)} className="btn btn-secondary" style={{ marginTop: "1rem" }}>Retry</button>
|
||||
</div>
|
||||
) : !data ? (
|
||||
<div style={{ padding: "3rem", textAlign: "center", color: "var(--text-secondary)" }}>
|
||||
<p style={{ fontSize: "1.125rem", fontWeight: 600 }}>No analytics data available</p>
|
||||
<p style={{ marginTop: "0.5rem" }}>Start some conversations to see metrics here.</p>
|
||||
<div className="empty-state">
|
||||
<p className="empty-state__title">No analytics data available</p>
|
||||
<p className="empty-state__description">Start some conversations to see metrics here.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -99,25 +99,25 @@ export function DashboardPage() {
|
||||
|
||||
<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)" }}>
|
||||
<div className="section-card">
|
||||
<h3 style={{ fontSize: "1.125rem", color: "var(--text-primary)", fontWeight: 700, margin: "0 0 1rem 0" }}>Agent Workload Distribution</h3>
|
||||
{data.agent_usage.length === 0 ? (
|
||||
<p style={{ color: "var(--text-secondary)", fontSize: "0.875rem" }}>No agent activity recorded yet.</p>
|
||||
) : (
|
||||
<table style={{ width: "100%", borderCollapse: "collapse", textAlign: "left" }}>
|
||||
<table className="data-table">
|
||||
<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)" }}>Message Count</th>
|
||||
<th style={{ padding: "0.75rem 0", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)" }}>Share</th>
|
||||
<tr>
|
||||
<th style={{ paddingLeft: 0 }}>Agent Name</th>
|
||||
<th>Message Count</th>
|
||||
<th>Share</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.agent_usage.map((a) => (
|
||||
<tr key={a.agent} 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}</td>
|
||||
<td style={{ padding: "1rem 0", fontSize: "0.9375rem" }}>{a.count.toLocaleString()}</td>
|
||||
<td style={{ padding: "1rem 1rem 1rem 0", fontSize: "0.9375rem" }}>{pct(a.percentage)}</td>
|
||||
<tr key={a.agent}>
|
||||
<td style={{ paddingLeft: 0, fontWeight: 600 }}>{a.agent}</td>
|
||||
<td>{a.count.toLocaleString()}</td>
|
||||
<td>{pct(a.percentage)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -126,7 +126,7 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* 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 className="section-card">
|
||||
<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>
|
||||
@@ -167,24 +167,10 @@ export function DashboardPage() {
|
||||
|
||||
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 className="section-card" style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
<div className="stat-label">{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,23 +37,27 @@ export function ReplayListPage() {
|
||||
return `$${usd.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function statusClass(status: string | null): string {
|
||||
if (status === "resolved") return "status-badge status-badge--resolved";
|
||||
if (status === "escalated") return "status-badge status-badge--escalated";
|
||||
return "status-badge status-badge--active";
|
||||
}
|
||||
|
||||
return (
|
||||
<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 className="page-header">
|
||||
<h2>Conversation Replay</h2>
|
||||
<p>Review autonomous agent sessions and audit MCP action execution trails.</p>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div style={{ padding: "3rem", textAlign: "center", color: "var(--text-secondary)" }}>
|
||||
<p style={{ fontSize: "1.125rem", fontWeight: 600, color: "var(--brand-accent)" }}>Failed to load conversations</p>
|
||||
<p style={{ marginTop: "0.5rem" }}>{error}</p>
|
||||
<div className="error-state">
|
||||
<p className="error-state__title">Failed to load conversations</p>
|
||||
<p className="error-state__description">{error}</p>
|
||||
<button onClick={() => setPage(1)} className="btn btn-secondary" style={{ marginTop: "1rem" }}>Retry</button>
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div style={{ backgroundColor: "var(--bg-surface)", borderRadius: "var(--radius-xl)", overflow: "hidden", border: "1px solid var(--border-light)", padding: "2rem" }}>
|
||||
<div className="section-card" style={{ padding: "2rem" }}>
|
||||
{[1, 2, 3, 4, 5].map(i => (
|
||||
<div key={i} className="skeleton-box" style={{ height: "60px", marginBottom: "1rem", borderRadius: "8px" }}>
|
||||
<div className="skeleton-text" style={{ width: "30%", height: "14px", margin: "12px 16px" }}></div>
|
||||
@@ -61,56 +65,38 @@ export function ReplayListPage() {
|
||||
))}
|
||||
</div>
|
||||
) : conversations.length === 0 ? (
|
||||
<div style={{ padding: "3rem", textAlign: "center", color: "var(--text-secondary)" }}>
|
||||
<p style={{ fontSize: "1.125rem", fontWeight: 600 }}>No conversations yet</p>
|
||||
<p style={{ marginTop: "0.5rem" }}>Start a chat session to see conversations here.</p>
|
||||
<div className="empty-state">
|
||||
<p className="empty-state__title">No conversations yet</p>
|
||||
<p className="empty-state__description">Start a chat session to see conversations here.</p>
|
||||
</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" }}>
|
||||
<div className="section-card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<table className="data-table">
|
||||
<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 }}>Created</th>
|
||||
<th style={{ padding: "1rem 1.5rem", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Last Activity</th>
|
||||
<th style={{ padding: "1rem 1.5rem", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Status</th>
|
||||
<th style={{ padding: "1rem 1.5rem", fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Cost</th>
|
||||
<tr style={{ backgroundColor: "var(--bg-surface-inner)" }}>
|
||||
<th>Thread</th>
|
||||
<th>Created</th>
|
||||
<th>Last Activity</th>
|
||||
<th>Status</th>
|
||||
<th>Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{conversations.map((c, i) => (
|
||||
{conversations.map((c) => (
|
||||
<tr
|
||||
key={c.thread_id}
|
||||
onClick={() => navigate(`/replay/${c.thread_id}`)}
|
||||
style={{
|
||||
borderBottom: i === conversations.length - 1 ? "none" : "1px solid var(--border-light)",
|
||||
cursor: "pointer",
|
||||
transition: "background-color 0.2s"
|
||||
}}
|
||||
className="replay-row-hover"
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<td style={{ padding: "1.25rem 1.5rem" }}>
|
||||
<div style={{ fontWeight: 600, color: "var(--text-primary)", fontSize: "0.9375rem", fontFamily: "monospace" }}>{c.thread_id}</div>
|
||||
<td>
|
||||
<span style={{ fontWeight: 600, fontFamily: "monospace" }}>{c.thread_id}</span>
|
||||
</td>
|
||||
<td style={{ padding: "1.25rem 1.5rem", fontSize: "0.875rem", color: "var(--text-secondary)" }}>
|
||||
{formatDate(c.created_at)}
|
||||
<td style={{ color: "var(--text-secondary)" }}>{formatDate(c.created_at)}</td>
|
||||
<td style={{ color: "var(--text-secondary)" }}>{formatDate(c.last_activity)}</td>
|
||||
<td>
|
||||
<span className={statusClass(c.status)}>{c.status ?? "active"}</span>
|
||||
</td>
|
||||
<td style={{ padding: "1.25rem 1.5rem", fontSize: "0.875rem", color: "var(--text-secondary)" }}>
|
||||
{formatDate(c.last_activity)}
|
||||
</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" : c.status === "escalated" ? "#FDE8E8" : "var(--bg-hover)",
|
||||
color: c.status === "resolved" ? "#03543F" : c.status === "escalated" ? "#9B1C1C" : "var(--text-secondary)",
|
||||
}}>
|
||||
{c.status ?? "active"}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: "1.25rem 1.5rem", fontSize: "0.875rem", color: "var(--text-secondary)" }}>
|
||||
<td style={{ color: "var(--text-secondary)" }}>
|
||||
{c.total_tokens.toLocaleString()} tokens / {formatCost(c.total_cost_usd)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -118,11 +104,11 @@ export function ReplayListPage() {
|
||||
</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)" }}>
|
||||
<div className="pagination-bar">
|
||||
<span className="pagination-bar__info">
|
||||
Showing {(page - 1) * perPage + 1}-{Math.min(page * perPage, total)} of {total} sessions
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||
<div className="pagination-bar__controls">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setPage(p => Math.max(1, p - 1)) }}
|
||||
disabled={page === 1}
|
||||
@@ -141,11 +127,6 @@ export function ReplayListPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<style>{`
|
||||
.replay-row-hover:hover {
|
||||
background-color: var(--bg-hover) !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,23 +28,21 @@ export function ReplayPage() {
|
||||
|
||||
return (
|
||||
<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" }}
|
||||
>
|
||||
← 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 className="page-header" style={{ marginBottom: "2rem" }}>
|
||||
<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" }}
|
||||
>
|
||||
← 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>
|
||||
|
||||
{error ? (
|
||||
<div style={{ padding: "3rem", textAlign: "center", color: "var(--text-secondary)" }}>
|
||||
<p style={{ fontSize: "1.125rem", fontWeight: 600, color: "var(--brand-accent)" }}>Failed to load replay</p>
|
||||
<p style={{ marginTop: "0.5rem" }}>{error}</p>
|
||||
<div className="error-state">
|
||||
<p className="error-state__title">Failed to load replay</p>
|
||||
<p className="error-state__description">{error}</p>
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 3fr", gap: "2rem" }}>
|
||||
@@ -52,29 +50,29 @@ export function ReplayPage() {
|
||||
<div className="skeleton-box" style={{ height: "400px", borderRadius: "var(--radius-xl)", background: "var(--bg-surface)" }}></div>
|
||||
</div>
|
||||
) : steps.length === 0 ? (
|
||||
<div style={{ padding: "3rem", textAlign: "center", color: "var(--text-secondary)" }}>
|
||||
<p style={{ fontSize: "1.125rem", fontWeight: 600 }}>No replay steps found</p>
|
||||
<p style={{ marginTop: "0.5rem" }}>This conversation has no recorded checkpoints.</p>
|
||||
<div className="empty-state">
|
||||
<p className="empty-state__title">No replay steps found</p>
|
||||
<p className="empty-state__description">This conversation has no recorded checkpoints.</p>
|
||||
</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" }}>
|
||||
<div className="section-card" style={{ 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 }}>Thread ID</div>
|
||||
<div style={{ fontWeight: 600, fontSize: "0.8125rem", fontFamily: "monospace", wordBreak: "break-all" }}>{threadId}</div>
|
||||
<div className="stat-label">Thread ID</div>
|
||||
<div className="stat-value" style={{ fontSize: "0.8125rem", fontFamily: "monospace", wordBreak: "break-all" }}>{threadId}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Total Steps</div>
|
||||
<div style={{ fontSize: "0.9375rem" }}>{totalSteps}</div>
|
||||
<div className="stat-label">Total Steps</div>
|
||||
<div className="stat-value">{totalSteps}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Time Range</div>
|
||||
<div className="stat-label">Time Range</div>
|
||||
<div style={{ fontSize: "0.8125rem" }}>
|
||||
{steps[0]?.timestamp ? new Date(steps[0].timestamp).toLocaleString() : "N/A"}
|
||||
{" - "}
|
||||
{" \u2013 "}
|
||||
{steps[steps.length - 1]?.timestamp ? new Date(steps[steps.length - 1].timestamp).toLocaleString() : "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,7 +80,7 @@ export function ReplayPage() {
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div style={{ backgroundColor: "var(--bg-surface)", padding: "2rem", borderRadius: "var(--radius-xl)", border: "1px solid var(--border-light)" }}>
|
||||
<div className="section-card" style={{ padding: "2rem" }}>
|
||||
<ReplayTimeline steps={steps as any} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user