import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import type { Task, TaskComment } from "@fusion/core"; import { getErrorMessage } from "@fusion/core"; import "./TaskComments.css"; import { addSteeringComment, updateTaskComment, deleteTaskComment } from "../api"; import type { ToastType } from "../hooks/useToast"; const MAX_COMMENT_LENGTH = 2000; interface TaskCommentsProps { task: Task; onTaskUpdated?: (task: Task) => void; addToast: (message: string, type?: ToastType) => void; currentAuthor?: string; projectId?: string; } function formatCommentTimestamp(comment: TaskComment, t: TFunction<"app">): string { const timestamp = comment.updatedAt || comment.createdAt; const label = new Date(timestamp).toLocaleString(); return comment.updatedAt ? `${label} ${t("comments.editedSuffix", "(edited)")}` : label; } function isAIGuidanceComment(author: string): boolean { return author === "agent" || author === "system"; } export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "user", projectId }: TaskCommentsProps) { const { t } = useTranslation("app"); const [draft, setDraft] = useState(""); const [editingId, setEditingId] = useState(null); const [editingText, setEditingText] = useState(""); const [submitting, setSubmitting] = useState(false); const [deletingId, setDeletingId] = useState(null); // Sort comments by createdAt descending (newest first) const comments = useMemo(() => { return [...(task.comments || [])].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); }, [task.comments]); const isOverLimit = draft.length > MAX_COMMENT_LENGTH; async function handleAddComment() { const text = draft.trim(); if (!text) return; setSubmitting(true); try { const updated = await addSteeringComment(task.id, text, projectId); setDraft(""); onTaskUpdated?.(updated); addToast(t("comments.addedSuccess", "Comment added"), "success"); } catch (error) { addToast(getErrorMessage(error) || "Failed to add comment", "error"); } finally { setSubmitting(false); } } async function handleSaveEdit(commentId: string) { const text = editingText.trim(); if (!text) return; setSubmitting(true); try { const updated = await updateTaskComment(task.id, commentId, text, projectId); setEditingId(null); setEditingText(""); onTaskUpdated?.(updated); addToast(t("comments.updatedSuccess", "Comment updated"), "success"); } catch (error) { addToast(getErrorMessage(error) || "Failed to update comment", "error"); } finally { setSubmitting(false); } } async function handleDelete(commentId: string) { setDeletingId(commentId); try { const updated = await deleteTaskComment(task.id, commentId, projectId); onTaskUpdated?.(updated); addToast(t("comments.deletedSuccess", "Comment deleted"), "success"); } catch (error) { addToast(getErrorMessage(error) || "Failed to delete comment", "error"); } finally { setDeletingId(null); } } function handleKeyDown(event: React.KeyboardEvent) { if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) { event.preventDefault(); void handleAddComment(); } } const placeholder = t("comments.placeholder", "Add a comment"); const buttonLabel = t("comments.addButton", "Add Comment"); return (

{t("comments.heading", "Comments")}

{comments.length === 0 ? (
{t("comments.emptyState", "No comments yet.")}
) : (
{comments.map((comment) => { const canEdit = comment.author === currentAuthor; const isEditing = editingId === comment.id; const isAIGuidance = isAIGuidanceComment(comment.author); return (
{isAIGuidance ? ( {t("comments.aiGuidance", "AI Guidance")} ) : ( {comment.author} )} {formatCommentTimestamp(comment, t)}
{canEdit && !isEditing ? (
) : null}
{isEditing ? (