refactor(01-06): decompose inbox.tsx into focused sub-components

- inbox.tsx (489 lines) -> 4 files: Inbox, StandupView, PerformanceView, inbox-helpers
- All sub-component files under 300 lines
- Backward-compatible re-export barrel preserves all 3 named exports (Inbox, StandupView, PerformanceView)
- App.tsx imports unchanged and resolving correctly
This commit is contained in:
ZacharyZhang-NY committed 2026-03-22 02:56:42 -04:00
1 parent 36b0d39102
commit d2ed17a81b
5 files changed
+506 -489

No files matched your search

+4 -489
View File
@@ -1,489 +1,4 @@
import { useMemo, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Bell,
ShieldCheck,
Warning,
XCircle,
CurrencyCircleDollar,
ArrowRight,
CheckCircle,
Clock,
Eye,
Robot,
Heartbeat,
Target,
ChatCircle,
Tray,
} from "@phosphor-icons/react";
import type { InboxItem, InboxItemType, SectionId, StandupReport, CompanyMetrics } from "@shared/types";
import { formatMoney, timeAgo, getAgentInitials, formatDuration } from "../lib/formatters";
import { useT } from "../i18n";
const typeConfig: Record<InboxItemType, { icon: React.ElementType; color: string; bg: string; label: string }> = {
pending_approval: { icon: ShieldCheck, color: "var(--warn)", bg: "var(--warn-soft)", label: "Approval" },
failed_run: { icon: XCircle, color: "var(--danger)", bg: "var(--danger-soft)", label: "Failed Run" },
budget_warning: { icon: CurrencyCircleDollar, color: "var(--warn)", bg: "var(--warn-soft)", label: "Budget" },
blocked_task: { icon: Warning, color: "var(--danger)", bg: "var(--danger-soft)", label: "Blocked" },
review_needed: { icon: Eye, color: "var(--accent)", bg: "var(--accent-soft)", label: "Review" },
agent_message: { icon: ChatCircle, color: "var(--accent)", bg: "var(--accent-soft)", label: "Message" },
};
const severityColors: Record<string, string> = {
critical: "border-l-[color:var(--danger)]",
high: "border-l-[color:var(--warn)]",
medium: "border-l-[color:var(--accent)]",
low: "border-l-[color:var(--line)]",
};
interface InboxProps {
items: InboxItem[];
onNavigate: (section: SectionId, entityId?: string) => void;
}
export function Inbox({ items, onNavigate }: InboxProps) {
const t = useT();
const [filter, setFilter] = useState<InboxItemType | "all">("all");
const filteredItems = useMemo(() => {
if (filter === "all") return items;
return items.filter(i => i.type === filter);
}, [items, filter]);
const counts = useMemo(() => ({
all: items.length,
pending_approval: items.filter(i => i.type === "pending_approval").length,
failed_run: items.filter(i => i.type === "failed_run").length,
budget_warning: items.filter(i => i.type === "budget_warning").length,
blocked_task: items.filter(i => i.type === "blocked_task").length,
review_needed: items.filter(i => i.type === "review_needed").length,
agent_message: items.filter(i => i.type === "agent_message").length,
}), [items]);
const handleItemClick = (item: InboxItem) => {
const sectionMap: Record<string, SectionId> = {
approval: "approvals",
run: "runs",
agent: "agents",
task: "tasks",
message: "communication",
};
const section = sectionMap[item.entityType] ?? "overview";
onNavigate(section, item.entityId);
};
const filterButtons: Array<{ key: InboxItemType | "all"; label: string; count: number }> = [
{ key: "all", label: "All", count: counts.all },
{ key: "pending_approval", label: "Approvals", count: counts.pending_approval },
{ key: "review_needed", label: "Reviews", count: counts.review_needed },
{ key: "failed_run", label: "Failed", count: counts.failed_run },
{ key: "blocked_task", label: "Blocked", count: counts.blocked_task },
{ key: "budget_warning", label: "Budget", count: counts.budget_warning },
{ key: "agent_message", label: "Messages", count: counts.agent_message },
];
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
<Bell size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("inbox.title")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention
</p>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-1.5">
{filterButtons.map(btn => (
<button
key={btn.key}
type="button"
onClick={() => setFilter(btn.key)}
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[11px] font-medium transition ${
filter === btn.key
? "bg-[color:var(--accent)] text-white"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"
}`}
>
{btn.label}
{btn.count > 0 ? (
<span className={`rounded-full px-1.5 py-0.5 text-[9px] font-bold ${
filter === btn.key ? "bg-white/20 text-white" : "bg-[color:var(--line)] text-[color:var(--muted)]"
}`}>
{btn.count}
</span>
) : null}
</button>
))}
</div>
{filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Tray size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("inbox.allClear")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("inbox.noItemsNow")}</div>
</div>
) : (
<div className="space-y-1.5">
<AnimatePresence>
{filteredItems.map((item) => {
const config = typeConfig[item.type];
const Icon = config.icon;
return (
<motion.div
key={item.id}
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 20 }}
className={`group flex items-center gap-3 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 transition hover:border-[color:var(--line-strong)] hover:shadow-sm ${severityColors[item.severity] ?? ""}`}
>
<div
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]"
style={{ backgroundColor: config.bg }}
>
<Icon size={18} weight="fill" style={{ color: config.color }} />
</div>
<button
type="button"
onClick={() => handleItemClick(item)}
className="min-w-0 flex-1 text-left"
>
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{item.title}</span>
<span
className="shrink-0 rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]"
style={{ backgroundColor: config.bg, color: config.color }}
>
{config.label}
</span>
</div>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
{item.agentName ? (
<span className="flex items-center gap-1">
<Robot size={10} />
{item.agentName}
</span>
) : null}
<span>{item.subtitle.slice(0, 80)}</span>
<span className="shrink-0">{timeAgo(item.createdAt)}</span>
</div>
</button>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)] opacity-0 transition group-hover:opacity-100" />
</motion.div>
);
})}
</AnimatePresence>
</div>
)}
</div>
);
}
interface StandupProps {
report: StandupReport | null;
onNavigate: (section: SectionId, entityId?: string) => void;
}
export function StandupView({ report, onNavigate }: StandupProps) {
const t = useT();
if (!report) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Clock size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("standup.noData")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("standup.noDataDesc")}</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
<Heartbeat size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("standup.dailyStandup")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
Generated {new Date(report.generatedAt).toLocaleString()}
</p>
</div>
</div>
<div className="text-[14px] font-medium text-[color:var(--text)]">{report.summary}</div>
<div className="mt-3 flex gap-4">
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--success)]">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.productive")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--danger)]">{report.blockers.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.blockers")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--warn)]">{report.pendingApprovals}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.pendingApprovals")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className={`text-[18px] font-bold ${report.budgetUtilization > 80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
{report.budgetUtilization}%
</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.budgetUsed")}</div>
</div>
</div>
</div>
{report.companyHighlights.length > 0 ? (
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.highlights")}</div>
<div className="space-y-1.5">
{report.companyHighlights.map((h, i) => (
<div key={i} className="flex items-center gap-2 rounded-[6px] bg-[color:var(--success-soft)] px-3 py-2 text-[12px] text-[color:var(--success)]">
<CheckCircle size={14} weight="fill" />
{h}
</div>
))}
</div>
</section>
) : null}
{report.blockers.length > 0 ? (
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--danger)]">Blockers</div>
<div className="space-y-1.5">
{report.blockers.map((b, i) => (
<div key={i} className="flex items-center gap-2 rounded-[6px] bg-[color:var(--danger-soft)] px-3 py-2 text-[12px] text-[color:var(--danger)]">
<Warning size={14} weight="fill" />
{b}
</div>
))}
</div>
</section>
) : null}
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.agentStatus")}</div>
<div className="space-y-2">
{report.agentReports.map((ar) => (
<button
key={ar.agentId}
type="button"
onClick={() => onNavigate("agents", ar.agentId)}
className="flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
>
<div className="relative">
<div className={`flex h-10 w-10 items-center justify-center rounded-full text-[12px] font-bold ${
ar.status === "running" ? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: ar.status === "error" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}>
{getAgentInitials(ar.agentName)}
</div>
{ar.status === "running" ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[color:var(--success)]" />
</span>
) : null}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold text-[color:var(--text)]">{ar.agentName}</span>
<span className="text-[10px] text-[color:var(--muted)]">{ar.role}</span>
</div>
<div className="mt-1 flex flex-wrap gap-3 text-[11px] text-[color:var(--muted)]">
{ar.tasksInProgress > 0 ? <span className="text-[color:var(--accent)]">{ar.tasksInProgress} in progress</span> : null}
{ar.tasksCompleted24h > 0 ? <span className="text-[color:var(--success)]">{ar.tasksCompleted24h} done</span> : null}
{ar.tasksBlocked > 0 ? <span className="text-[color:var(--danger)]">{ar.tasksBlocked} blocked</span> : null}
{ar.costLast24h > 0 ? <span>${ar.costLast24h.toFixed(2)}</span> : null}
</div>
{ar.highlights.length > 0 ? (
<div className="mt-1 text-[10px] text-[color:var(--muted)]">{ar.highlights.join(" · ")}</div>
) : null}
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
))}
</div>
</section>
</div>
);
}
interface PerformanceProps {
metrics: CompanyMetrics | null;
onNavigate: (section: SectionId, entityId?: string) => void;
}
export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
const t = useT();
if (!metrics) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Target size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("perf.noMetrics")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("perf.noMetricsDesc")}</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
<CompactMetric
label={t("perf.successRate")}
value={`${metrics.overallSuccessRate}%`}
tone={metrics.overallSuccessRate >= 80 ? "success" : metrics.overallSuccessRate >= 50 ? "warn" : "danger"}
/>
<CompactMetric
label={t("perf.tasksDone")}
value={metrics.completedTasks}
tone="success"
subtitle={`of ${metrics.totalTasks} total`}
/>
<CompactMetric
label={t("perf.throughput")}
value={`${metrics.throughputTasksPerDay.toFixed(1)}`}
tone="accent"
subtitle={t("perf.tasksPerDay")}
/>
<CompactMetric
label={t("perf.avgCostTask")}
value={formatMoney(metrics.avgCostPerTask)}
tone="default"
/>
</div>
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Run Activity (30d)</div>
<div className="flex items-center gap-6">
<div className="flex-1">
<RunProgressBar
succeeded={metrics.successfulRuns}
failed={metrics.failedRuns}
total={metrics.totalRuns}
/>
</div>
<div className="flex gap-4 text-[12px]">
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" />
<span className="text-[color:var(--muted)]">{metrics.successfulRuns} succeeded</span>
</span>
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-[color:var(--danger)]" />
<span className="text-[color:var(--muted)]">{metrics.failedRuns} failed</span>
</span>
</div>
</div>
</div>
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agent Performance</div>
<div className="overflow-x-auto rounded-[10px] border border-[color:var(--line)]">
<table className="w-full text-left">
<thead>
<tr className="border-b border-[color:var(--line)] bg-[color:var(--panel-soft)]">
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Agent</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Tasks Done</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Success %</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Runs</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Avg Duration</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Cost</th>
</tr>
</thead>
<tbody>
{metrics.agentMetrics
.sort((a, b) => b.tasksCompleted - a.tasksCompleted)
.map((am) => (
<tr
key={am.agentId}
className="border-b border-[color:var(--line)] bg-[color:var(--panel)] transition hover:bg-[color:var(--panel-soft)] cursor-pointer"
onClick={() => onNavigate("agents", am.agentId)}
>
<td className="px-4 py-2.5">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-[color:var(--panel-soft)] text-[10px] font-bold text-[color:var(--muted-strong)]">
{getAgentInitials(am.agentName)}
</div>
<span className="text-[12px] font-medium text-[color:var(--text)]">{am.agentName}</span>
</div>
</td>
<td className="px-3 py-2.5 text-right text-[12px] font-semibold text-[color:var(--success)]">{am.tasksCompleted}</td>
<td className="px-3 py-2.5 text-right">
<span className={`text-[12px] font-semibold ${
am.successRate >= 80 ? "text-[color:var(--success)]"
: am.successRate >= 50 ? "text-[color:var(--warn)]"
: "text-[color:var(--danger)]"
}`}>
{am.successRate}%
</span>
</td>
<td className="px-3 py-2.5 text-right text-[12px] text-[color:var(--muted)]">{am.runsTotal}</td>
<td className="px-3 py-2.5 text-right text-[12px] text-[color:var(--muted)]">{formatDuration(am.avgRunDurationSec)}</td>
<td className="px-3 py-2.5 text-right text-[12px] text-[color:var(--muted)]">{formatMoney(am.totalCostUsd)}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
);
}
function CompactMetric({ label, value, tone = "default", subtitle }: {
label: string;
value: string | number;
tone?: "default" | "success" | "warn" | "danger" | "accent";
subtitle?: string;
}) {
const toneMap = {
default: "text-[color:var(--text)]",
success: "text-[color:var(--success)]",
warn: "text-[color:var(--warn)]",
danger: "text-[color:var(--danger)]",
accent: "text-[color:var(--accent)]",
};
return (
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
<div className={`text-[22px] font-bold ${toneMap[tone]}`}>{value}</div>
{subtitle ? <div className="text-[10px] text-[color:var(--muted)]">{subtitle}</div> : null}
</div>
);
}
function RunProgressBar({ succeeded, failed, total }: { succeeded: number; failed: number; total: number }) {
if (total === 0) return <div className="h-3 rounded-full bg-[color:var(--panel-soft)]" />;
const successPct = (succeeded / total) * 100;
const failPct = (failed / total) * 100;
return (
<div className="flex h-3 overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
<motion.div
className="h-full bg-[color:var(--success)]"
initial={{ width: 0 }}
animate={{ width: `${successPct}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
/>
<motion.div
className="h-full bg-[color:var(--danger)]"
initial={{ width: 0 }}
animate={{ width: `${failPct}%` }}
transition={{ duration: 0.8, ease: "easeOut", delay: 0.2 }}
/>
</div>
);
}
// Backward-compatible re-exports from decomposed inbox components
export { Inbox } from "./inbox/Inbox";
export { StandupView } from "./inbox/StandupView";
export { PerformanceView } from "./inbox/PerformanceView";
+160
View File
@@ -0,0 +1,160 @@
import { useMemo, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Bell,
ArrowRight,
Robot,
Tray,
} from "@phosphor-icons/react";
import type { InboxItem, InboxItemType, SectionId } from "@shared/types";
import { timeAgo } from "../../lib/formatters";
import { useT } from "../../i18n";
import { typeConfig, severityColors } from "./inbox-helpers";
interface InboxProps {
items: InboxItem[];
onNavigate: (section: SectionId, entityId?: string) => void;
}
export function Inbox({ items, onNavigate }: InboxProps) {
const t = useT();
const [filter, setFilter] = useState<InboxItemType | "all">("all");
const filteredItems = useMemo(() => {
if (filter === "all") return items;
return items.filter(i => i.type === filter);
}, [items, filter]);
const counts = useMemo(() => ({
all: items.length,
pending_approval: items.filter(i => i.type === "pending_approval").length,
failed_run: items.filter(i => i.type === "failed_run").length,
budget_warning: items.filter(i => i.type === "budget_warning").length,
blocked_task: items.filter(i => i.type === "blocked_task").length,
review_needed: items.filter(i => i.type === "review_needed").length,
agent_message: items.filter(i => i.type === "agent_message").length,
}), [items]);
const handleItemClick = (item: InboxItem) => {
const sectionMap: Record<string, SectionId> = {
approval: "approvals",
run: "runs",
agent: "agents",
task: "tasks",
message: "communication",
};
const section = sectionMap[item.entityType] ?? "overview";
onNavigate(section, item.entityId);
};
const filterButtons: Array<{ key: InboxItemType | "all"; label: string; count: number }> = [
{ key: "all", label: "All", count: counts.all },
{ key: "pending_approval", label: "Approvals", count: counts.pending_approval },
{ key: "review_needed", label: "Reviews", count: counts.review_needed },
{ key: "failed_run", label: "Failed", count: counts.failed_run },
{ key: "blocked_task", label: "Blocked", count: counts.blocked_task },
{ key: "budget_warning", label: "Budget", count: counts.budget_warning },
{ key: "agent_message", label: "Messages", count: counts.agent_message },
];
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
<Bell size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("inbox.title")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention
</p>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-1.5">
{filterButtons.map(btn => (
<button
key={btn.key}
type="button"
onClick={() => setFilter(btn.key)}
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[11px] font-medium transition ${
filter === btn.key
? "bg-[color:var(--accent)] text-white"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"
}`}
>
{btn.label}
{btn.count > 0 ? (
<span className={`rounded-full px-1.5 py-0.5 text-[9px] font-bold ${
filter === btn.key ? "bg-white/20 text-white" : "bg-[color:var(--line)] text-[color:var(--muted)]"
}`}>
{btn.count}
</span>
) : null}
</button>
))}
</div>
{filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Tray size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("inbox.allClear")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("inbox.noItemsNow")}</div>
</div>
) : (
<div className="space-y-1.5">
<AnimatePresence>
{filteredItems.map((item) => {
const config = typeConfig[item.type];
const Icon = config.icon;
return (
<motion.div
key={item.id}
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 20 }}
className={`group flex items-center gap-3 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 transition hover:border-[color:var(--line-strong)] hover:shadow-sm ${severityColors[item.severity] ?? ""}`}
>
<div
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]"
style={{ backgroundColor: config.bg }}
>
<Icon size={18} weight="fill" style={{ color: config.color }} />
</div>
<button
type="button"
onClick={() => handleItemClick(item)}
className="min-w-0 flex-1 text-left"
>
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{item.title}</span>
<span
className="shrink-0 rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]"
style={{ backgroundColor: config.bg, color: config.color }}
>
{config.label}
</span>
</div>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
{item.agentName ? (
<span className="flex items-center gap-1">
<Robot size={10} />
{item.agentName}
</span>
) : null}
<span>{item.subtitle.slice(0, 80)}</span>
<span className="shrink-0">{timeAgo(item.createdAt)}</span>
</div>
</button>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)] opacity-0 transition group-hover:opacity-100" />
</motion.div>
);
})}
</AnimatePresence>
</div>
)}
</div>
);
}
@@ -0,0 +1,126 @@
import { Target } from "@phosphor-icons/react";
import type { CompanyMetrics, SectionId } from "@shared/types";
import { formatDuration, formatMoney, getAgentInitials } from "../../lib/formatters";
import { useT } from "../../i18n";
import { CompactMetric, RunProgressBar } from "./inbox-helpers";
interface PerformanceProps {
metrics: CompanyMetrics | null;
onNavigate: (section: SectionId, entityId?: string) => void;
}
export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
const t = useT();
if (!metrics) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Target size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("perf.noMetrics")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("perf.noMetricsDesc")}</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
<CompactMetric
label={t("perf.successRate")}
value={`${metrics.overallSuccessRate}%`}
tone={metrics.overallSuccessRate >= 80 ? "success" : metrics.overallSuccessRate >= 50 ? "warn" : "danger"}
/>
<CompactMetric
label={t("perf.tasksDone")}
value={metrics.completedTasks}
tone="success"
subtitle={`of ${metrics.totalTasks} total`}
/>
<CompactMetric
label={t("perf.throughput")}
value={`${metrics.throughputTasksPerDay.toFixed(1)}`}
tone="accent"
subtitle={t("perf.tasksPerDay")}
/>
<CompactMetric
label={t("perf.avgCostTask")}
value={formatMoney(metrics.avgCostPerTask)}
tone="default"
/>
</div>
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Run Activity (30d)</div>
<div className="flex items-center gap-6">
<div className="flex-1">
<RunProgressBar
succeeded={metrics.successfulRuns}
failed={metrics.failedRuns}
total={metrics.totalRuns}
/>
</div>
<div className="flex gap-4 text-[12px]">
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" />
<span className="text-[color:var(--muted)]">{metrics.successfulRuns} succeeded</span>
</span>
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-[color:var(--danger)]" />
<span className="text-[color:var(--muted)]">{metrics.failedRuns} failed</span>
</span>
</div>
</div>
</div>
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agent Performance</div>
<div className="overflow-x-auto rounded-[10px] border border-[color:var(--line)]">
<table className="w-full text-left">
<thead>
<tr className="border-b border-[color:var(--line)] bg-[color:var(--panel-soft)]">
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Agent</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Tasks Done</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Success %</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Runs</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Avg Duration</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Cost</th>
</tr>
</thead>
<tbody>
{metrics.agentMetrics
.sort((a, b) => b.tasksCompleted - a.tasksCompleted)
.map((am) => (
<tr
key={am.agentId}
className="border-b border-[color:var(--line)] bg-[color:var(--panel)] transition hover:bg-[color:var(--panel-soft)] cursor-pointer"
onClick={() => onNavigate("agents", am.agentId)}
>
<td className="px-4 py-2.5">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-[color:var(--panel-soft)] text-[10px] font-bold text-[color:var(--muted-strong)]">
{getAgentInitials(am.agentName)}
</div>
<span className="text-[12px] font-medium text-[color:var(--text)]">{am.agentName}</span>
</div>
</td>
<td className="px-3 py-2.5 text-right text-[12px] font-semibold text-[color:var(--success)]">{am.tasksCompleted}</td>
<td className="px-3 py-2.5 text-right">
<span className={`text-[12px] font-semibold ${
am.successRate >= 80 ? "text-[color:var(--success)]"
: am.successRate >= 50 ? "text-[color:var(--warn)]"
: "text-[color:var(--danger)]"
}`}>
{am.successRate}%
</span>
</td>
<td className="px-3 py-2.5 text-right text-[12px] text-[color:var(--muted)]">{am.runsTotal}</td>
<td className="px-3 py-2.5 text-right text-[12px] text-[color:var(--muted)]">{formatDuration(am.avgRunDurationSec)}</td>
<td className="px-3 py-2.5 text-right text-[12px] text-[color:var(--muted)]">{formatMoney(am.totalCostUsd)}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
);
}
@@ -0,0 +1,144 @@
import {
ArrowRight,
CheckCircle,
Clock,
Heartbeat,
Warning,
} from "@phosphor-icons/react";
import type { SectionId, StandupReport } from "@shared/types";
import { getAgentInitials } from "../../lib/formatters";
import { useT } from "../../i18n";
interface StandupProps {
report: StandupReport | null;
onNavigate: (section: SectionId, entityId?: string) => void;
}
export function StandupView({ report, onNavigate }: StandupProps) {
const t = useT();
if (!report) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Clock size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("standup.noData")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("standup.noDataDesc")}</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
<Heartbeat size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("standup.dailyStandup")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
Generated {new Date(report.generatedAt).toLocaleString()}
</p>
</div>
</div>
<div className="text-[14px] font-medium text-[color:var(--text)]">{report.summary}</div>
<div className="mt-3 flex gap-4">
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--success)]">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.productive")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--danger)]">{report.blockers.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.blockers")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--warn)]">{report.pendingApprovals}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.pendingApprovals")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className={`text-[18px] font-bold ${report.budgetUtilization > 80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
{report.budgetUtilization}%
</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.budgetUsed")}</div>
</div>
</div>
</div>
{report.companyHighlights.length > 0 ? (
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.highlights")}</div>
<div className="space-y-1.5">
{report.companyHighlights.map((h, i) => (
<div key={i} className="flex items-center gap-2 rounded-[6px] bg-[color:var(--success-soft)] px-3 py-2 text-[12px] text-[color:var(--success)]">
<CheckCircle size={14} weight="fill" />
{h}
</div>
))}
</div>
</section>
) : null}
{report.blockers.length > 0 ? (
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--danger)]">Blockers</div>
<div className="space-y-1.5">
{report.blockers.map((b, i) => (
<div key={i} className="flex items-center gap-2 rounded-[6px] bg-[color:var(--danger-soft)] px-3 py-2 text-[12px] text-[color:var(--danger)]">
<Warning size={14} weight="fill" />
{b}
</div>
))}
</div>
</section>
) : null}
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.agentStatus")}</div>
<div className="space-y-2">
{report.agentReports.map((ar) => (
<button
key={ar.agentId}
type="button"
onClick={() => onNavigate("agents", ar.agentId)}
className="flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
>
<div className="relative">
<div className={`flex h-10 w-10 items-center justify-center rounded-full text-[12px] font-bold ${
ar.status === "running" ? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: ar.status === "error" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}>
{getAgentInitials(ar.agentName)}
</div>
{ar.status === "running" ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[color:var(--success)]" />
</span>
) : null}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold text-[color:var(--text)]">{ar.agentName}</span>
<span className="text-[10px] text-[color:var(--muted)]">{ar.role}</span>
</div>
<div className="mt-1 flex flex-wrap gap-3 text-[11px] text-[color:var(--muted)]">
{ar.tasksInProgress > 0 ? <span className="text-[color:var(--accent)]">{ar.tasksInProgress} in progress</span> : null}
{ar.tasksCompleted24h > 0 ? <span className="text-[color:var(--success)]">{ar.tasksCompleted24h} done</span> : null}
{ar.tasksBlocked > 0 ? <span className="text-[color:var(--danger)]">{ar.tasksBlocked} blocked</span> : null}
{ar.costLast24h > 0 ? <span>${ar.costLast24h.toFixed(2)}</span> : null}
</div>
{ar.highlights.length > 0 ? (
<div className="mt-1 text-[10px] text-[color:var(--muted)]">{ar.highlights.join(" \u00B7 ")}</div>
) : null}
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
))}
</div>
</section>
</div>
);
}
@@ -0,0 +1,72 @@
import { motion } from "framer-motion";
import {
ShieldCheck,
XCircle,
CurrencyCircleDollar,
Warning,
Eye,
ChatCircle,
} from "@phosphor-icons/react";
import type { InboxItemType } from "@shared/types";
export const typeConfig: Record<InboxItemType, { icon: React.ElementType; color: string; bg: string; label: string }> = {
pending_approval: { icon: ShieldCheck, color: "var(--warn)", bg: "var(--warn-soft)", label: "Approval" },
failed_run: { icon: XCircle, color: "var(--danger)", bg: "var(--danger-soft)", label: "Failed Run" },
budget_warning: { icon: CurrencyCircleDollar, color: "var(--warn)", bg: "var(--warn-soft)", label: "Budget" },
blocked_task: { icon: Warning, color: "var(--danger)", bg: "var(--danger-soft)", label: "Blocked" },
review_needed: { icon: Eye, color: "var(--accent)", bg: "var(--accent-soft)", label: "Review" },
agent_message: { icon: ChatCircle, color: "var(--accent)", bg: "var(--accent-soft)", label: "Message" },
};
export const severityColors: Record<string, string> = {
critical: "border-l-[color:var(--danger)]",
high: "border-l-[color:var(--warn)]",
medium: "border-l-[color:var(--accent)]",
low: "border-l-[color:var(--line)]",
};
export function CompactMetric({ label, value, tone = "default", subtitle }: {
label: string;
value: string | number;
tone?: "default" | "success" | "warn" | "danger" | "accent";
subtitle?: string;
}) {
const toneMap = {
default: "text-[color:var(--text)]",
success: "text-[color:var(--success)]",
warn: "text-[color:var(--warn)]",
danger: "text-[color:var(--danger)]",
accent: "text-[color:var(--accent)]",
};
return (
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
<div className={`text-[22px] font-bold ${toneMap[tone]}`}>{value}</div>
{subtitle ? <div className="text-[10px] text-[color:var(--muted)]">{subtitle}</div> : null}
</div>
);
}
export function RunProgressBar({ succeeded, failed, total }: { succeeded: number; failed: number; total: number }) {
if (total === 0) return <div className="h-3 rounded-full bg-[color:var(--panel-soft)]" />;
const successPct = (succeeded / total) * 100;
const failPct = (failed / total) * 100;
return (
<div className="flex h-3 overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
<motion.div
className="h-full bg-[color:var(--success)]"
initial={{ width: 0 }}
animate={{ width: `${successPct}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
/>
<motion.div
className="h-full bg-[color:var(--danger)]"
initial={{ width: 0 }}
animate={{ width: `${failPct}%` }}
transition={{ duration: 0.8, ease: "easeOut", delay: 0.2 }}
/>
</div>
);
}