update
This commit is contained in:
1 parent
65e85135c1
commit
6f1de8047e
64 files changed
+1269
-515
No files matched your search
@@ -37,11 +37,13 @@ export function ActivityFeed({ activity, agents, limit = 15 }: { activity: Activ
|
||||
);
|
||||
}
|
||||
|
||||
const agentMap = useMemo(() => new Map(agents.map((a) => [a.id, a])), [agents]);
|
||||
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
{recentActivity.map((item, index) => {
|
||||
const Icon = actionIcons[item.action] ?? Lightning;
|
||||
const agent = agents.find((a) => a.id === item.actor);
|
||||
const agent = agentMap.get(item.actor);
|
||||
const actorName = agent?.name ?? (item.actor === "board" ? "Board" : item.actor.slice(0, 8));
|
||||
|
||||
return (
|
||||
|
||||
@@ -79,8 +79,8 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
{incidents.map((incident) => {
|
||||
const isHardStop = incident.thresholdType === "hard_stop";
|
||||
const borderColor = isHardStop ? "border-[color:var(--destructive)]" : "border-[color:var(--destructive)]";
|
||||
const bgColor = isHardStop ? "bg-red-100 dark:bg-red-900/50" : "bg-yellow-100 dark:bg-yellow-900/50";
|
||||
const textColor = isHardStop ? "text-destructive" : "text-yellow-600 dark:text-yellow-400";
|
||||
const bgColor = isHardStop ? "bg-destructive-muted" : "bg-warning-muted";
|
||||
const textColor = isHardStop ? "text-destructive" : "text-warning";
|
||||
const pct = incident.amountLimit > 0 ? Math.round((incident.amountObserved / incident.amountLimit) * 100) : 0;
|
||||
|
||||
return (
|
||||
@@ -132,14 +132,14 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="rounded-lg border border-[color:var(--destructive)] border-opacity-30 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3"
|
||||
className="rounded-lg border border-[color:var(--destructive)] border-opacity-30 bg-warning-muted px-4 py-3"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 shrink-0">
|
||||
<Warning size={18} weight="bold" className="text-yellow-600 dark:text-yellow-400" />
|
||||
<Warning size={18} weight="bold" className="text-warning" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[13px] font-semibold text-yellow-600 dark:text-yellow-400">
|
||||
<div className="text-[13px] font-semibold text-warning">
|
||||
Budget Alert
|
||||
</div>
|
||||
<div className="mt-0.5 text-[13px] text-muted-foreground">
|
||||
@@ -152,7 +152,7 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
className={`whitespace-nowrap text-[11px] font-[family-name:var(--font-mono)] ${
|
||||
agent.pct >= 100
|
||||
? "text-destructive"
|
||||
: "text-yellow-600 dark:text-yellow-400"
|
||||
: "text-warning"
|
||||
}`}
|
||||
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
|
||||
@@ -84,7 +84,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
<h2 className="text-[28px] font-bold tracking-[-0.02em] text-foreground">{companyName}</h2>
|
||||
<div className="mt-2 flex items-center gap-3 text-[13px] tracking-[0.005em] text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-green-600 dark:bg-green-400" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-success" />
|
||||
{activeAgents.length} {activeAgents.length === 1 ? "agent" : "agents"} active
|
||||
</span>
|
||||
<span className="h-3 w-px bg-[color:var(--border)]" />
|
||||
@@ -103,7 +103,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
<BudgetAlertBanner agents={agents} onNavigate={onNavigate} companyId={companyId} />
|
||||
|
||||
{/* Key Metrics — 4 cards */}
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4">
|
||||
<MetricCard
|
||||
label={t("dash.activeTasks")}
|
||||
value={activeTasks.length}
|
||||
@@ -142,8 +142,8 @@ export function Dashboard(props: DashboardProps) {
|
||||
{/* Needs Attention — moved to top, only shows when items exist */}
|
||||
{hasAttentionItems ? (
|
||||
<section>
|
||||
<div className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-yellow-600 dark:text-yellow-400">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-yellow-600 dark:bg-yellow-400" />
|
||||
<div className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-warning">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-warning" />
|
||||
{t("dash.needsAttentionSection")}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -157,9 +157,9 @@ export function Dashboard(props: DashboardProps) {
|
||||
transition={{ duration: 0.22, delay: i * 0.04, ease: [0.16, 1, 0.3, 1] }}
|
||||
type="button"
|
||||
onClick={() => onNavigate("approvals", approval.id)}
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-warning-muted px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<ShieldCheck size={18} className="shrink-0 text-yellow-600 dark:text-yellow-400" />
|
||||
<ShieldCheck size={18} className="shrink-0 text-warning" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{approval.payloadSummary}</div>
|
||||
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||
@@ -171,14 +171,17 @@ export function Dashboard(props: DashboardProps) {
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
{failedRuns.slice(0, 3).map((run) => {
|
||||
{failedRuns.slice(0, 3).map((run, i) => {
|
||||
const agent = agentMap.get(run.agentId);
|
||||
return (
|
||||
<button
|
||||
<motion.button
|
||||
key={run.id}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.22, delay: (pendingApprovals.length + i) * 0.04, ease: [0.16, 1, 0.3, 1] }}
|
||||
type="button"
|
||||
onClick={() => onNavigate("runs", run.id)}
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-red-100 dark:bg-red-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-destructive-muted px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<XCircle size={18} className="shrink-0 text-destructive" />
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -188,19 +191,22 @@ export function Dashboard(props: DashboardProps) {
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
{urgentMessages.slice(0, 3).map((msg) => {
|
||||
{urgentMessages.slice(0, 3).map((msg, i) => {
|
||||
const sender = agentMap.get(msg.fromAgentId);
|
||||
return (
|
||||
<button
|
||||
<motion.button
|
||||
key={msg.id}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.22, delay: (pendingApprovals.length + failedRuns.length + i) * 0.04, ease: [0.16, 1, 0.3, 1] }}
|
||||
type="button"
|
||||
onClick={() => onNavigate("communication", msg.id)}
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-warning-muted px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<Warning size={18} className="shrink-0 text-yellow-600 dark:text-yellow-400" />
|
||||
<Warning size={18} className="shrink-0 text-warning" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{msg.subject}</div>
|
||||
<div className="mt-0.5 text-[11px] text-muted-foreground">
|
||||
@@ -208,7 +214,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -235,11 +241,11 @@ export function Dashboard(props: DashboardProps) {
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 px-4 py-3 text-left transition hover:bg-muted"
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
<Robot size={16} className="text-green-600 dark:text-green-400" />
|
||||
<Robot size={16} className="text-success" />
|
||||
{run.status === "running" ? (
|
||||
<span className="absolute -right-0.5 -top-0.5 h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-600 dark:bg-green-400 opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-green-600 dark:bg-green-400" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-success opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-success" />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -11,10 +11,10 @@ interface KanbanProps {
|
||||
|
||||
const KANBAN_COLUMNS: Array<{ status: TaskRecord["status"]; label: string; tone: string; dotColor: string }> = [
|
||||
{ status: "backlog", label: "Backlog", tone: "text-muted-foreground", dotColor: "bg-[color:var(--muted-foreground)]" },
|
||||
{ status: "todo", label: "Todo", tone: "text-yellow-600 dark:text-yellow-400", dotColor: "bg-yellow-600 dark:bg-yellow-400" },
|
||||
{ status: "todo", label: "Todo", tone: "text-warning", dotColor: "bg-warning" },
|
||||
{ status: "in_progress", label: "In Progress", tone: "text-primary", dotColor: "bg-primary" },
|
||||
{ status: "in_review", label: "In Review", tone: "text-yellow-600 dark:text-yellow-400", dotColor: "bg-yellow-600 dark:bg-yellow-400" },
|
||||
{ status: "done", label: "Done", tone: "text-green-600 dark:text-green-400", dotColor: "bg-green-600 dark:bg-green-400" },
|
||||
{ status: "in_review", label: "In Review", tone: "text-warning", dotColor: "bg-warning" },
|
||||
{ status: "done", label: "Done", tone: "text-success", dotColor: "bg-success" },
|
||||
{ status: "blocked", label: "Blocked", tone: "text-destructive", dotColor: "bg-destructive" },
|
||||
];
|
||||
|
||||
@@ -57,7 +57,7 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
{KANBAN_COLUMNS.map((col) => {
|
||||
const colTasks = tasksByStatus[col.status] ?? [];
|
||||
return (
|
||||
<div key={col.status} className="flex w-[220px] min-w-[180px] shrink-0 flex-col">
|
||||
<div key={col.status} className="flex w-[220px] min-w-[160px] shrink-0 flex-col lg:w-[240px]">
|
||||
<div className="mb-2 flex items-center gap-2 px-1">
|
||||
<span className={`h-2.5 w-2.5 rounded-full ${col.dotColor}`} aria-hidden="true" />
|
||||
<span className={`text-[11px] font-semibold uppercase tracking-[0.08em] ${col.tone}`}>{col.label}</span>
|
||||
@@ -88,14 +88,14 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
<span className="text-[10px] italic text-muted-foreground">Unassigned</span>
|
||||
)}
|
||||
{isRunning ? (
|
||||
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-green-600 dark:text-green-400">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-green-600 dark:bg-green-400" aria-hidden="true" />
|
||||
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-success">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-success" aria-hidden="true" />
|
||||
Running
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{task.priority === "critical" || task.priority === "high" ? (
|
||||
<div className={`mt-1 text-[9px] font-bold uppercase tracking-[0.12em] ${task.priority === "critical" ? "text-destructive" : "text-yellow-600 dark:text-yellow-400"}`}>
|
||||
<div className={`mt-1 text-[9px] font-bold uppercase tracking-[0.12em] ${task.priority === "critical" ? "text-destructive" : "text-warning"}`}>
|
||||
{task.priority}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -17,23 +17,23 @@ export function MetricCard({
|
||||
}) {
|
||||
const toneMap = {
|
||||
default: "text-foreground",
|
||||
success: "text-green-600 dark:text-green-400",
|
||||
warn: "text-yellow-600 dark:text-yellow-400",
|
||||
success: "text-success",
|
||||
warn: "text-warning",
|
||||
danger: "text-destructive",
|
||||
accent: "text-primary",
|
||||
};
|
||||
const bgMap = {
|
||||
default: "bg-muted",
|
||||
success: "bg-green-100 dark:bg-green-900/50",
|
||||
warn: "bg-yellow-100 dark:bg-yellow-900/50",
|
||||
danger: "bg-red-100 dark:bg-red-900/50",
|
||||
success: "bg-success-muted",
|
||||
warn: "bg-warning-muted",
|
||||
danger: "bg-destructive-muted",
|
||||
accent: "bg-accent",
|
||||
};
|
||||
|
||||
const borderMap = {
|
||||
default: "border-l-transparent",
|
||||
success: "border-l-[color:var(--primary)]",
|
||||
warn: "border-l-[color:var(--destructive)]",
|
||||
success: "border-l-[color:var(--success)]",
|
||||
warn: "border-l-[color:var(--warning)]",
|
||||
danger: "border-l-[color:var(--destructive)]",
|
||||
accent: "border-l-[color:var(--primary)]",
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export function MetricCard({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex items-center gap-4 rounded-lg border border-l-[3px] border-border bg-card px-4 py-4 text-left transition duration-150 hover:border-border hover:shadow-sm ${borderMap[tone]}`}
|
||||
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex items-center gap-4 rounded-lg border border-l-[3px] border-border bg-card px-4 py-4 text-left transition duration-200 hover:shadow-md hover:-translate-y-0.5 ${borderMap[tone]}`}
|
||||
>
|
||||
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-lg ${bgMap[tone]}`}>
|
||||
<Icon size={24} weight="fill" className={toneMap[tone]} />
|
||||
|
||||
@@ -41,9 +41,9 @@ export function RecoveryBanner() {
|
||||
const handleDismissAll = () => setDismissed(true);
|
||||
|
||||
return (
|
||||
<div className="bg-yellow-100 dark:bg-yellow-900/50 border border-[color:var(--destructive)]/30 rounded-lg px-4 py-4 mb-4">
|
||||
<div className="bg-warning-muted border border-[color:var(--destructive)]/30 rounded-lg px-4 py-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2 text-yellow-600 dark:text-yellow-400 font-medium">
|
||||
<div className="flex items-center gap-2 text-warning font-medium">
|
||||
<Warning size={18} weight="bold" />
|
||||
<span>{runs.length} interrupted task(s) detected from previous session</span>
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@ export function RecoveryBanner() {
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleRetry(run)}
|
||||
className="flex items-center gap-1 text-green-600 dark:text-green-400 hover:text-green-600 dark:text-green-400 text-xs transition-colors"
|
||||
className="flex items-center gap-1 text-success hover:text-success text-xs transition-colors"
|
||||
>
|
||||
<ArrowClockwise size={14} /> Retry
|
||||
</button>
|
||||
|
||||
@@ -17,8 +17,8 @@ interface BudgetRow {
|
||||
|
||||
function getBarColor(pct: number): string {
|
||||
if (pct >= 100) return "bg-destructive";
|
||||
if (pct >= 80) return "bg-yellow-600 dark:bg-yellow-400";
|
||||
return "bg-green-600 dark:bg-green-400";
|
||||
if (pct >= 80) return "bg-warning";
|
||||
return "bg-success";
|
||||
}
|
||||
|
||||
function scopeLabel(scopeType: string): string {
|
||||
@@ -119,7 +119,7 @@ export function BudgetUtilization({ companyId, agents }: BudgetUtilizationProps)
|
||||
/>
|
||||
{/* 80% warning marker */}
|
||||
<div
|
||||
className="absolute top-0 h-full w-px bg-yellow-600 dark:bg-yellow-400 opacity-50"
|
||||
className="absolute top-0 h-full w-px bg-warning opacity-50"
|
||||
style={{ left: "80%" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user