feat(ui): implement premium beige design system and ux refinements
This commit is contained in:
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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} — 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,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user