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

@@ -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",
},
};