feat: enhance agent creation with human-like naming and improve task assignment notifications

This commit is contained in:
Abhimanyu Saharan
2026-02-05 22:51:46 +05:30
parent cbf9fd1b0a
commit e09460a881
10 changed files with 1212 additions and 203 deletions

View File

@@ -25,6 +25,11 @@ type Approval = {
type BoardApprovalsPanelProps = {
boardId: string;
approvals?: Approval[];
isLoading?: boolean;
error?: string | null;
onRefresh?: () => void;
onDecision?: (approvalId: string, status: "approved" | "rejected") => void;
};
const formatTimestamp = (value?: string | null) => {
@@ -51,14 +56,71 @@ const confidenceVariant = (confidence: number) => {
return "warning";
};
export function BoardApprovalsPanel({ boardId }: BoardApprovalsPanelProps) {
const humanizeAction = (value: string) =>
value
.split(".")
.map((part) =>
part
.replace(/_/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase())
)
.join(" · ");
const payloadValue = (payload: Approval["payload"], key: string) => {
if (!payload) return null;
const value = payload[key as keyof typeof payload];
if (typeof value === "string" || typeof value === "number") {
return String(value);
}
return null;
};
const approvalSummary = (approval: Approval) => {
const payload = approval.payload ?? {};
const taskId =
payloadValue(payload, "task_id") ??
payloadValue(payload, "taskId") ??
payloadValue(payload, "taskID");
const assignedAgentId =
payloadValue(payload, "assigned_agent_id") ??
payloadValue(payload, "assignedAgentId");
const reason = payloadValue(payload, "reason");
const title = payloadValue(payload, "title");
const role = payloadValue(payload, "role");
const isAssign = approval.action_type.includes("assign");
const rows: Array<{ label: string; value: string }> = [];
if (taskId) rows.push({ label: "Task", value: taskId });
if (isAssign) {
rows.push({
label: "Assignee",
value: assignedAgentId ?? "Unassigned",
});
}
if (title) rows.push({ label: "Title", value: title });
if (role) rows.push({ label: "Role", value: role });
return { taskId, reason, rows };
};
export function BoardApprovalsPanel({
boardId,
approvals: externalApprovals,
isLoading: externalLoading,
error: externalError,
onRefresh,
onDecision,
}: BoardApprovalsPanelProps) {
const { getToken, isSignedIn } = useAuth();
const [approvals, setApprovals] = useState<Approval[]>([]);
const [internalApprovals, setInternalApprovals] = useState<Approval[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [updatingId, setUpdatingId] = useState<string | null>(null);
const usingExternal = Array.isArray(externalApprovals);
const approvals = usingExternal ? externalApprovals ?? [] : internalApprovals;
const loadingState = usingExternal ? externalLoading ?? false : isLoading;
const errorState = usingExternal ? externalError ?? null : error;
const loadApprovals = useCallback(async () => {
if (usingExternal) return;
if (!isSignedIn || !boardId) return;
setIsLoading(true);
setError(null);
@@ -71,23 +133,29 @@ export function BoardApprovalsPanel({ boardId }: BoardApprovalsPanelProps) {
});
if (!res.ok) throw new Error("Unable to load approvals.");
const data = (await res.json()) as Approval[];
setApprovals(data);
setInternalApprovals(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to load approvals.");
} finally {
setIsLoading(false);
}
}, [boardId, getToken, isSignedIn]);
}, [boardId, getToken, isSignedIn, usingExternal]);
useEffect(() => {
if (usingExternal) return;
loadApprovals();
if (!isSignedIn || !boardId) return;
const interval = setInterval(loadApprovals, 15000);
return () => clearInterval(interval);
}, [boardId, isSignedIn, loadApprovals]);
}, [boardId, isSignedIn, loadApprovals, usingExternal]);
const handleDecision = useCallback(
async (approvalId: string, status: "approved" | "rejected") => {
if (onDecision) {
onDecision(approvalId, status);
return;
}
if (usingExternal) return;
if (!isSignedIn || !boardId) return;
setUpdatingId(approvalId);
setError(null);
@@ -106,7 +174,7 @@ export function BoardApprovalsPanel({ boardId }: BoardApprovalsPanelProps) {
);
if (!res.ok) throw new Error("Unable to update approval.");
const updated = (await res.json()) as Approval;
setApprovals((prev) =>
setInternalApprovals((prev) =>
prev.map((item) => (item.id === approvalId ? updated : item))
);
} catch (err) {
@@ -117,19 +185,23 @@ export function BoardApprovalsPanel({ boardId }: BoardApprovalsPanelProps) {
setUpdatingId(null);
}
},
[boardId, getToken, isSignedIn]
[boardId, getToken, isSignedIn, onDecision, usingExternal]
);
const sortedApprovals = useMemo(() => {
const pending = approvals.filter((item) => item.status === "pending");
const resolved = approvals.filter((item) => item.status !== "pending");
const sortByTime = (items: Approval[]) =>
[...items].sort((a, b) => {
const aTime = new Date(a.created_at).getTime();
const bTime = new Date(b.created_at).getTime();
return bTime - aTime;
});
return [...sortByTime(pending), ...sortByTime(resolved)];
const pending = sortByTime(
approvals.filter((item) => item.status === "pending")
);
const resolved = sortByTime(
approvals.filter((item) => item.status !== "pending")
);
return { pending, resolved };
}, [approvals]);
return (
@@ -141,10 +213,14 @@ export function BoardApprovalsPanel({ boardId }: BoardApprovalsPanelProps) {
Approvals
</p>
<p className="mt-1 text-lg font-semibold text-strong">
Pending decisions
{sortedApprovals.pending.length} pending
</p>
</div>
<Button variant="secondary" size="sm" onClick={loadApprovals}>
<Button
variant="secondary"
size="sm"
onClick={onRefresh ?? loadApprovals}
>
Refresh
</Button>
</div>
@@ -153,82 +229,179 @@ export function BoardApprovalsPanel({ boardId }: BoardApprovalsPanelProps) {
</p>
</CardHeader>
<CardContent className="space-y-4 pt-5">
{error ? (
{errorState ? (
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{error}
{errorState}
</div>
) : null}
{isLoading ? (
{loadingState ? (
<p className="text-sm text-muted">Loading approvals</p>
) : sortedApprovals.length === 0 ? (
) : sortedApprovals.pending.length === 0 &&
sortedApprovals.resolved.length === 0 ? (
<p className="text-sm text-muted">No approvals yet.</p>
) : (
<div className="space-y-4">
{sortedApprovals.map((approval) => (
<div
key={approval.id}
className="space-y-2 rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4"
>
<div className="flex flex-wrap items-start justify-between gap-2">
<div>
<p className="text-sm font-semibold text-strong">
{approval.action_type.replace(/_/g, " ")}
</p>
<p className="text-xs text-muted">
Requested {formatTimestamp(approval.created_at)}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Badge variant={confidenceVariant(approval.confidence)}>
{approval.confidence}% confidence
</Badge>
<Badge variant={statusBadgeVariant(approval.status)}>
{approval.status}
</Badge>
</div>
</div>
{approval.payload || approval.rubric_scores ? (
<details className="rounded-xl border border-dashed border-[color:var(--border)] px-3 py-2 text-xs text-muted">
<summary className="cursor-pointer font-semibold text-strong">
Details
</summary>
{approval.payload ? (
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted">
Payload: {JSON.stringify(approval.payload, null, 2)}
</pre>
) : null}
{approval.rubric_scores ? (
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted">
Rubric: {JSON.stringify(approval.rubric_scores, null, 2)}
</pre>
) : null}
</details>
) : null}
{approval.status === "pending" ? (
<div className="flex flex-wrap gap-2">
<Button
variant="primary"
size="sm"
onClick={() => handleDecision(approval.id, "approved")}
disabled={updatingId === approval.id}
<div className="space-y-6">
{sortedApprovals.pending.length > 0 ? (
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
Pending
</p>
{sortedApprovals.pending.map((approval) => {
const summary = approvalSummary(approval);
return (
<div
key={approval.id}
className="space-y-3 rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4"
>
Approve
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDecision(approval.id, "rejected")}
disabled={updatingId === approval.id}
className={cn(
"border-[color:var(--danger)] text-[color:var(--danger)] hover:text-[color:var(--danger)]"
)}
>
Reject
</Button>
</div>
) : null}
<div className="flex flex-wrap items-start justify-between gap-2">
<div>
<p className="text-sm font-semibold text-strong">
{humanizeAction(approval.action_type)}
</p>
<p className="text-xs text-muted">
Requested {formatTimestamp(approval.created_at)}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Badge variant={confidenceVariant(approval.confidence)}>
{approval.confidence}% confidence
</Badge>
<Badge variant={statusBadgeVariant(approval.status)}>
{approval.status}
</Badge>
</div>
</div>
{summary.rows.length > 0 ? (
<div className="grid gap-2 text-sm text-strong sm:grid-cols-2">
{summary.rows.map((row) => (
<div key={`${approval.id}-${row.label}`}>
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
{row.label}
</p>
<p className="mt-1 text-sm text-strong">
{row.value}
</p>
</div>
))}
</div>
) : null}
{summary.reason ? (
<p className="text-sm text-muted">{summary.reason}</p>
) : null}
{approval.payload || approval.rubric_scores ? (
<details className="rounded-xl border border-dashed border-[color:var(--border)] px-3 py-2 text-xs text-muted">
<summary className="cursor-pointer font-semibold text-strong">
Details
</summary>
{approval.payload ? (
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted">
Payload: {JSON.stringify(approval.payload, null, 2)}
</pre>
) : null}
{approval.rubric_scores ? (
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted">
Rubric:{" "}
{JSON.stringify(approval.rubric_scores, null, 2)}
</pre>
) : null}
</details>
) : null}
<div className="flex flex-wrap gap-2">
<Button
variant="primary"
size="sm"
onClick={() => handleDecision(approval.id, "approved")}
disabled={updatingId === approval.id}
>
Approve
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDecision(approval.id, "rejected")}
disabled={updatingId === approval.id}
className={cn(
"border-[color:var(--danger)] text-[color:var(--danger)] hover:text-[color:var(--danger)]"
)}
>
Reject
</Button>
</div>
</div>
);
})}
</div>
))}
) : null}
{sortedApprovals.resolved.length > 0 ? (
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
Resolved
</p>
{sortedApprovals.resolved.map((approval) => {
const summary = approvalSummary(approval);
return (
<div
key={approval.id}
className="space-y-3 rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4"
>
<div className="flex flex-wrap items-start justify-between gap-2">
<div>
<p className="text-sm font-semibold text-strong">
{humanizeAction(approval.action_type)}
</p>
<p className="text-xs text-muted">
Requested {formatTimestamp(approval.created_at)}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Badge variant={confidenceVariant(approval.confidence)}>
{approval.confidence}% confidence
</Badge>
<Badge variant={statusBadgeVariant(approval.status)}>
{approval.status}
</Badge>
</div>
</div>
{summary.rows.length > 0 ? (
<div className="grid gap-2 text-sm text-strong sm:grid-cols-2">
{summary.rows.map((row) => (
<div key={`${approval.id}-${row.label}`}>
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
{row.label}
</p>
<p className="mt-1 text-sm text-strong">
{row.value}
</p>
</div>
))}
</div>
) : null}
{summary.reason ? (
<p className="text-sm text-muted">{summary.reason}</p>
) : null}
{approval.payload || approval.rubric_scores ? (
<details className="rounded-xl border border-dashed border-[color:var(--border)] px-3 py-2 text-xs text-muted">
<summary className="cursor-pointer font-semibold text-strong">
Details
</summary>
{approval.payload ? (
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted">
Payload: {JSON.stringify(approval.payload, null, 2)}
</pre>
) : null}
{approval.rubric_scores ? (
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted">
Rubric:{" "}
{JSON.stringify(approval.rubric_scores, null, 2)}
</pre>
) : null}
</details>
) : null}
</div>
);
})}
</div>
) : null}
</div>
)}
</CardContent>