feat(ui): improve live feed cards

This commit is contained in:
Abhimanyu Saharan
2026-02-07 04:04:34 +05:30
parent f683eba02f
commit 9c965d0ff4

View File

@@ -4,7 +4,14 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
import { Activity, MessageSquare, Pencil, Settings, X } from "lucide-react"; import {
Activity,
ArrowUpRight,
MessageSquare,
Pencil,
Settings,
X,
} from "lucide-react";
import ReactMarkdown, { type Components } from "react-markdown"; import ReactMarkdown, { type Components } from "react-markdown";
import remarkBreaks from "remark-breaks"; import remarkBreaks from "remark-breaks";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -140,10 +147,7 @@ const SSE_RECONNECT_BACKOFF = {
const MARKDOWN_TABLE_COMPONENTS: Components = { const MARKDOWN_TABLE_COMPONENTS: Components = {
table: ({ node: _node, className, ...props }) => ( table: ({ node: _node, className, ...props }) => (
<div className="my-3 overflow-x-auto"> <div className="my-3 overflow-x-auto">
<table <table className={cn("w-full border-collapse", className)} {...props} />
className={cn("w-full border-collapse", className)}
{...props}
/>
</div> </div>
), ),
thead: ({ node: _node, className, ...props }) => ( thead: ({ node: _node, className, ...props }) => (
@@ -240,7 +244,9 @@ const Markdown = memo(function Markdown({
? MARKDOWN_REMARK_PLUGINS_WITH_BREAKS ? MARKDOWN_REMARK_PLUGINS_WITH_BREAKS
: MARKDOWN_REMARK_PLUGINS_BASIC; : MARKDOWN_REMARK_PLUGINS_BASIC;
const components = const components =
variant === "description" ? MARKDOWN_COMPONENTS_DESCRIPTION : MARKDOWN_COMPONENTS_BASIC; variant === "description"
? MARKDOWN_COMPONENTS_DESCRIPTION
: MARKDOWN_COMPONENTS_BASIC;
return ( return (
<ReactMarkdown remarkPlugins={remarkPlugins} components={components}> <ReactMarkdown remarkPlugins={remarkPlugins} components={components}>
{trimmed} {trimmed}
@@ -296,7 +302,9 @@ const ChatMessageCard = memo(function ChatMessageCard({
return ( return (
<div className="rounded-2xl border border-slate-200 bg-slate-50/60 p-4"> <div className="rounded-2xl border border-slate-200 bg-slate-50/60 p-4">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-sm font-semibold text-slate-900">{message.source ?? "User"}</p> <p className="text-sm font-semibold text-slate-900">
{message.source ?? "User"}
</p>
<span className="text-xs text-slate-400"> <span className="text-xs text-slate-400">
{formatShortTimestamp(message.created_at)} {formatShortTimestamp(message.created_at)}
</span> </span>
@@ -313,56 +321,80 @@ ChatMessageCard.displayName = "ChatMessageCard";
const LiveFeedCard = memo(function LiveFeedCard({ const LiveFeedCard = memo(function LiveFeedCard({
comment, comment,
taskTitle, taskTitle,
authorLabel, authorName,
authorRole,
authorAvatar,
onViewTask, onViewTask,
}: { }: {
comment: TaskComment; comment: TaskComment;
taskTitle: string; taskTitle: string;
authorLabel: string; authorName: string;
authorRole?: string | null;
authorAvatar: string;
onViewTask?: () => void; onViewTask?: () => void;
}) { }) {
const message = (comment.message ?? "").trim(); const message = (comment.message ?? "").trim();
return ( return (
<div className="rounded-xl border border-slate-200 bg-white p-3"> <div className="rounded-xl border border-slate-200 bg-white p-3 transition hover:border-slate-300">
<div className="flex items-start justify-between gap-3 text-xs text-slate-500"> <div className="flex items-start gap-3">
<div className="min-w-0"> <div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-700">
{authorAvatar}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<button <button
type="button" type="button"
onClick={onViewTask} onClick={onViewTask}
disabled={!onViewTask} disabled={!onViewTask}
className={cn( className={cn(
"block truncate text-left text-xs font-semibold text-slate-700", "text-left text-sm font-semibold leading-snug text-slate-900",
onViewTask onViewTask
? "cursor-pointer transition hover:text-slate-900 hover:underline" ? "cursor-pointer transition hover:text-slate-950 hover:underline"
: "cursor-default", : "cursor-default",
)} )}
title={onViewTask ? "View task" : undefined} title={taskTitle}
style={{
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
> >
{taskTitle} {taskTitle}
</button> </button>
<p className="mt-1 text-[11px] text-slate-400">{authorLabel}</p>
</div>
<div className="flex flex-col items-end gap-1 text-right">
<span className="text-[11px] text-slate-400">
{formatShortTimestamp(comment.created_at)}
</span>
{onViewTask ? ( {onViewTask ? (
<button <button
type="button" type="button"
onClick={onViewTask} onClick={onViewTask}
className="text-[11px] font-semibold text-slate-600 transition hover:text-slate-900" className="inline-flex flex-shrink-0 items-center gap-1 rounded-md px-2 py-1 text-[11px] font-semibold text-slate-600 transition hover:bg-slate-50 hover:text-slate-900"
aria-label="View task"
> >
View task View task
<ArrowUpRight className="h-3 w-3" />
</button> </button>
) : null} ) : null}
</div> </div>
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-slate-500">
<span className="font-medium text-slate-700">{authorName}</span>
{authorRole ? (
<>
<span className="text-slate-300">·</span>
<span className="text-slate-500">{authorRole}</span>
</>
) : null}
<span className="text-slate-300">·</span>
<span className="text-slate-400">
{formatShortTimestamp(comment.created_at)}
</span>
</div>
</div>
</div> </div>
{message ? ( {message ? (
<div className="mt-2 select-text cursor-text text-xs leading-relaxed text-slate-900 break-words"> <div className="mt-3 select-text cursor-text text-sm leading-relaxed text-slate-900 break-words">
<Markdown content={message} variant="basic" /> <Markdown content={message} variant="basic" />
</div> </div>
) : ( ) : (
<p className="mt-2 text-xs text-slate-500"></p> <p className="mt-3 text-sm text-slate-500"></p>
)} )}
</div> </div>
); );
@@ -519,9 +551,8 @@ export default function BoardDetailPage() {
setApprovalsError(null); setApprovalsError(null);
setChatError(null); setChatError(null);
try { try {
const snapshotResult = await getBoardSnapshotApiV1BoardsBoardIdSnapshotGet( const snapshotResult =
boardId, await getBoardSnapshotApiV1BoardsBoardIdSnapshotGet(boardId);
);
if (snapshotResult.status !== 200) { if (snapshotResult.status !== 200) {
throw new Error("Unable to load board snapshot."); throw new Error("Unable to load board snapshot.");
} }
@@ -532,7 +563,8 @@ export default function BoardDetailPage() {
setApprovals((snapshot.approvals ?? []).map(normalizeApproval)); setApprovals((snapshot.approvals ?? []).map(normalizeApproval));
setChatMessages(snapshot.chat_messages ?? []); setChatMessages(snapshot.chat_messages ?? []);
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : "Something went wrong."; const message =
err instanceof Error ? err.message : "Something went wrong.";
setError(message); setError(message);
setApprovalsError(message); setApprovalsError(message);
setChatError(message); setChatError(message);
@@ -641,7 +673,9 @@ export default function BoardDetailPage() {
} }
if (eventType === "memory" && data) { if (eventType === "memory" && data) {
try { try {
const payload = JSON.parse(data) as { memory?: BoardChatMessage }; const payload = JSON.parse(data) as {
memory?: BoardChatMessage;
};
if (payload.memory?.tags?.includes("chat")) { if (payload.memory?.tags?.includes("chat")) {
setChatMessages((prev) => { setChatMessages((prev) => {
const exists = prev.some( const exists = prev.some(
@@ -900,17 +934,26 @@ export default function BoardDetailPage() {
task?: TaskRead; task?: TaskRead;
comment?: TaskCommentRead; comment?: TaskCommentRead;
}; };
if (payload.comment?.task_id && payload.type === "task.comment") { if (
payload.comment?.task_id &&
payload.type === "task.comment"
) {
pushLiveFeed(payload.comment); pushLiveFeed(payload.comment);
setComments((prev) => { setComments((prev) => {
if (selectedTaskIdRef.current !== payload.comment?.task_id) { if (
selectedTaskIdRef.current !== payload.comment?.task_id
) {
return prev; return prev;
} }
const exists = prev.some((item) => item.id === payload.comment?.id); const exists = prev.some(
(item) => item.id === payload.comment?.id,
);
if (exists) { if (exists) {
return prev; return prev;
} }
const createdMs = apiDatetimeToMs(payload.comment?.created_at); const createdMs = apiDatetimeToMs(
payload.comment?.created_at,
);
if (prev.length === 0 || createdMs === null) { if (prev.length === 0 || createdMs === null) {
return [...prev, payload.comment as TaskComment]; return [...prev, payload.comment as TaskComment];
} }
@@ -929,12 +972,15 @@ export default function BoardDetailPage() {
}); });
} else if (payload.task) { } else if (payload.task) {
setTasks((prev) => { setTasks((prev) => {
const index = prev.findIndex((item) => item.id === payload.task?.id); const index = prev.findIndex(
(item) => item.id === payload.task?.id,
);
if (index === -1) { if (index === -1) {
const assignee = payload.task?.assigned_agent_id const assignee = payload.task?.assigned_agent_id
? agentsRef.current.find( ? (agentsRef.current.find(
(agent) => agent.id === payload.task?.assigned_agent_id, (agent) =>
)?.name ?? null agent.id === payload.task?.assigned_agent_id,
)?.name ?? null)
: null; : null;
const created = normalizeTask({ const created = normalizeTask({
...payload.task, ...payload.task,
@@ -947,9 +993,10 @@ export default function BoardDetailPage() {
const next = [...prev]; const next = [...prev];
const existing = next[index]; const existing = next[index];
const assignee = payload.task?.assigned_agent_id const assignee = payload.task?.assigned_agent_id
? agentsRef.current.find( ? (agentsRef.current.find(
(agent) => agent.id === payload.task?.assigned_agent_id, (agent) =>
)?.name ?? null agent.id === payload.task?.assigned_agent_id,
)?.name ?? null)
: null; : null;
const updated = normalizeTask({ const updated = normalizeTask({
...existing, ...existing,
@@ -1054,7 +1101,9 @@ export default function BoardDetailPage() {
if (payload.agent) { if (payload.agent) {
const normalized = normalizeAgent(payload.agent); const normalized = normalizeAgent(payload.agent);
setAgents((prev) => { setAgents((prev) => {
const index = prev.findIndex((item) => item.id === normalized.id); const index = prev.findIndex(
(item) => item.id === normalized.id,
);
if (index === -1) { if (index === -1) {
return [normalized, ...prev]; return [normalized, ...prev];
} }
@@ -1127,7 +1176,7 @@ export default function BoardDetailPage() {
const created = normalizeTask({ const created = normalizeTask({
...result.data, ...result.data,
assignee: result.data.assigned_agent_id assignee: result.data.assigned_agent_id
? assigneeById.get(result.data.assigned_agent_id) ?? null ? (assigneeById.get(result.data.assigned_agent_id) ?? null)
: null, : null,
approvals_count: 0, approvals_count: 0,
approvals_pending_count: 0, approvals_pending_count: 0,
@@ -1136,23 +1185,29 @@ export default function BoardDetailPage() {
setIsDialogOpen(false); setIsDialogOpen(false);
resetForm(); resetForm();
} catch (err) { } catch (err) {
setCreateError(err instanceof Error ? err.message : "Something went wrong."); setCreateError(
err instanceof Error ? err.message : "Something went wrong.",
);
} finally { } finally {
setIsCreating(false); setIsCreating(false);
} }
}; };
const handleSendChat = useCallback(async (content: string): Promise<boolean> => { const handleSendChat = useCallback(
async (content: string): Promise<boolean> => {
if (!isSignedIn || !boardId) return false; if (!isSignedIn || !boardId) return false;
const trimmed = content.trim(); const trimmed = content.trim();
if (!trimmed) return false; if (!trimmed) return false;
setIsChatSending(true); setIsChatSending(true);
setChatError(null); setChatError(null);
try { try {
const result = await createBoardMemoryApiV1BoardsBoardIdMemoryPost(boardId, { const result = await createBoardMemoryApiV1BoardsBoardIdMemoryPost(
boardId,
{
content: trimmed, content: trimmed,
tags: ["chat"], tags: ["chat"],
}); },
);
if (result.status !== 200) { if (result.status !== 200) {
throw new Error("Unable to send message."); throw new Error("Unable to send message.");
} }
@@ -1179,7 +1234,9 @@ export default function BoardDetailPage() {
} finally { } finally {
setIsChatSending(false); setIsChatSending(false);
} }
}, [boardId, isSignedIn]); },
[boardId, isSignedIn],
);
const assigneeById = useMemo(() => { const assigneeById = useMemo(() => {
const map = new Map<string, string>(); const map = new Map<string, string>();
@@ -1307,7 +1364,8 @@ export default function BoardDetailPage() {
}); });
}, [agents, workingAgentIds]); }, [agents, workingAgentIds]);
const loadComments = useCallback(async (taskId: string) => { const loadComments = useCallback(
async (taskId: string) => {
if (!isSignedIn || !boardId) return; if (!isSignedIn || !boardId) return;
setIsCommentsLoading(true); setIsCommentsLoading(true);
setCommentsError(null); setCommentsError(null);
@@ -1326,13 +1384,18 @@ export default function BoardDetailPage() {
}); });
setComments(items); setComments(items);
} catch (err) { } catch (err) {
setCommentsError(err instanceof Error ? err.message : "Something went wrong."); setCommentsError(
err instanceof Error ? err.message : "Something went wrong.",
);
} finally { } finally {
setIsCommentsLoading(false); setIsCommentsLoading(false);
} }
}, [boardId, isSignedIn]); },
[boardId, isSignedIn],
);
const openComments = useCallback((task: { id: string }) => { const openComments = useCallback(
(task: { id: string }) => {
setIsChatOpen(false); setIsChatOpen(false);
setIsLiveFeedOpen(false); setIsLiveFeedOpen(false);
const fullTask = tasksRef.current.find((item) => item.id === task.id); const fullTask = tasksRef.current.find((item) => item.id === task.id);
@@ -1341,7 +1404,9 @@ export default function BoardDetailPage() {
setSelectedTask(fullTask); setSelectedTask(fullTask);
setIsDetailOpen(true); setIsDetailOpen(true);
void loadComments(task.id); void loadComments(task.id);
}, [loadComments]); },
[loadComments],
);
const closeComments = () => { const closeComments = () => {
setIsDetailOpen(false); setIsDetailOpen(false);
@@ -1470,20 +1535,24 @@ export default function BoardDetailPage() {
...previous, ...previous,
...result.data, ...result.data,
assignee: result.data.assigned_agent_id assignee: result.data.assigned_agent_id
? assigneeById.get(result.data.assigned_agent_id) ?? null ? (assigneeById.get(result.data.assigned_agent_id) ?? null)
: null, : null,
approvals_count: previous.approvals_count, approvals_count: previous.approvals_count,
approvals_pending_count: previous.approvals_pending_count, approvals_pending_count: previous.approvals_pending_count,
} as TaskCardRead); } as TaskCardRead);
setTasks((prev) => setTasks((prev) =>
prev.map((task) => (task.id === updated.id ? { ...task, ...updated } : task)), prev.map((task) =>
task.id === updated.id ? { ...task, ...updated } : task,
),
); );
setSelectedTask(updated); setSelectedTask(updated);
if (closeOnSuccess) { if (closeOnSuccess) {
setIsEditDialogOpen(false); setIsEditDialogOpen(false);
} }
} catch (err) { } catch (err) {
setSaveTaskError(err instanceof Error ? err.message : "Something went wrong."); setSaveTaskError(
err instanceof Error ? err.message : "Something went wrong.",
);
} finally { } finally {
setIsSavingTask(false); setIsSavingTask(false);
} }
@@ -1522,7 +1591,8 @@ export default function BoardDetailPage() {
} }
}; };
const handleTaskMove = useCallback(async (taskId: string, status: TaskStatus) => { const handleTaskMove = useCallback(
async (taskId: string, status: TaskStatus) => {
if (!isSignedIn || !boardId) return; if (!isSignedIn || !boardId) return;
const currentTask = tasksRef.current.find((task) => task.id === taskId); const currentTask = tasksRef.current.find((task) => task.id === taskId);
if (!currentTask || currentTask.status === status) return; if (!currentTask || currentTask.status === status) return;
@@ -1563,12 +1633,14 @@ export default function BoardDetailPage() {
} }
if (result.status === 422) { if (result.status === 422) {
throw new Error( throw new Error(
result.data.detail?.[0]?.msg ?? "Validation error while moving task.", result.data.detail?.[0]?.msg ??
"Validation error while moving task.",
); );
} }
const assignee = result.data.assigned_agent_id const assignee = result.data.assigned_agent_id
? agentsRef.current.find((agent) => agent.id === result.data.assigned_agent_id) ? (agentsRef.current.find(
?.name ?? null (agent) => agent.id === result.data.assigned_agent_id,
)?.name ?? null)
: null; : null;
const updated = normalizeTask({ const updated = normalizeTask({
...currentTask, ...currentTask,
@@ -1578,13 +1650,17 @@ export default function BoardDetailPage() {
approvals_pending_count: currentTask.approvals_pending_count, approvals_pending_count: currentTask.approvals_pending_count,
} as TaskCardRead); } as TaskCardRead);
setTasks((prev) => setTasks((prev) =>
prev.map((task) => (task.id === updated.id ? { ...task, ...updated } : task)), prev.map((task) =>
task.id === updated.id ? { ...task, ...updated } : task,
),
); );
} catch (err) { } catch (err) {
setTasks(previousTasks); setTasks(previousTasks);
setError(err instanceof Error ? err.message : "Unable to move task."); setError(err instanceof Error ? err.message : "Unable to move task.");
} }
}, [boardId, isSignedIn, taskTitleById]); },
[boardId, isSignedIn, taskTitleById],
);
const agentInitials = (agent: Agent) => const agentInitials = (agent: Agent) =>
agent.name agent.name
@@ -1608,7 +1684,8 @@ export default function BoardDetailPage() {
if (agent.is_board_lead) return "⚙️"; if (agent.is_board_lead) return "⚙️";
let emojiValue: string | null = null; let emojiValue: string | null = null;
if (agent.identity_profile && typeof agent.identity_profile === "object") { if (agent.identity_profile && typeof agent.identity_profile === "object") {
const rawEmoji = (agent.identity_profile as Record<string, unknown>).emoji; const rawEmoji = (agent.identity_profile as Record<string, unknown>)
.emoji;
emojiValue = typeof rawEmoji === "string" ? rawEmoji : null; emojiValue = typeof rawEmoji === "string" ? rawEmoji : null;
} }
const emoji = resolveEmoji(emojiValue); const emoji = resolveEmoji(emojiValue);
@@ -1683,16 +1760,11 @@ export default function BoardDetailPage() {
value value
.split(".") .split(".")
.map((part) => .map((part) =>
part part.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()),
.replace(/_/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase())
) )
.join(" · "); .join(" · ");
const approvalPayloadValue = ( const approvalPayloadValue = (payload: Approval["payload"], key: string) => {
payload: Approval["payload"],
key: string,
) => {
if (!payload || typeof payload !== "object") return null; if (!payload || typeof payload !== "object") return null;
const value = (payload as Record<string, unknown>)[key]; const value = (payload as Record<string, unknown>)[key];
if (typeof value === "string" || typeof value === "number") { if (typeof value === "string" || typeof value === "number") {
@@ -2001,7 +2073,8 @@ export default function BoardDetailPage() {
{task.approvals_pending_count ? ( {task.approvals_pending_count ? (
<span className="inline-flex items-center gap-2 text-[10px] font-semibold uppercase tracking-wide text-amber-700"> <span className="inline-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" /> <span className="h-1.5 w-1.5 rounded-full bg-amber-500" />
Approval needed · {task.approvals_pending_count} Approval needed ·{" "}
{task.approvals_pending_count}
</span> </span>
) : null} ) : null}
<span <span
@@ -2097,10 +2170,15 @@ export default function BoardDetailPage() {
</p> </p>
{selectedTask?.description ? ( {selectedTask?.description ? (
<div className="prose prose-sm max-w-none text-slate-700"> <div className="prose prose-sm max-w-none text-slate-700">
<Markdown content={selectedTask.description} variant="description" /> <Markdown
content={selectedTask.description}
variant="description"
/>
</div> </div>
) : ( ) : (
<p className="text-sm text-slate-500">No description provided.</p> <p className="text-sm text-slate-500">
No description provided.
</p>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -2204,7 +2282,8 @@ export default function BoardDetailPage() {
{humanizeApprovalAction(approval.action_type)} {humanizeApprovalAction(approval.action_type)}
</p> </p>
<p className="mt-1 text-xs text-slate-500"> <p className="mt-1 text-xs text-slate-500">
Requested {formatApprovalTimestamp(approval.created_at)} Requested{" "}
{formatApprovalTimestamp(approval.created_at)}
</p> </p>
</div> </div>
<span className="text-xs font-semibold text-slate-700"> <span className="text-xs font-semibold text-slate-700">
@@ -2299,7 +2378,7 @@ export default function BoardDetailPage() {
comment={comment} comment={comment}
authorLabel={ authorLabel={
comment.agent_id comment.agent_id
? assigneeById.get(comment.agent_id) ?? "Agent" ? (assigneeById.get(comment.agent_id) ?? "Agent")
: "Admin" : "Admin"
} }
/> />
@@ -2354,7 +2433,10 @@ export default function BoardDetailPage() {
)} )}
<div ref={chatEndRef} /> <div ref={chatEndRef} />
</div> </div>
<BoardChatComposer isSending={isChatSending} onSend={handleSendChat} /> <BoardChatComposer
isSending={isChatSending}
onSend={handleSendChat}
/>
</div> </div>
</div> </div>
</aside> </aside>
@@ -2393,18 +2475,27 @@ export default function BoardDetailPage() {
<div className="space-y-3"> <div className="space-y-3">
{orderedLiveFeed.map((comment) => { {orderedLiveFeed.map((comment) => {
const taskId = comment.task_id; const taskId = comment.task_id;
const authorAgent = comment.agent_id
? (agents.find((agent) => agent.id === comment.agent_id) ??
null)
: null;
const authorName = authorAgent ? authorAgent.name : "Admin";
const authorRole = authorAgent
? agentRoleLabel(authorAgent)
: null;
const authorAvatar = authorAgent
? agentAvatarLabel(authorAgent)
: "A";
return ( return (
<LiveFeedCard <LiveFeedCard
key={comment.id} key={comment.id}
comment={comment} comment={comment}
taskTitle={ taskTitle={
taskId ? taskTitleById.get(taskId) ?? "Task" : "Task" taskId ? (taskTitleById.get(taskId) ?? "Task") : "Task"
}
authorLabel={
comment.agent_id
? assigneeById.get(comment.agent_id) ?? "Agent"
: "Admin"
} }
authorName={authorName}
authorRole={authorRole}
authorAvatar={authorAvatar}
onViewTask={ onViewTask={
taskId ? () => openComments({ id: taskId }) : undefined taskId ? () => openComments({ id: taskId }) : undefined
} }
@@ -2713,16 +2804,10 @@ export default function BoardDetailPage() {
) : null} ) : null}
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button variant="outline" onClick={() => setIsDialogOpen(false)}>
variant="outline"
onClick={() => setIsDialogOpen(false)}
>
Cancel Cancel
</Button> </Button>
<Button <Button onClick={handleCreateTask} disabled={isCreating}>
onClick={handleCreateTask}
disabled={isCreating}
>
{isCreating ? "Creating…" : "Create task"} {isCreating ? "Creating…" : "Create task"}
</Button> </Button>
</DialogFooter> </DialogFooter>