feat: implement task creation endpoint for board leads and enhance board chat functionality

This commit is contained in:
Abhimanyu Saharan
2026-02-06 00:44:03 +05:30
parent f8f5849341
commit 69a6597936
10 changed files with 911 additions and 197 deletions

View File

@@ -30,6 +30,7 @@ type BoardApprovalsPanelProps = {
error?: string | null;
onRefresh?: () => void;
onDecision?: (approvalId: string, status: "approved" | "rejected") => void;
scrollable?: boolean;
};
const formatTimestamp = (value?: string | null) => {
@@ -108,12 +109,14 @@ export function BoardApprovalsPanel({
error: externalError,
onRefresh,
onDecision,
scrollable = false,
}: BoardApprovalsPanelProps) {
const { getToken, isSignedIn } = useAuth();
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 [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const usingExternal = Array.isArray(externalApprovals);
const approvals = usingExternal ? externalApprovals ?? [] : internalApprovals;
const loadingState = usingExternal ? externalLoading ?? false : isLoading;
@@ -204,8 +207,20 @@ export function BoardApprovalsPanel({
return { pending, resolved };
}, [approvals]);
const toggleExpanded = useCallback((approvalId: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(approvalId)) {
next.delete(approvalId);
} else {
next.add(approvalId);
}
return next;
});
}, []);
return (
<Card>
<Card className={scrollable ? "flex h-full flex-col" : undefined}>
<CardHeader className="flex flex-col gap-4 border-b border-[color:var(--border)] pb-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
@@ -228,7 +243,12 @@ export function BoardApprovalsPanel({
Review lead-agent decisions that require human approval.
</p>
</CardHeader>
<CardContent className="space-y-4 pt-5">
<CardContent
className={cn(
"space-y-4 pt-5",
scrollable && "flex-1 overflow-y-auto"
)}
>
{errorState ? (
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{errorState}
@@ -246,90 +266,106 @@ export function BoardApprovalsPanel({
<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"
>
<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 className="divide-y divide-[color:var(--border)] rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface)]">
{sortedApprovals.pending.map((approval) => {
const summary = approvalSummary(approval);
const summaryLine = summary.rows
.map((row) => `${row.label}: ${row.value}`)
.join(" • ");
const detailsPayload = JSON.stringify(
{
payload: approval.payload ?? null,
rubric_scores: approval.rubric_scores ?? null,
},
null,
2
);
const isExpanded = expandedIds.has(approval.id);
return (
<div key={approval.id} className="space-y-3 px-5 py-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<p className="text-sm font-semibold text-strong">
{humanizeAction(approval.action_type)}
</p>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
<span>
Requested {formatTimestamp(approval.created_at)}
</span>
{summaryLine ? (
<>
<span className="text-slate-300"></span>
<span className="truncate">{summaryLine}</span>
</>
) : null}
</div>
))}
</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.reason ? (
<p className="text-sm text-muted">{summary.reason}</p>
) : null}
{summary.rows.length > 0 ? (
<dl className="grid gap-2 text-xs text-muted sm:grid-cols-2">
{summary.rows.map((row) => (
<div key={`${approval.id}-${row.label}`}>
<dt className="font-semibold uppercase tracking-wide text-slate-500">
{row.label}
</dt>
<dd className="mt-1 text-sm text-strong">
{row.value}
</dd>
</div>
))}
</dl>
) : null}
<div className="flex flex-wrap items-center gap-3 text-xs text-muted">
<button
type="button"
className="font-semibold text-slate-700 hover:text-slate-900"
onClick={() => toggleExpanded(approval.id)}
>
{isExpanded ? "Hide raw" : "View raw"}
</button>
<span>JSON payload + rubric</span>
</div>
{isExpanded ? (
<pre className="max-h-40 overflow-auto rounded-xl bg-slate-950 px-3 py-3 text-[11px] text-slate-100">
{detailsPayload}
</pre>
) : 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>
) : 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>
</div>
) : null}
{sortedApprovals.resolved.length > 0 ? (
@@ -337,69 +373,85 @@ export function BoardApprovalsPanel({
<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 className="divide-y divide-[color:var(--border)] rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface)]">
{sortedApprovals.resolved.map((approval) => {
const summary = approvalSummary(approval);
const summaryLine = summary.rows
.map((row) => `${row.label}: ${row.value}`)
.join(" • ");
const detailsPayload = JSON.stringify(
{
payload: approval.payload ?? null,
rubric_scores: approval.rubric_scores ?? null,
},
null,
2
);
const isExpanded = expandedIds.has(approval.id);
return (
<div key={approval.id} className="space-y-3 px-5 py-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<p className="text-sm font-semibold text-strong">
{humanizeAction(approval.action_type)}
</p>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
<span>
Requested {formatTimestamp(approval.created_at)}
</span>
{summaryLine ? (
<>
<span className="text-slate-300"></span>
<span className="truncate">{summaryLine}</span>
</>
) : null}
</div>
))}
</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>
) : 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>
);
})}
{summary.reason ? (
<p className="text-sm text-muted">{summary.reason}</p>
) : null}
{summary.rows.length > 0 ? (
<dl className="grid gap-2 text-xs text-muted sm:grid-cols-2">
{summary.rows.map((row) => (
<div key={`${approval.id}-${row.label}`}>
<dt className="font-semibold uppercase tracking-wide text-slate-500">
{row.label}
</dt>
<dd className="mt-1 text-sm text-strong">
{row.value}
</dd>
</div>
))}
</dl>
) : null}
<div className="flex flex-wrap items-center gap-3 text-xs text-muted">
<button
type="button"
className="font-semibold text-slate-700 hover:text-slate-900"
onClick={() => toggleExpanded(approval.id)}
>
{isExpanded ? "Hide raw" : "View raw"}
</button>
<span>JSON payload + rubric</span>
</div>
{isExpanded ? (
<pre className="max-h-40 overflow-auto rounded-xl bg-slate-950 px-3 py-3 text-[11px] text-slate-100">
{detailsPayload}
</pre>
) : null}
</div>
);
})}
</div>
</div>
) : null}
</div>

