diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index 1cdf9ad..39a2533 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; -import { MessageSquare, Pencil, Settings, X } from "lucide-react"; +import { Activity, MessageSquare, Pencil, Settings, X } from "lucide-react"; import ReactMarkdown from "react-markdown"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; @@ -146,6 +146,7 @@ export default function BoardDetailPage() { const [error, setError] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const [comments, setComments] = useState([]); + const [liveFeed, setLiveFeed] = useState([]); const [isCommentsLoading, setIsCommentsLoading] = useState(false); const [commentsError, setCommentsError] = useState(null); const [newComment, setNewComment] = useState(""); @@ -174,6 +175,16 @@ export default function BoardDetailPage() { const [isDeletingTask, setIsDeletingTask] = useState(false); const [deleteTaskError, setDeleteTaskError] = useState(null); const [viewMode, setViewMode] = useState<"board" | "list">("board"); + const [isLiveFeedOpen, setIsLiveFeedOpen] = useState(false); + const pushLiveFeed = useCallback((comment: TaskComment) => { + setLiveFeed((prev) => { + if (prev.some((item) => item.id === comment.id)) { + return prev; + } + const next = [comment, ...prev]; + return next.slice(0, 50); + }); + }, []); const [isDialogOpen, setIsDialogOpen] = useState(false); const [title, setTitle] = useState(""); @@ -634,6 +645,7 @@ export default function BoardDetailPage() { comment?: TaskComment; }; if (payload.comment?.task_id && payload.type === "task.comment") { + pushLiveFeed(payload.comment as TaskComment); setComments((prev) => { if (selectedTask?.id !== payload.comment?.task_id) { return prev; @@ -674,7 +686,7 @@ export default function BoardDetailPage() { isCancelled = true; abortController.abort(); }; - }, [board, boardId, getToken, isSignedIn, selectedTask?.id]); + }, [board, boardId, getToken, isSignedIn, selectedTask?.id, pushLiveFeed]); useEffect(() => { if (!isSignedIn || !boardId) return; @@ -869,6 +881,22 @@ export default function BoardDetailPage() { return map; }, [agents, boardId]); + const taskTitleById = useMemo(() => { + const map = new Map(); + tasks.forEach((task) => { + map.set(task.id, task.title); + }); + return map; + }, [tasks]); + + const orderedLiveFeed = useMemo(() => { + return [...liveFeed].sort((a, b) => { + const aTime = new Date(a.created_at).getTime(); + const bTime = new Date(b.created_at).getTime(); + return bTime - aTime; + }); + }, [liveFeed]); + const pendingApprovalsByTaskId = useMemo(() => { const map = new Map(); approvals @@ -1008,6 +1036,7 @@ export default function BoardDetailPage() { const openComments = (task: Task) => { setIsChatOpen(false); + setIsLiveFeedOpen(false); setSelectedTask(task); setIsDetailOpen(true); void loadComments(task.id); @@ -1027,6 +1056,7 @@ export default function BoardDetailPage() { if (isDetailOpen) { closeComments(); } + setIsLiveFeedOpen(false); setIsChatOpen(true); }; @@ -1035,6 +1065,20 @@ export default function BoardDetailPage() { setChatError(null); }; + const openLiveFeed = () => { + if (isDetailOpen) { + closeComments(); + } + if (isChatOpen) { + closeBoardChat(); + } + setIsLiveFeedOpen(true); + }; + + const closeLiveFeed = () => { + setIsLiveFeedOpen(false); + }; + const handlePostComment = async () => { if (!selectedTask || !boardId || !isSignedIn) return; const trimmed = newComment.trim(); @@ -1453,10 +1497,20 @@ export default function BoardDetailPage() { + + +
+ {orderedLiveFeed.length === 0 ? ( +

+ Waiting for new comments… +

+ ) : ( +
+ {orderedLiveFeed.map((comment) => ( +
+
+
+

+ {comment.task_id + ? taskTitleById.get(comment.task_id) ?? "Task" + : "Task"} +

+

+ {comment.agent_id + ? assigneeById.get(comment.agent_id) ?? "Agent" + : "Admin"} +

+
+ + {formatCommentTimestamp(comment.created_at)} + +
+ {comment.message?.trim() ? ( +
+ ( +

+ ), + ul: ({ ...props }) => ( +

    + ), + ol: ({ ...props }) => ( +
      + ), + li: ({ ...props }) => ( +
    1. + ), + strong: ({ ...props }) => ( + + ), + }} + > + {comment.message} + +
+ ) : ( +

—

+ )} +
+ ))} +
+ )} +
+ + +