From 9c965d0ff43320886ab34d12207722f31f2161d8 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sat, 7 Feb 2026 04:04:34 +0530 Subject: [PATCH] feat(ui): improve live feed cards --- frontend/src/app/boards/[boardId]/page.tsx | 647 ++++++++++++--------- 1 file changed, 366 insertions(+), 281 deletions(-) diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index 2f9875d6..11a24cc3 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -4,7 +4,14 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter } from "next/navigation"; 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 remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; @@ -140,10 +147,7 @@ const SSE_RECONNECT_BACKOFF = { const MARKDOWN_TABLE_COMPONENTS: Components = { table: ({ 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_BASIC; const components = - variant === "description" ? MARKDOWN_COMPONENTS_DESCRIPTION : MARKDOWN_COMPONENTS_BASIC; + variant === "description" + ? MARKDOWN_COMPONENTS_DESCRIPTION + : MARKDOWN_COMPONENTS_BASIC; return ( {trimmed} @@ -296,7 +302,9 @@ const ChatMessageCard = memo(function ChatMessageCard({ return (
-

{message.source ?? "User"}

+

+ {message.source ?? "User"} +

{formatShortTimestamp(message.created_at)} @@ -313,56 +321,80 @@ ChatMessageCard.displayName = "ChatMessageCard"; const LiveFeedCard = memo(function LiveFeedCard({ comment, taskTitle, - authorLabel, + authorName, + authorRole, + authorAvatar, onViewTask, }: { comment: TaskComment; taskTitle: string; - authorLabel: string; + authorName: string; + authorRole?: string | null; + authorAvatar: string; onViewTask?: () => void; }) { const message = (comment.message ?? "").trim(); return ( -
-
-
- -

{authorLabel}

+
+
+
+ {authorAvatar}
-
- - {formatShortTimestamp(comment.created_at)} - - {onViewTask ? ( +
+
- ) : null} + {onViewTask ? ( + + ) : null} +
+
+ {authorName} + {authorRole ? ( + <> + · + {authorRole} + + ) : null} + · + + {formatShortTimestamp(comment.created_at)} + +
{message ? ( -
+
) : ( -

+

)}
); @@ -519,9 +551,8 @@ export default function BoardDetailPage() { setApprovalsError(null); setChatError(null); try { - const snapshotResult = await getBoardSnapshotApiV1BoardsBoardIdSnapshotGet( - boardId, - ); + const snapshotResult = + await getBoardSnapshotApiV1BoardsBoardIdSnapshotGet(boardId); if (snapshotResult.status !== 200) { throw new Error("Unable to load board snapshot."); } @@ -532,7 +563,8 @@ export default function BoardDetailPage() { setApprovals((snapshot.approvals ?? []).map(normalizeApproval)); setChatMessages(snapshot.chat_messages ?? []); } catch (err) { - const message = err instanceof Error ? err.message : "Something went wrong."; + const message = + err instanceof Error ? err.message : "Something went wrong."; setError(message); setApprovalsError(message); setChatError(message); @@ -641,21 +673,23 @@ export default function BoardDetailPage() { } if (eventType === "memory" && data) { try { - const payload = JSON.parse(data) as { memory?: BoardChatMessage }; + const payload = JSON.parse(data) as { + memory?: BoardChatMessage; + }; if (payload.memory?.tags?.includes("chat")) { setChatMessages((prev) => { const exists = prev.some( (item) => item.id === payload.memory?.id, ); if (exists) return prev; - const next = [...prev, payload.memory as BoardChatMessage]; - next.sort((a, b) => { - const aTime = apiDatetimeToMs(a.created_at) ?? 0; - const bTime = apiDatetimeToMs(b.created_at) ?? 0; - return aTime - bTime; - }); - return next; - }); + const next = [...prev, payload.memory as BoardChatMessage]; + next.sort((a, b) => { + const aTime = apiDatetimeToMs(a.created_at) ?? 0; + const bTime = apiDatetimeToMs(b.created_at) ?? 0; + return aTime - bTime; + }); + return next; + }); } } catch { // ignore malformed @@ -900,41 +934,53 @@ export default function BoardDetailPage() { task?: TaskRead; comment?: TaskCommentRead; }; - if (payload.comment?.task_id && payload.type === "task.comment") { + if ( + payload.comment?.task_id && + payload.type === "task.comment" + ) { pushLiveFeed(payload.comment); setComments((prev) => { - if (selectedTaskIdRef.current !== payload.comment?.task_id) { + if ( + selectedTaskIdRef.current !== payload.comment?.task_id + ) { return prev; } - const exists = prev.some((item) => item.id === payload.comment?.id); + const exists = prev.some( + (item) => item.id === payload.comment?.id, + ); if (exists) { return prev; - } - const createdMs = apiDatetimeToMs(payload.comment?.created_at); - if (prev.length === 0 || createdMs === null) { - return [...prev, payload.comment as TaskComment]; - } - const last = prev[prev.length - 1]; - const lastMs = apiDatetimeToMs(last?.created_at); - if (lastMs !== null && createdMs >= lastMs) { - return [...prev, payload.comment as TaskComment]; - } - const next = [...prev, payload.comment as TaskComment]; - next.sort((a, b) => { - const aTime = apiDatetimeToMs(a.created_at) ?? 0; - const bTime = apiDatetimeToMs(b.created_at) ?? 0; - return aTime - bTime; - }); - return next; - }); + } + const createdMs = apiDatetimeToMs( + payload.comment?.created_at, + ); + if (prev.length === 0 || createdMs === null) { + return [...prev, payload.comment as TaskComment]; + } + const last = prev[prev.length - 1]; + const lastMs = apiDatetimeToMs(last?.created_at); + if (lastMs !== null && createdMs >= lastMs) { + return [...prev, payload.comment as TaskComment]; + } + const next = [...prev, payload.comment as TaskComment]; + next.sort((a, b) => { + const aTime = apiDatetimeToMs(a.created_at) ?? 0; + const bTime = apiDatetimeToMs(b.created_at) ?? 0; + return aTime - bTime; + }); + return next; + }); } else if (payload.task) { 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) { const assignee = payload.task?.assigned_agent_id - ? agentsRef.current.find( - (agent) => agent.id === payload.task?.assigned_agent_id, - )?.name ?? null + ? (agentsRef.current.find( + (agent) => + agent.id === payload.task?.assigned_agent_id, + )?.name ?? null) : null; const created = normalizeTask({ ...payload.task, @@ -947,9 +993,10 @@ export default function BoardDetailPage() { const next = [...prev]; const existing = next[index]; const assignee = payload.task?.assigned_agent_id - ? agentsRef.current.find( - (agent) => agent.id === payload.task?.assigned_agent_id, - )?.name ?? null + ? (agentsRef.current.find( + (agent) => + agent.id === payload.task?.assigned_agent_id, + )?.name ?? null) : null; const updated = normalizeTask({ ...existing, @@ -1054,7 +1101,9 @@ export default function BoardDetailPage() { if (payload.agent) { const normalized = normalizeAgent(payload.agent); setAgents((prev) => { - const index = prev.findIndex((item) => item.id === normalized.id); + const index = prev.findIndex( + (item) => item.id === normalized.id, + ); if (index === -1) { return [normalized, ...prev]; } @@ -1127,7 +1176,7 @@ export default function BoardDetailPage() { const created = normalizeTask({ ...result.data, assignee: result.data.assigned_agent_id - ? assigneeById.get(result.data.assigned_agent_id) ?? null + ? (assigneeById.get(result.data.assigned_agent_id) ?? null) : null, approvals_count: 0, approvals_pending_count: 0, @@ -1136,50 +1185,58 @@ export default function BoardDetailPage() { setIsDialogOpen(false); resetForm(); } catch (err) { - setCreateError(err instanceof Error ? err.message : "Something went wrong."); + setCreateError( + err instanceof Error ? err.message : "Something went wrong.", + ); } finally { setIsCreating(false); } }; - const handleSendChat = useCallback(async (content: string): Promise => { - if (!isSignedIn || !boardId) return false; - const trimmed = content.trim(); - if (!trimmed) return false; - setIsChatSending(true); - setChatError(null); - try { - const result = await createBoardMemoryApiV1BoardsBoardIdMemoryPost(boardId, { - content: trimmed, - tags: ["chat"], - }); - if (result.status !== 200) { - throw new Error("Unable to send message."); + const handleSendChat = useCallback( + async (content: string): Promise => { + if (!isSignedIn || !boardId) return false; + const trimmed = content.trim(); + if (!trimmed) return false; + setIsChatSending(true); + setChatError(null); + try { + const result = await createBoardMemoryApiV1BoardsBoardIdMemoryPost( + boardId, + { + content: trimmed, + tags: ["chat"], + }, + ); + if (result.status !== 200) { + throw new Error("Unable to send message."); + } + const created = result.data; + if (created.tags?.includes("chat")) { + setChatMessages((prev) => { + const exists = prev.some((item) => item.id === created.id); + if (exists) return prev; + const next = [...prev, created]; + next.sort((a, b) => { + const aTime = apiDatetimeToMs(a.created_at) ?? 0; + const bTime = apiDatetimeToMs(b.created_at) ?? 0; + return aTime - bTime; + }); + return next; + }); + } + return true; + } catch (err) { + setChatError( + err instanceof Error ? err.message : "Unable to send message.", + ); + return false; + } finally { + setIsChatSending(false); } - const created = result.data; - if (created.tags?.includes("chat")) { - setChatMessages((prev) => { - const exists = prev.some((item) => item.id === created.id); - if (exists) return prev; - const next = [...prev, created]; - next.sort((a, b) => { - const aTime = apiDatetimeToMs(a.created_at) ?? 0; - const bTime = apiDatetimeToMs(b.created_at) ?? 0; - return aTime - bTime; - }); - return next; - }); - } - return true; - } catch (err) { - setChatError( - err instanceof Error ? err.message : "Unable to send message.", - ); - return false; - } finally { - setIsChatSending(false); - } - }, [boardId, isSignedIn]); + }, + [boardId, isSignedIn], + ); const assigneeById = useMemo(() => { const map = new Map(); @@ -1307,41 +1364,49 @@ export default function BoardDetailPage() { }); }, [agents, workingAgentIds]); - const loadComments = useCallback(async (taskId: string) => { - if (!isSignedIn || !boardId) return; - setIsCommentsLoading(true); - setCommentsError(null); - try { - const result = - await listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet( - boardId, - taskId, + const loadComments = useCallback( + async (taskId: string) => { + if (!isSignedIn || !boardId) return; + setIsCommentsLoading(true); + setCommentsError(null); + try { + const result = + await listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet( + boardId, + taskId, + ); + if (result.status !== 200) throw new Error("Unable to load comments."); + const items = [...(result.data.items ?? [])]; + items.sort((a, b) => { + const aTime = apiDatetimeToMs(a.created_at) ?? 0; + const bTime = apiDatetimeToMs(b.created_at) ?? 0; + return aTime - bTime; + }); + setComments(items); + } catch (err) { + setCommentsError( + err instanceof Error ? err.message : "Something went wrong.", ); - if (result.status !== 200) throw new Error("Unable to load comments."); - const items = [...(result.data.items ?? [])]; - items.sort((a, b) => { - const aTime = apiDatetimeToMs(a.created_at) ?? 0; - const bTime = apiDatetimeToMs(b.created_at) ?? 0; - return aTime - bTime; - }); - setComments(items); - } catch (err) { - setCommentsError(err instanceof Error ? err.message : "Something went wrong."); - } finally { - setIsCommentsLoading(false); - } - }, [boardId, isSignedIn]); + } finally { + setIsCommentsLoading(false); + } + }, + [boardId, isSignedIn], + ); - const openComments = useCallback((task: { id: string }) => { - setIsChatOpen(false); - setIsLiveFeedOpen(false); - const fullTask = tasksRef.current.find((item) => item.id === task.id); - if (!fullTask) return; - selectedTaskIdRef.current = fullTask.id; - setSelectedTask(fullTask); - setIsDetailOpen(true); - void loadComments(task.id); - }, [loadComments]); + const openComments = useCallback( + (task: { id: string }) => { + setIsChatOpen(false); + setIsLiveFeedOpen(false); + const fullTask = tasksRef.current.find((item) => item.id === task.id); + if (!fullTask) return; + selectedTaskIdRef.current = fullTask.id; + setSelectedTask(fullTask); + setIsDetailOpen(true); + void loadComments(task.id); + }, + [loadComments], + ); const closeComments = () => { setIsDetailOpen(false); @@ -1470,20 +1535,24 @@ export default function BoardDetailPage() { ...previous, ...result.data, assignee: result.data.assigned_agent_id - ? assigneeById.get(result.data.assigned_agent_id) ?? null + ? (assigneeById.get(result.data.assigned_agent_id) ?? null) : null, approvals_count: previous.approvals_count, approvals_pending_count: previous.approvals_pending_count, } as TaskCardRead); setTasks((prev) => - prev.map((task) => (task.id === updated.id ? { ...task, ...updated } : task)), + prev.map((task) => + task.id === updated.id ? { ...task, ...updated } : task, + ), ); setSelectedTask(updated); if (closeOnSuccess) { setIsEditDialogOpen(false); } } catch (err) { - setSaveTaskError(err instanceof Error ? err.message : "Something went wrong."); + setSaveTaskError( + err instanceof Error ? err.message : "Something went wrong.", + ); } finally { setIsSavingTask(false); } @@ -1522,69 +1591,76 @@ export default function BoardDetailPage() { } }; - const handleTaskMove = useCallback(async (taskId: string, status: TaskStatus) => { - if (!isSignedIn || !boardId) return; - const currentTask = tasksRef.current.find((task) => task.id === taskId); - if (!currentTask || currentTask.status === status) return; - if (currentTask.is_blocked && status !== "inbox") { - setError("Task is blocked by incomplete dependencies."); - return; - } - const previousTasks = tasksRef.current; - setTasks((prev) => - prev.map((task) => - task.id === taskId - ? { - ...task, - status, - assigned_agent_id: - status === "inbox" ? null : task.assigned_agent_id, - assignee: status === "inbox" ? null : task.assignee, - } - : task, - ), - ); - try { - const result = await updateTaskApiV1BoardsBoardIdTasksTaskIdPatch( - boardId, - taskId, - { status }, - ); - if (result.status === 409) { - const blockedIds = result.data.detail.blocked_by_task_ids ?? []; - const blockedTitles = blockedIds - .map((id) => taskTitleById.get(id) ?? id) - .join(", "); - throw new Error( - blockedTitles - ? `${result.data.detail.message} Blocked by: ${blockedTitles}` - : result.data.detail.message, - ); + const handleTaskMove = useCallback( + async (taskId: string, status: TaskStatus) => { + if (!isSignedIn || !boardId) return; + const currentTask = tasksRef.current.find((task) => task.id === taskId); + if (!currentTask || currentTask.status === status) return; + if (currentTask.is_blocked && status !== "inbox") { + setError("Task is blocked by incomplete dependencies."); + return; } - if (result.status === 422) { - throw new Error( - result.data.detail?.[0]?.msg ?? "Validation error while moving task.", - ); - } - const assignee = result.data.assigned_agent_id - ? agentsRef.current.find((agent) => agent.id === result.data.assigned_agent_id) - ?.name ?? null - : null; - const updated = normalizeTask({ - ...currentTask, - ...result.data, - assignee, - approvals_count: currentTask.approvals_count, - approvals_pending_count: currentTask.approvals_pending_count, - } as TaskCardRead); + const previousTasks = tasksRef.current; setTasks((prev) => - prev.map((task) => (task.id === updated.id ? { ...task, ...updated } : task)), + prev.map((task) => + task.id === taskId + ? { + ...task, + status, + assigned_agent_id: + status === "inbox" ? null : task.assigned_agent_id, + assignee: status === "inbox" ? null : task.assignee, + } + : task, + ), ); - } catch (err) { - setTasks(previousTasks); - setError(err instanceof Error ? err.message : "Unable to move task."); - } - }, [boardId, isSignedIn, taskTitleById]); + try { + const result = await updateTaskApiV1BoardsBoardIdTasksTaskIdPatch( + boardId, + taskId, + { status }, + ); + if (result.status === 409) { + const blockedIds = result.data.detail.blocked_by_task_ids ?? []; + const blockedTitles = blockedIds + .map((id) => taskTitleById.get(id) ?? id) + .join(", "); + throw new Error( + blockedTitles + ? `${result.data.detail.message} Blocked by: ${blockedTitles}` + : result.data.detail.message, + ); + } + if (result.status === 422) { + throw new Error( + result.data.detail?.[0]?.msg ?? + "Validation error while moving task.", + ); + } + const assignee = result.data.assigned_agent_id + ? (agentsRef.current.find( + (agent) => agent.id === result.data.assigned_agent_id, + )?.name ?? null) + : null; + const updated = normalizeTask({ + ...currentTask, + ...result.data, + assignee, + approvals_count: currentTask.approvals_count, + approvals_pending_count: currentTask.approvals_pending_count, + } as TaskCardRead); + setTasks((prev) => + prev.map((task) => + task.id === updated.id ? { ...task, ...updated } : task, + ), + ); + } catch (err) { + setTasks(previousTasks); + setError(err instanceof Error ? err.message : "Unable to move task."); + } + }, + [boardId, isSignedIn, taskTitleById], + ); const agentInitials = (agent: Agent) => agent.name @@ -1608,7 +1684,8 @@ export default function BoardDetailPage() { if (agent.is_board_lead) return "⚙️"; let emojiValue: string | null = null; if (agent.identity_profile && typeof agent.identity_profile === "object") { - const rawEmoji = (agent.identity_profile as Record).emoji; + const rawEmoji = (agent.identity_profile as Record) + .emoji; emojiValue = typeof rawEmoji === "string" ? rawEmoji : null; } const emoji = resolveEmoji(emojiValue); @@ -1683,16 +1760,11 @@ export default function BoardDetailPage() { value .split(".") .map((part) => - part - .replace(/_/g, " ") - .replace(/\b\w/g, (char) => char.toUpperCase()) + part.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()), ) .join(" · "); - const approvalPayloadValue = ( - payload: Approval["payload"], - key: string, - ) => { + const approvalPayloadValue = (payload: Approval["payload"], key: string) => { if (!payload || typeof payload !== "object") return null; const value = (payload as Record)[key]; if (typeof value === "string" || typeof value === "number") { @@ -2001,7 +2073,8 @@ export default function BoardDetailPage() { {task.approvals_pending_count ? ( - Approval needed · {task.approvals_pending_count} + Approval needed ·{" "} + {task.approvals_pending_count} ) : null} ) : null} - -