View File

@@ -7,6 +7,8 @@ interface TaskCardProps {
priority?: string;
assignee?: string;
due?: string;
approvalsCount?: number;
approvalsPendingCount?: number;
onClick?: () => void;
draggable?: boolean;
isDragging?: boolean;
@@ -19,12 +21,15 @@ export function TaskCard({
priority,
assignee,
due,
approvalsCount = 0,
approvalsPendingCount = 0,
onClick,
draggable = false,
isDragging = false,
onDragStart,
onDragEnd,
}: TaskCardProps) {
const hasPendingApproval = approvalsPendingCount > 0;
const priorityBadge = (value?: string) => {
if (!value) return null;
const normalized = value.toLowerCase();
@@ -45,8 +50,9 @@ export function TaskCard({
return (
<div
className={cn(
"group cursor-pointer rounded-lg border border-slate-200 bg-white p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-slate-300 hover:shadow-md",
"group relative cursor-pointer rounded-lg border border-slate-200 bg-white p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-slate-300 hover:shadow-md",
isDragging && "opacity-60 shadow-none",
hasPendingApproval && "border-amber-200 bg-amber-50/40",
)}
draggable={draggable}
onDragStart={onDragStart}
@@ -61,18 +67,29 @@ export function TaskCard({
}
}}
>
{hasPendingApproval ? (
<span className="absolute left-0 top-0 h-full w-1 rounded-l-lg bg-amber-400" />
) : null}
<div className="flex items-start justify-between gap-3">
<div className="space-y-2">
<p className="text-sm font-medium text-slate-900">{title}</p>
{hasPendingApproval ? (
<div className="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-wide text-amber-700">
<span className="h-1.5 w-1.5 rounded-full bg-amber-500" />
Approval needed · {approvalsPendingCount}
</div>
) : null}
</div>
<div className="flex flex-col items-end gap-2">
<span
className={cn(
"inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold uppercase tracking-wide",
priorityBadge(priority) ?? "bg-slate-100 text-slate-600",
)}
>
{priorityLabel}
</span>
</div>
<span
className={cn(
"inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold uppercase tracking-wide",
priorityBadge(priority) ?? "bg-slate-100 text-slate-600",
)}
>
{priorityLabel}
</span>
</div>
<div className="mt-3 flex items-center justify-between text-xs text-slate-500">
<div className="flex items-center gap-2">

View File

@@ -14,6 +14,8 @@ type Task = {
due_at?: string | null;
assigned_agent_id?: string | null;
assignee?: string;
approvalsCount?: number;
approvalsPendingCount?: number;
};
type TaskBoardProps = {
@@ -173,17 +175,19 @@ export function TaskBoard({
<div className="rounded-b-xl border border-t-0 border-slate-200 bg-white p-3">
<div className="space-y-3">
{columnTasks.map((task) => (
<TaskCard
key={task.id}
title={task.title}
priority={task.priority}
assignee={task.assignee}
due={formatDueDate(task.due_at)}
onClick={() => onTaskSelect?.(task)}
draggable
isDragging={draggingId === task.id}
onDragStart={handleDragStart(task)}
onDragEnd={handleDragEnd}
<TaskCard
key={task.id}
title={task.title}
priority={task.priority}
assignee={task.assignee}
due={formatDueDate(task.due_at)}
approvalsCount={task.approvalsCount}
approvalsPendingCount={task.approvalsPendingCount}
onClick={() => onTaskSelect?.(task)}
draggable
isDragging={draggingId === task.id}
onDragStart={handleDragStart(task)}
onDragEnd={handleDragEnd}
/>
))}
</div>