update
This commit is contained in:
1 parent
b15f1c0e5e
commit
b56aaa1498
71 files changed
+866
-659
No files matched your search
@@ -65,7 +65,7 @@ export function ActivityFeed({ activity, agents, limit = 15 }: { activity: Activ
|
||||
<span className="text-[color:var(--muted)]"> — {item.detail.slice(0, 60)}{item.detail.length > 60 ? "..." : ""}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-[color:var(--muted)]">{timeAgo(item.createdAt)}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[10px] text-[color:var(--muted)]">{timeAgo(item.createdAt)}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ export function BudgetAlertBanner({ agents, onNavigate }: BudgetAlertBannerProps
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="rounded-[10px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"
|
||||
className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Icon */}
|
||||
@@ -69,11 +69,12 @@ export function BudgetAlertBanner({ agents, onNavigate }: BudgetAlertBannerProps
|
||||
{agentsOverThreshold.map((agent) => (
|
||||
<span
|
||||
key={agent.id}
|
||||
className={`text-[11px] font-[family-name:var(--font-mono)] ${
|
||||
className={`whitespace-nowrap text-[11px] font-[family-name:var(--font-mono)] ${
|
||||
agent.pct >= 100
|
||||
? "text-[color:var(--danger)]"
|
||||
: "text-[color:var(--warn)]"
|
||||
}`}
|
||||
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
{agent.name} ({agent.pct}%)
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
ArrowRight,
|
||||
Briefcase,
|
||||
@@ -28,6 +29,7 @@ import { RecoveryBanner } from "./RecoveryBanner";
|
||||
import { BudgetAlertBanner } from "./BudgetAlertBanner";
|
||||
|
||||
export interface DashboardProps {
|
||||
companyName?: string;
|
||||
agents: AgentRecord[];
|
||||
tasks: TaskRecord[];
|
||||
runs: RunRecord[];
|
||||
@@ -42,7 +44,10 @@ export interface DashboardProps {
|
||||
|
||||
export function Dashboard(props: DashboardProps) {
|
||||
const t = useT();
|
||||
const { agents, tasks, runs, approvals, activity, costs, messages = [], onNavigate } = props;
|
||||
const { companyName, agents, tasks, runs, approvals, activity, costs, messages = [], onNavigate } = props;
|
||||
|
||||
const agentMap = useMemo(() => new Map(agents.map((a) => [a.id, a])), [agents]);
|
||||
const taskMap = useMemo(() => new Map(tasks.map((t) => [t.id, t])), [tasks]);
|
||||
|
||||
const activeAgents = useMemo(() => agents.filter((a) => a.status !== "terminated" && a.status !== "pending_approval"), [agents]);
|
||||
const activeTasks = useMemo(() => tasks.filter((t) => t.status === "in_progress" || t.status === "in_review"), [tasks]);
|
||||
@@ -59,15 +64,35 @@ export function Dashboard(props: DashboardProps) {
|
||||
return runs
|
||||
.filter((r) => r.status === "running" || r.status === "queued")
|
||||
.map((run) => {
|
||||
const agent = agents.find((a) => a.id === run.agentId);
|
||||
const task = tasks.find((t) => t.id === run.taskId);
|
||||
const agent = agentMap.get(run.agentId);
|
||||
const task = taskMap.get(run.taskId);
|
||||
return { run, agent, task };
|
||||
})
|
||||
.slice(0, 8);
|
||||
}, [runs, agents, tasks]);
|
||||
}, [runs, agentMap, taskMap]);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
{/* Company status line */}
|
||||
{companyName ? (
|
||||
<div>
|
||||
<h2 className="text-[28px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{companyName}</h2>
|
||||
<div className="mt-2 flex items-center gap-3 text-[13px] tracking-[0.005em] text-[color:var(--muted)]">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
|
||||
{activeAgents.length} {activeAgents.length === 1 ? "agent" : "agents"} active
|
||||
</span>
|
||||
<span className="h-3 w-px bg-[color:var(--line-strong)]" />
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--accent)]" />
|
||||
{activeTasks.length} {activeTasks.length === 1 ? "task" : "tasks"} in progress
|
||||
</span>
|
||||
<span className="h-3 w-px bg-[color:var(--line-strong)]" />
|
||||
<span>{tasks.filter((tk) => tk.status === "done").length} completed</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Conditional banners */}
|
||||
<RecoveryBanner />
|
||||
<BudgetAlertBanner agents={agents} onNavigate={onNavigate} />
|
||||
@@ -109,15 +134,19 @@ export function Dashboard(props: DashboardProps) {
|
||||
{/* Needs Attention — moved to top, only shows when items exist */}
|
||||
{hasAttentionItems ? (
|
||||
<section>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">
|
||||
<div className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--warn)]" />
|
||||
{t("dash.needsAttentionSection")}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{pendingApprovals.slice(0, 5).map((approval) => {
|
||||
const requester = agents.find((a) => a.id === approval.requestedByAgentId);
|
||||
{pendingApprovals.slice(0, 5).map((approval, i) => {
|
||||
const requester = approval.requestedByAgentId ? agentMap.get(approval.requestedByAgentId) : undefined;
|
||||
return (
|
||||
<button
|
||||
<motion.button
|
||||
key={approval.id}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.22, delay: i * 0.04, ease: [0.16, 1, 0.3, 1] }}
|
||||
type="button"
|
||||
onClick={() => onNavigate("approvals", approval.id)}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
@@ -131,11 +160,11 @@ export function Dashboard(props: DashboardProps) {
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
</button>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
{failedRuns.slice(0, 3).map((run) => {
|
||||
const agent = agents.find((a) => a.id === run.agentId);
|
||||
const agent = agentMap.get(run.agentId);
|
||||
return (
|
||||
<button
|
||||
key={run.id}
|
||||
@@ -155,7 +184,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
);
|
||||
})}
|
||||
{urgentMessages.slice(0, 3).map((msg) => {
|
||||
const sender = agents.find((a) => a.id === msg.fromAgentId);
|
||||
const sender = agentMap.get(msg.fromAgentId);
|
||||
return (
|
||||
<button
|
||||
key={msg.id}
|
||||
@@ -189,7 +218,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
</button>
|
||||
</div>
|
||||
{activeWork.length > 0 ? (
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] divide-y divide-[color:var(--line)]">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] divide-y divide-[color:var(--line)]">
|
||||
{activeWork.map(({ run, agent, task }) => (
|
||||
<button
|
||||
key={run.id}
|
||||
@@ -201,7 +230,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
<Robot size={16} className="text-[color:var(--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-[color:var(--success)] opacity-40" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
|
||||
</span>
|
||||
) : null}
|
||||
@@ -218,7 +247,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-[10px] border border-dashed border-[color:var(--line)] py-8 text-center">
|
||||
<div className="rounded-[8px] border border-dashed border-[color:var(--line)] py-8 text-center">
|
||||
<div className="text-[13px] text-[color:var(--muted)]">{t("dash.allQuiet")}</div>
|
||||
<div className="mt-1 text-[11px] text-[color:var(--muted)]">No tasks currently running</div>
|
||||
</div>
|
||||
@@ -232,7 +261,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
{t("dash.recentActivity")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[360px] overflow-y-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
|
||||
<div className="max-h-[360px] overflow-y-auto rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
|
||||
<ActivityFeed activity={activity} agents={agents} limit={10} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -19,12 +19,12 @@ const DEPARTMENT_INFO: Record<string, { label: string; icon: React.ElementType;
|
||||
engineering: { label: "Engineering", icon: Cpu, color: "#6366f1" },
|
||||
product: { label: "Product", icon: Rocket, color: "#8b5cf6" },
|
||||
design: { label: "Design", icon: Target, color: "#ec4899" },
|
||||
marketing: { label: "Marketing", icon: TrendUp, color: "#f59e0b" },
|
||||
marketing: { label: "Marketing", icon: TrendUp, color: "#0ea5e9" },
|
||||
sales: { label: "Sales", icon: ChartLineUp, color: "#10b981" },
|
||||
hr: { label: "People", icon: Users, color: "#06b6d4" },
|
||||
finance: { label: "Finance", icon: CurrencyCircleDollar, color: "#14b8a6" },
|
||||
legal: { label: "Legal", icon: ShieldCheck, color: "#64748b" },
|
||||
operations: { label: "Operations", icon: Lightning, color: "#f97316" },
|
||||
operations: { label: "Operations", icon: Lightning, color: "#475569" },
|
||||
customer_support: { label: "Support", icon: Heartbeat, color: "#3b82f6" },
|
||||
research: { label: "Research", icon: Target, color: "#a855f7" },
|
||||
};
|
||||
@@ -88,12 +88,12 @@ export function DepartmentOverview({
|
||||
key={deptId}
|
||||
type="button"
|
||||
onClick={() => onNavigate("agents")}
|
||||
className="focus-ring group relative overflow-hidden rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-3 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
className="focus-ring group relative overflow-hidden rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-3.5 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-full w-[3px]" style={{ backgroundColor: info.color }} />
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<Icon size={14} style={{ color: info.color }} />
|
||||
<span className="text-[11px] font-semibold text-[color:var(--text)]">{info.label}</span>
|
||||
<Icon size={15} weight="fill" style={{ color: info.color }} />
|
||||
<span className="truncate text-[11px] font-medium text-[color:var(--text)]">{info.label}</span>
|
||||
{runningAgents > 0 && (
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" />
|
||||
@@ -103,18 +103,18 @@ export function DepartmentOverview({
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-3 gap-1 pl-1">
|
||||
<div>
|
||||
<div className="text-[14px] font-bold text-[color:var(--text)]">{data.agents.length}</div>
|
||||
<div className="text-[8px] uppercase tracking-[0.1em] text-[color:var(--muted)]">People</div>
|
||||
<div className="text-[16px] font-bold tabular-nums text-[color:var(--text)]">{data.agents.length}</div>
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">People</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[14px] font-bold text-[color:var(--accent)]">{activeTasks}</div>
|
||||
<div className="text-[8px] uppercase tracking-[0.1em] text-[color:var(--muted)]">Active</div>
|
||||
<div className="text-[16px] font-bold tabular-nums text-[color:var(--accent)]">{activeTasks}</div>
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Active</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-[14px] font-bold ${blockedTasks > 0 ? "text-[color:var(--danger)]" : "text-[color:var(--success)]"}`}>
|
||||
<div className={`text-[16px] font-bold tabular-nums ${blockedTasks > 0 ? "text-[color:var(--danger)]" : "text-[color:var(--success)]"}`}>
|
||||
{blockedTasks > 0 ? blockedTasks : doneTasks}
|
||||
</div>
|
||||
<div className="text-[8px] uppercase tracking-[0.1em] text-[color:var(--muted)]">
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
{blockedTasks > 0 ? "Blocked" : "Done"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Target } from "@phosphor-icons/react";
|
||||
import type { GoalRecord, SectionId, TaskRecord } from "@shared/types";
|
||||
|
||||
@@ -21,24 +20,22 @@ export function GoalProgress({ goals, tasks, onNavigate }: { goals: GoalRecord[]
|
||||
key={goal.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("goals")}
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 text-left transition hover:border-[color:var(--line-strong)]"
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target size={14} className="text-[color:var(--accent)]" />
|
||||
<span className="text-[12px] font-medium text-[color:var(--text)]">{goal.title}</span>
|
||||
<Target size={15} weight="fill" className="text-[color:var(--accent)]" />
|
||||
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{goal.title}</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-[color:var(--accent)]">{pct}%</span>
|
||||
<span className="whitespace-nowrap text-[13px] font-bold tabular-nums text-[color:var(--accent)]">{pct}%</span>
|
||||
</div>
|
||||
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-[color:var(--accent)]"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${pct}%` }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
<div className="mt-2.5 h-2 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
|
||||
<div
|
||||
className="h-full rounded-full bg-[color:var(--accent)] transition-all duration-700 ease-out"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] text-[color:var(--muted)]">{done}/{total} tasks complete</div>
|
||||
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]" style={{ fontVariantNumeric: "tabular-nums" }}>{done}/{total} tasks complete</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<Briefcase size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No tasks yet</div>
|
||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Tasks created by agents will appear here on the board</div>
|
||||
@@ -57,11 +57,11 @@ 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] shrink-0 flex-col">
|
||||
<div key={col.status} className="flex w-[220px] min-w-[180px] shrink-0 flex-col">
|
||||
<div className="mb-2 flex items-center gap-2 px-1">
|
||||
<span className={`h-2 w-2 rounded-full ${col.dotColor}`} />
|
||||
<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>
|
||||
<span className="ml-auto rounded-full bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[10px] font-bold text-[color:var(--muted)]">
|
||||
<span className="ml-auto rounded-full bg-[color:var(--panel-soft)] px-2 py-0.5 text-[10px] font-bold tabular-nums text-[color:var(--muted-strong)]">
|
||||
{colTasks.length}
|
||||
</span>
|
||||
</div>
|
||||
@@ -75,9 +75,9 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
key={task.id}
|
||||
type="button"
|
||||
onClick={() => onSelectTask(task.id)}
|
||||
className={`focus-ring rounded-[6px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
|
||||
className={`focus-ring rounded-[6px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition duration-150 hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
|
||||
>
|
||||
<div className="text-[12px] font-medium leading-snug text-[color:var(--text)]">{task.title}</div>
|
||||
<div className="truncate text-[12px] font-medium leading-snug text-[color:var(--text)]">{task.title}</div>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
{assignee ? (
|
||||
<span className="flex items-center gap-1 text-[10px] text-[color:var(--muted)]">
|
||||
@@ -88,8 +88,8 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
<span className="text-[10px] italic text-[color:var(--muted)]">Unassigned</span>
|
||||
)}
|
||||
{isRunning ? (
|
||||
<span className="flex items-center gap-1 text-[10px] text-[color:var(--success)]">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" />
|
||||
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-[color:var(--success)]">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" aria-hidden="true" />
|
||||
Running
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -30,19 +30,27 @@ export function MetricCard({
|
||||
accent: "bg-[color:var(--accent-soft)]",
|
||||
};
|
||||
|
||||
const borderMap = {
|
||||
default: "border-l-transparent",
|
||||
success: "border-l-[color:var(--success)]",
|
||||
warn: "border-l-[color:var(--warn)]",
|
||||
danger: "border-l-[color:var(--danger)]",
|
||||
accent: "border-l-[color:var(--accent)]",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="focus-ring flex items-center gap-3.5 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
className={`focus-ring flex items-center gap-4 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${borderMap[tone]}`}
|
||||
>
|
||||
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px] ${bgMap[tone]}`}>
|
||||
<Icon size={20} weight="fill" className={toneMap[tone]} />
|
||||
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-[8px] ${bgMap[tone]}`}>
|
||||
<Icon size={24} weight="fill" className={toneMap[tone]} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[11px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{label}</div>
|
||||
<div className={`text-[20px] font-bold tracking-[-0.02em] leading-tight ${toneMap[tone]}`}>{value}</div>
|
||||
{subtitle ? <div className="mt-0.5 truncate text-[11px] text-[color:var(--muted)]">{subtitle}</div> : null}
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
|
||||
<div className={`text-[28px] font-bold tracking-[-0.02em] leading-none ${toneMap[tone]}`} style={{ fontVariantNumeric: "tabular-nums" }}>{value}</div>
|
||||
{subtitle ? <div className="mt-1.5 truncate text-[11px] font-medium text-[color:var(--muted)]">{subtitle}</div> : null}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -121,7 +121,7 @@ export function NeedsAttention({
|
||||
<Cpu size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{action.resultSummary || action.actionType}</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">Browser action {action.status.replaceAll("_", " ")}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[11px] text-[color:var(--muted)]">Browser action {action.status.replaceAll("_", " ")}</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
</button>
|
||||
@@ -136,7 +136,7 @@ export function NeedsAttention({
|
||||
<Lightning size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{workflow.name}</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">Workflow {workflow.status}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[11px] text-[color:var(--muted)]">Workflow {workflow.status}</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
</button>
|
||||
|
||||
@@ -20,17 +20,17 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`focus-ring group relative flex flex-col items-center gap-2 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-3 text-center transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
|
||||
className={`focus-ring group relative flex flex-col items-center gap-2 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-3.5 text-center transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[14px] font-bold ${
|
||||
<div className={`flex h-12 w-12 items-center justify-center rounded-full text-[15px] font-bold ${
|
||||
isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
|
||||
}`}>
|
||||
{initials}
|
||||
</div>
|
||||
{agent.status === "running" ? (
|
||||
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" />
|
||||
</span>
|
||||
) : (
|
||||
@@ -61,7 +61,7 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
|
||||
export function WorkforceGrid({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent: (id: string) => void }) {
|
||||
if (agents.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-10 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-10 text-center">
|
||||
<Robot size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No agents hired yet</div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ export function RecoveryBanner() {
|
||||
const handleDismissAll = () => setDismissed(true);
|
||||
|
||||
return (
|
||||
<div className="bg-[color:var(--warn-soft)] border border-[color:var(--warn)]/30 rounded-[10px] p-4 mb-4">
|
||||
<div className="bg-[color:var(--warn-soft)] border border-[color:var(--warn)]/30 rounded-[8px] px-4 py-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2 text-[color:var(--warn)] font-medium">
|
||||
<Warning size={18} weight="bold" />
|
||||
@@ -53,10 +53,10 @@ export function RecoveryBanner() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{runs.map(run => (
|
||||
<div key={run.runId} className="flex items-center justify-between bg-[color:var(--panel-soft)] rounded px-3 py-2 text-sm">
|
||||
<div>
|
||||
<span className="text-[color:var(--text)]">{run.taskTitle}</span>
|
||||
<span className="text-[color:var(--muted)] ml-2">({run.agentName})</span>
|
||||
<div key={run.runId} className="flex items-center justify-between bg-[color:var(--panel-soft)] rounded-[4px] px-3 py-2 text-[13px]">
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="truncate text-[color:var(--text)]">{run.taskTitle}</span>
|
||||
<span className="whitespace-nowrap text-[color:var(--muted)] ml-2">({run.agentName})</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
|
||||
@@ -30,11 +30,11 @@ export function SprintProgress({ sprint, tasks }: { sprint: SprintRecord; tasks:
|
||||
}, [tasks]);
|
||||
|
||||
return (
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarCheck size={16} className="text-[color:var(--accent)]" />
|
||||
<span className="text-[12px] font-semibold text-[color:var(--text)]">{sprint.name}</span>
|
||||
<span className="truncate text-[12px] font-semibold text-[color:var(--text)]">{sprint.name}</span>
|
||||
<StatusPill status={sprint.status} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-[color:var(--muted)]">
|
||||
@@ -53,11 +53,11 @@ export function SprintProgress({ sprint, tasks }: { sprint: SprintRecord; tasks:
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between text-[10px] text-[color:var(--muted)]">
|
||||
<span>{sprint.completedPoints}/{sprint.velocityPoints} points ({progress}%)</span>
|
||||
<span style={{ fontVariantNumeric: "tabular-nums" }}>{sprint.completedPoints}/{sprint.velocityPoints} points ({progress}%)</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[color:var(--success)]">{sprintTasks.done} done</span>
|
||||
<span className="text-[color:var(--accent)]">{sprintTasks.active} active</span>
|
||||
<span>{sprintTasks.total} total</span>
|
||||
<span className="whitespace-nowrap text-[color:var(--success)]">{sprintTasks.done} done</span>
|
||||
<span className="whitespace-nowrap text-[color:var(--accent)]">{sprintTasks.active} active</span>
|
||||
<span className="whitespace-nowrap">{sprintTasks.total} total</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export function TaskPipeline({ tasks }: { tasks: TaskRecord[] }) {
|
||||
{stages.map((stage, i) => (
|
||||
<div key={stage.label} className="flex items-center gap-1.5">
|
||||
<div className={`flex items-center gap-2 rounded-[8px] px-3 py-2 ${stage.bg}`}>
|
||||
<span className={`text-[18px] font-bold ${stage.tone}`}>{stage.count}</span>
|
||||
<span className={`text-[18px] font-bold ${stage.tone}`} style={{ fontVariantNumeric: "tabular-nums" }}>{stage.count}</span>
|
||||
<span className="text-[10px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{stage.label}</span>
|
||||
</div>
|
||||
{i < stages.length - 1 ? (
|
||||
|
||||
Reference in new issue
Block a user