feat(ui): implement premium beige design system and ux refinements
This commit is contained in:
1180
frontend/package-lock.json
generated
1180
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -23,46 +23,23 @@ export function ChatInput({ onSend, disabled }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={disabled ? "Waiting for response..." : "Type a message..."}
|
||||
disabled={disabled}
|
||||
style={styles.input}
|
||||
/>
|
||||
<button onClick={handleSubmit} disabled={disabled || !value.trim()} style={styles.button}>
|
||||
Send
|
||||
</button>
|
||||
<div className="chat-input-container">
|
||||
<div className="chat-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={disabled ? "Agent is working..." : "Message Smart Support..."}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<button className="chat-send-btn" onClick={handleSubmit} disabled={disabled || !value.trim()} aria-label="Send Message">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
container: {
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
padding: "12px 16px",
|
||||
borderTop: "1px solid #e0e0e0",
|
||||
background: "white",
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
padding: "10px 14px",
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "8px",
|
||||
fontSize: "14px",
|
||||
outline: "none",
|
||||
},
|
||||
button: {
|
||||
padding: "10px 20px",
|
||||
background: "#0066cc",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
fontSize: "14px",
|
||||
cursor: "pointer",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import type { ChatMessage } from "../types";
|
||||
|
||||
interface Props {
|
||||
@@ -13,70 +14,33 @@ export function ChatMessages({ messages }: Props) {
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div className="chat-messages-container">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
style={{
|
||||
...styles.message,
|
||||
...(msg.sender === "user" ? styles.userMessage : styles.agentMessage),
|
||||
}}
|
||||
>
|
||||
<div style={styles.header}>
|
||||
<span style={styles.sender}>
|
||||
{msg.sender === "user" ? "You" : msg.agent || "Agent"}
|
||||
</span>
|
||||
<div key={msg.id} className="chat-message-row">
|
||||
<div className={`avatar ${msg.sender === "user" ? "user" : "agent"}`}>
|
||||
{msg.sender === "user" ? "Me" : "AI"}
|
||||
</div>
|
||||
<div style={styles.content}>
|
||||
{msg.content}
|
||||
{msg.isStreaming && <span style={styles.cursor}>|</span>}
|
||||
<div className="message-body">
|
||||
<div className="message-sender">
|
||||
{msg.sender === "user" ? "You" : msg.agent || "Agent"}
|
||||
</div>
|
||||
<div className="message-content md-prose">
|
||||
<ReactMarkdown>{msg.content}</ReactMarkdown>
|
||||
{msg.isStreaming && <span className="cursor-blink">|</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{messages.length === 0 && (
|
||||
<div className="chat-message-row">
|
||||
<div className="avatar agent">AI</div>
|
||||
<div className="message-body">
|
||||
<div className="message-sender">Smart Support</div>
|
||||
<div className="message-content">Hello! How can I help you today?</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
container: {
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
padding: "16px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
},
|
||||
message: {
|
||||
maxWidth: "80%",
|
||||
padding: "10px 14px",
|
||||
borderRadius: "12px",
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
userMessage: {
|
||||
alignSelf: "flex-end",
|
||||
background: "#0066cc",
|
||||
color: "white",
|
||||
},
|
||||
agentMessage: {
|
||||
alignSelf: "flex-start",
|
||||
background: "#f0f0f0",
|
||||
color: "#333",
|
||||
},
|
||||
header: {
|
||||
marginBottom: "4px",
|
||||
},
|
||||
sender: {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
opacity: 0.8,
|
||||
},
|
||||
content: {
|
||||
fontSize: "14px",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
cursor: {
|
||||
animation: "blink 1s infinite",
|
||||
opacity: 0.7,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,75 +7,49 @@ interface Props {
|
||||
|
||||
export function InterruptPrompt({ interrupt, onRespond }: Props) {
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.header}>Action Requires Approval</div>
|
||||
<div style={styles.action}>
|
||||
<strong>Action:</strong> {interrupt.action}
|
||||
</div>
|
||||
{"message" in interrupt.params && interrupt.params.message != null && (
|
||||
<div style={styles.detail}>{String(interrupt.params.message)}</div>
|
||||
)}
|
||||
{"order_id" in interrupt.params && interrupt.params.order_id != null && (
|
||||
<div style={styles.detail}>
|
||||
<strong>Order:</strong> {String(interrupt.params.order_id)}
|
||||
<div className="action-card-container">
|
||||
<div className="action-card">
|
||||
<div className="action-card-header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--brand-accent)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
<h3 className="action-card-title">Action Requires Approval</h3>
|
||||
<div style={{ flex: 1 }} />
|
||||
<span className="action-card-badge">Pending</span>
|
||||
</div>
|
||||
|
||||
<div className="action-card-body">
|
||||
<div className="action-detail-row">
|
||||
<span className="action-detail-label">Action Name</span>
|
||||
<span className="action-detail-value" style={{ fontWeight: 600 }}>{interrupt.action}</span>
|
||||
</div>
|
||||
|
||||
{"message" in interrupt.params && interrupt.params.message != null && (
|
||||
<div className="action-detail-row">
|
||||
<span className="action-detail-label">Detail Message</span>
|
||||
<span className="action-detail-value">{String(interrupt.params.message)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{"order_id" in interrupt.params && interrupt.params.order_id != null && (
|
||||
<div className="action-detail-row">
|
||||
<span className="action-detail-label">Target Order ID</span>
|
||||
<span className="action-detail-value">{String(interrupt.params.order_id)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="action-card-footer">
|
||||
<button className="btn btn-secondary" onClick={() => onRespond(false)}>
|
||||
Reject & Escalate
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={() => onRespond(true)}>
|
||||
Approve Action
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={styles.buttons}>
|
||||
<button onClick={() => onRespond(true)} style={styles.approveBtn}>
|
||||
Approve
|
||||
</button>
|
||||
<button onClick={() => onRespond(false)} style={styles.rejectBtn}>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
container: {
|
||||
margin: "12px 16px",
|
||||
padding: "16px",
|
||||
border: "2px solid #ff9800",
|
||||
borderRadius: "12px",
|
||||
background: "#fff8e1",
|
||||
},
|
||||
header: {
|
||||
fontWeight: 700,
|
||||
fontSize: "14px",
|
||||
color: "#e65100",
|
||||
marginBottom: "8px",
|
||||
},
|
||||
action: {
|
||||
fontSize: "14px",
|
||||
marginBottom: "4px",
|
||||
},
|
||||
detail: {
|
||||
fontSize: "13px",
|
||||
color: "#555",
|
||||
marginBottom: "4px",
|
||||
},
|
||||
buttons: {
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
marginTop: "12px",
|
||||
},
|
||||
approveBtn: {
|
||||
padding: "8px 20px",
|
||||
background: "#4caf50",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
cursor: "pointer",
|
||||
fontWeight: 600,
|
||||
},
|
||||
rejectBtn: {
|
||||
padding: "8px 20px",
|
||||
background: "#f44336",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
cursor: "pointer",
|
||||
fontWeight: 600,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ import { NavBar } from "./NavBar";
|
||||
|
||||
export function Layout() {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
|
||||
<div className="app-layout">
|
||||
<NavBar />
|
||||
<main style={{ flex: 1, overflow: "auto" }}>
|
||||
<main className="app-main">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,64 +1,56 @@
|
||||
import { NavLink } from "react-router-dom";
|
||||
|
||||
const navLinks = [
|
||||
{ to: "/", label: "Chat", exact: true },
|
||||
{ to: "/replay", label: "Replay" },
|
||||
{ to: "/dashboard", label: "Dashboard" },
|
||||
{ to: "/review", label: "API Review" },
|
||||
{ to: "/dashboard", label: "Dashboard", icon: "grid" },
|
||||
{ to: "/", label: "Inbox", icon: "inbox" },
|
||||
{ to: "/replay", label: "Conversation Replay", icon: "play" },
|
||||
{ to: "/review", label: "Agents & Tools", icon: "cpu" },
|
||||
];
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
nav: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0",
|
||||
padding: "0 16px",
|
||||
borderBottom: "1px solid #e0e0e0",
|
||||
background: "#fff",
|
||||
height: "48px",
|
||||
boxShadow: "0 1px 4px rgba(0,0,0,0.06)",
|
||||
},
|
||||
brand: {
|
||||
fontWeight: 700,
|
||||
fontSize: "16px",
|
||||
color: "#1a1a1a",
|
||||
marginRight: "24px",
|
||||
textDecoration: "none",
|
||||
},
|
||||
link: {
|
||||
padding: "0 14px",
|
||||
height: "48px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontSize: "14px",
|
||||
color: "#555",
|
||||
textDecoration: "none",
|
||||
borderBottom: "2px solid transparent",
|
||||
transition: "color 0.15s, border-color 0.15s",
|
||||
},
|
||||
activeLink: {
|
||||
color: "#1976d2",
|
||||
borderBottom: "2px solid #1976d2",
|
||||
},
|
||||
};
|
||||
function getIcon(name: string) {
|
||||
switch (name) {
|
||||
case "grid": return <svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>;
|
||||
case "inbox": return <svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path></svg>;
|
||||
case "play": return <svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>;
|
||||
case "cpu": return <svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function NavBar() {
|
||||
return (
|
||||
<nav style={styles.nav}>
|
||||
<span style={styles.brand}>Smart Support</span>
|
||||
{navLinks.map(({ to, label }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={to === "/"}
|
||||
style={({ isActive }) => ({
|
||||
...styles.link,
|
||||
...(isActive ? styles.activeLink : {}),
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
<nav className="app-sidebar">
|
||||
<div className="brand-header">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="brand-logo-svg">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
|
||||
<line x1="9" y1="9" x2="9.01" y2="9"></line>
|
||||
<line x1="15" y1="9" x2="15.01" y2="9"></line>
|
||||
</svg>
|
||||
<span style={{ fontSize: "1.25rem", letterSpacing: "-0.03em" }}>Nexus AI</span>
|
||||
</div>
|
||||
<div className="nav-links" style={{ marginTop: "1rem" }}>
|
||||
{navLinks.map(({ to, label, icon }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={to === "/"}
|
||||
className={({ isActive }) => `nav-link ${isActive ? "active" : ""}`}
|
||||
style={{ display: "flex", gap: "12px", padding: "0.875rem 1rem", fontSize: "0.9375rem" }}
|
||||
>
|
||||
<span style={{ opacity: 0.7 }}>{getIcon(icon)}</span>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "12px", borderTop: "1px solid var(--border-light)", paddingTop: "1rem" }}>
|
||||
<div style={{ width: "36px", height: "36px", borderRadius: "50%", background: "var(--brand-primary)", color: "white", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: "bold" }}>A</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: "0.875rem" }}>Alex Thompson</div>
|
||||
<div style={{ fontSize: "0.75rem", color: "var(--text-secondary)" }}>Nexus Corp</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,31 +2,31 @@ import { useState } from "react";
|
||||
import type { ReplayStep } from "../api";
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
message: "#1976d2",
|
||||
token: "#388e3c",
|
||||
tool_call: "#f57c00",
|
||||
tool_result: "#7b1fa2",
|
||||
interrupt: "#d32f2f",
|
||||
interrupt_response: "#c2185b",
|
||||
error: "#c62828",
|
||||
message: "var(--brand-primary)",
|
||||
token: "#9CA3AF", // Soft gray
|
||||
tool_call: "#D97706", // Amber
|
||||
tool_result: "#059669", // Emerald
|
||||
interrupt: "#DC2626", // Red for wait
|
||||
interrupt_response: "#7C3AED", // Purple for human action
|
||||
error: "#991B1B", // Dark red
|
||||
};
|
||||
|
||||
function TypeBadge({ type }: { type: string }) {
|
||||
const color = TYPE_COLORS[type] ?? "#555";
|
||||
const color = TYPE_COLORS[type] ?? "var(--text-secondary)";
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
background: color,
|
||||
color: "#fff",
|
||||
fontSize: "11px",
|
||||
fontWeight: 600,
|
||||
padding: "2px 7px",
|
||||
borderRadius: "10px",
|
||||
fontSize: "0.65rem",
|
||||
fontWeight: 700,
|
||||
padding: "0.2rem 0.5rem",
|
||||
borderRadius: "99px",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.5px",
|
||||
letterSpacing: "0.05em",
|
||||
}}
|
||||
>
|
||||
{type}
|
||||
{type.replace("_", " ")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -38,9 +38,9 @@ function ReplayStepItem({ step }: { step: ReplayStep }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderLeft: "2px solid #e0e0e0",
|
||||
paddingLeft: "12px",
|
||||
marginBottom: "12px",
|
||||
borderLeft: "2px solid var(--border-light)",
|
||||
paddingLeft: "1.25rem",
|
||||
paddingBottom: "1.5rem",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
@@ -48,70 +48,91 @@ function ReplayStepItem({ step }: { step: ReplayStep }) {
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "-5px",
|
||||
top: "4px",
|
||||
top: "6px",
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
borderRadius: "50%",
|
||||
background: TYPE_COLORS[step.type] ?? "#555",
|
||||
background: TYPE_COLORS[step.type] ?? "var(--text-secondary)",
|
||||
boxShadow: `0 0 0 4px var(--bg-surface)`
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}>
|
||||
<span style={{ fontSize: "11px", color: "#888" }}>#{step.step}</span>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", marginBottom: "0.5rem" }}>
|
||||
<TypeBadge type={step.type} />
|
||||
{step.agent && (
|
||||
<span style={{ fontSize: "11px", color: "#666", fontStyle: "italic" }}>
|
||||
<span style={{ fontSize: "0.8125rem", color: "var(--text-primary)", fontWeight: 600 }}>
|
||||
{step.agent}
|
||||
</span>
|
||||
)}
|
||||
{step.tool && (
|
||||
<span style={{ fontSize: "11px", color: "#555" }}>
|
||||
tool: <strong>{step.tool}</strong>
|
||||
<span style={{ fontSize: "0.8125rem", color: "var(--text-secondary)", fontFamily: "monospace", backgroundColor: "var(--bg-app)", padding: "2px 6px", borderRadius: "4px" }}>
|
||||
{step.tool}()
|
||||
</span>
|
||||
)}
|
||||
<span style={{ fontSize: "11px", color: "#aaa", marginLeft: "auto" }}>
|
||||
{new Date(step.timestamp).toLocaleTimeString()}
|
||||
<span style={{ fontSize: "0.75rem", color: "var(--text-secondary)", marginLeft: "auto" }}>
|
||||
{new Date(step.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{step.content && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
color: "#333",
|
||||
background: "#f9f9f9",
|
||||
padding: "6px 10px",
|
||||
borderRadius: "4px",
|
||||
maxHeight: "80px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
style={
|
||||
["message", "interrupt", "interrupt_response"].includes(step.type)
|
||||
? {
|
||||
fontSize: "0.9375rem",
|
||||
color: "var(--text-primary)",
|
||||
background: step.type === "interrupt" ? "#FEF2F2" : (step.type === "interrupt_response" ? "#F5F3FF" : "var(--bg-app)"),
|
||||
border: step.type === "interrupt" ? "1px solid #FECACA" : (step.type === "interrupt_response" ? "1px solid #DDD6FE" : "1px solid var(--border-light)"),
|
||||
padding: "0.875rem 1rem",
|
||||
borderRadius: "var(--radius-md)",
|
||||
lineHeight: 1.5,
|
||||
whiteSpace: "pre-wrap"
|
||||
}
|
||||
: {
|
||||
fontSize: "0.8125rem",
|
||||
color: "var(--text-secondary)",
|
||||
padding: "0.25rem 0",
|
||||
fontStyle: "italic",
|
||||
lineHeight: 1.4
|
||||
}
|
||||
}
|
||||
>
|
||||
{step.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasDetails && (
|
||||
<button
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "#1976d2",
|
||||
color: "var(--text-secondary)",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
padding: "2px 0",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
padding: "0.5rem 0 0 0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem"
|
||||
}}
|
||||
>
|
||||
{expanded ? "Hide details" : "Show details"}
|
||||
{expanded ? "▼ Hide JSON Payload" : "▶ View JSON Payload"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{expanded && hasDetails && (
|
||||
<pre
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
background: "#f3f3f3",
|
||||
padding: "8px",
|
||||
borderRadius: "4px",
|
||||
fontSize: "0.75rem",
|
||||
background: "var(--text-primary)",
|
||||
color: "white",
|
||||
padding: "1rem",
|
||||
borderRadius: "var(--radius-md)",
|
||||
overflow: "auto",
|
||||
maxHeight: "200px",
|
||||
maxHeight: "250px",
|
||||
marginTop: "0.5rem",
|
||||
fontFamily: "monospace"
|
||||
}}
|
||||
>
|
||||
{JSON.stringify({ params: step.params, result: step.result }, null, 2)}
|
||||
@@ -126,19 +147,14 @@ interface ReplayTimelineProps {
|
||||
}
|
||||
|
||||
export function ReplayTimeline({ steps }: ReplayTimelineProps) {
|
||||
if (steps.length === 0) {
|
||||
return (
|
||||
<div style={{ color: "#888", fontSize: "14px", padding: "16px 0" }}>
|
||||
No steps recorded.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!steps || steps.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={{ padding: "8px 0" }}>
|
||||
<div style={{ marginLeft: "4px" }}>
|
||||
{steps.map((step) => (
|
||||
<ReplayStepItem key={step.step} step={step} />
|
||||
))}
|
||||
<div style={{ borderLeft: "2px dashed var(--border-light)", height: "20px", marginLeft: "0px", opacity: 0.5 }}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
678
frontend/src/index.css
Normal file
678
frontend/src/index.css
Normal file
@@ -0,0 +1,678 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
/* Rich Warm Beige Theme based on Design Mockup */
|
||||
--bg-app: #F4EFE7; /* Main app background (Sidebar & Main area) */
|
||||
--bg-surface: #EBE4D8; /* Slightly darker for cards */
|
||||
--bg-surface-inner: #F6F2EC; /* Lighter inner container */
|
||||
--bg-hover: #E1D9CC; /* Hover state for sidebar and buttons */
|
||||
|
||||
--text-primary: #1C1917; /* Slate dark/brownish */
|
||||
--text-secondary: #5C554D; /* Muted stone */
|
||||
|
||||
--border-light: #D5CCC0; /* Warm border */
|
||||
--border-focus: #B6AAA0;
|
||||
|
||||
--brand-primary: #3B342D; /* Dark brown/grey for buttons */
|
||||
--brand-hover: #26211C;
|
||||
--brand-accent: #3B342D;
|
||||
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.02);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.04);
|
||||
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.06);
|
||||
|
||||
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 24px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-sans);
|
||||
background-color: #DBD2C6; /* Subtle deeper tone */
|
||||
color: var(--text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Application Shell Layout */
|
||||
.app-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-surface);
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
padding: 1.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.app-layout {
|
||||
height: calc(100vh - 3rem);
|
||||
width: 100%;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.02);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar layout */
|
||||
.app-sidebar {
|
||||
width: 260px;
|
||||
background-color: transparent; /* Makes it blend into the main background */
|
||||
border-right: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 1rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.brand-header {
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2rem;
|
||||
padding-left: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-app);
|
||||
}
|
||||
|
||||
/* --- Chat Interface (Option B) --- */
|
||||
.chat-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background-color: transparent;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chat-messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem 0;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.chat-message-row {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
padding: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.chat-message-row {
|
||||
padding: 1rem 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-row:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.avatar.user {
|
||||
background-color: var(--border-light);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.avatar.agent {
|
||||
background: linear-gradient(135deg, var(--brand-primary), #334155);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.375rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.cursor-blink {
|
||||
animation: blink 1s infinite alternate;
|
||||
font-weight: 700;
|
||||
color: var(--brand-accent);
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
/* Markdown Prose Styles */
|
||||
.md-prose p {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
.md-prose p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.md-prose strong {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.md-prose ul, .md-prose ol {
|
||||
margin: 0.25rem 0 0.75rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.md-prose li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.md-prose pre {
|
||||
background-color: var(--bg-hover);
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius-md);
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border-light);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.md-prose code {
|
||||
font-family: monospace;
|
||||
background-color: var(--bg-hover);
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.md-prose pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Chat Input Bar */
|
||||
.chat-input-container {
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(to top, var(--bg-app) 80%, transparent);
|
||||
}
|
||||
|
||||
.chat-input-wrapper {
|
||||
margin: 0 1rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow-md);
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.chat-input-wrapper {
|
||||
margin: 0 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-wrapper:focus-within {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.chat-input-wrapper input {
|
||||
flex: 1;
|
||||
padding: 1rem 1.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-input-wrapper input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chat-send-btn {
|
||||
margin-right: 0.75rem;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--brand-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chat-send-btn:hover:not(:disabled) {
|
||||
background-color: var(--brand-hover);
|
||||
}
|
||||
|
||||
.chat-send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background-color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* --- Human in the loop Action Card --- */
|
||||
.action-card-container {
|
||||
margin: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.action-card-container {
|
||||
margin: 1.5rem 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.action-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background-color: var(--brand-accent);
|
||||
}
|
||||
|
||||
.action-card-header {
|
||||
padding: 1.25rem 1.5rem 0.75rem 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
border-bottom: 1px solid var(--bg-hover);
|
||||
}
|
||||
|
||||
.action-card-title {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-card-badge {
|
||||
background-color: #FEF2F2;
|
||||
color: #B91C1C;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.action-card-body {
|
||||
padding: 1.25rem 1.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-detail-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.action-detail-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.action-detail-value {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
background-color: var(--bg-hover);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-light);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.action-card-footer {
|
||||
padding: 1.25rem 1.75rem;
|
||||
background-color: var(--bg-hover);
|
||||
border-top: 1px solid var(--border-light);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--brand-accent);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #C2410C;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: transparent;
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-focus);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* --- Agent Card Grid (Option 2) --- */
|
||||
.page-container {
|
||||
padding: 2.5rem;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 0.5rem 0;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.import-form {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 2rem;
|
||||
background-color: var(--bg-surface);
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.import-input {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.9375rem;
|
||||
font-family: inherit;
|
||||
background-color: var(--bg-app);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.import-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
/* Agent Grid */
|
||||
.agent-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.agent-grid-card {
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.agent-grid-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.agent-card-header-bg {
|
||||
padding: 1.5rem 1.5rem 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--bg-hover);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.agent-avatar-lg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, var(--text-primary), var(--text-secondary));
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.agent-card-meta h3 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.agent-card-meta span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--brand-primary);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.agent-tools-list {
|
||||
padding: 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
background-color: var(--bg-surface-inner);
|
||||
border-radius: 20px;
|
||||
margin: 0 1.25rem 1.25rem 1.25rem;
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.tool-pill-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.tool-pill-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.tool-pill-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-method-badge {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 99px;
|
||||
background-color: var(--text-primary);
|
||||
color: white;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.tool-path-text {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tool-summary-text {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.tool-pill-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-select, .tool-input {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border-focus);
|
||||
border-radius: 6px;
|
||||
padding: 0.375rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tool-select:focus, .tool-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* --- Skeleton Loading Animation --- */
|
||||
@keyframes pulse-skeleton {
|
||||
0% { opacity: 0.5; background-color: var(--bg-hover); }
|
||||
50% { opacity: 0.8; background-color: var(--border-light); }
|
||||
100% { opacity: 0.5; background-color: var(--bg-hover); }
|
||||
}
|
||||
|
||||
.skeleton-box {
|
||||
animation: pulse-skeleton 1.5s infinite ease-in-out;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
height: 1rem;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
animation: pulse-skeleton 1.5s infinite ease-in-out;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -7,11 +7,11 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/ws": {
|
||||
target: "http://localhost:8000",
|
||||
target: "http://localhost:8001",
|
||||
ws: true,
|
||||
},
|
||||
"/api": {
|
||||
target: "http://localhost:8000",
|
||||
target: "http://localhost:8001",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user