import { useCallback, useEffect, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { Task, TaskDetail, TaskAttachment, Column, MergeResult } from "@hai/core"; import { COLUMN_LABELS, VALID_TRANSITIONS } from "@hai/core"; import { uploadAttachment, deleteAttachment } from "../api"; import type { ToastType } from "../hooks/useToast"; function formatTimestamp(iso: string): string { const date = new Date(iso); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMin = Math.floor(diffMs / 60000); const diffHr = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHr / 24); if (diffMin < 1) return "just now"; if (diffMin < 60) return `${diffMin}m ago`; if (diffHr < 24) return `${diffHr}h ago`; if (diffDay < 7) return `${diffDay}d ago`; return date.toLocaleDateString(); } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } interface TaskDetailModalProps { task: TaskDetail; onClose: () => void; onMoveTask: (id: string, column: Column) => Promise; onDeleteTask: (id: string) => Promise; onMergeTask: (id: string) => Promise; addToast: (message: string, type?: ToastType) => void; } export function TaskDetailModal({ task, onClose, onMoveTask, onDeleteTask, onMergeTask, addToast, }: TaskDetailModalProps) { const [attachments, setAttachments] = useState(task.attachments || []); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [onClose]); const handleOverlayClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget) onClose(); }, [onClose], ); const handleMove = useCallback( async (column: Column) => { try { await onMoveTask(task.id, column); onClose(); addToast(`Moved to ${COLUMN_LABELS[column]}`, "success"); } catch (err: any) { addToast(err.message, "error"); } }, [task.id, onMoveTask, onClose, addToast], ); const handleDelete = useCallback(async () => { if (!confirm(`Delete ${task.id}?`)) return; try { await onDeleteTask(task.id); onClose(); addToast(`Deleted ${task.id}`, "info"); } catch (err: any) { addToast(err.message, "error"); } }, [task.id, onDeleteTask, onClose, addToast]); const handleMerge = useCallback(() => { if (!confirm(`Merge ${task.id} into the current branch?`)) return; onClose(); addToast(`Merging ${task.id}...`, "info"); onMergeTask(task.id) .then((result) => { const msg = result.merged ? `Merged ${task.id} (branch: ${result.branch})` : `Closed ${task.id} (${result.error || "no branch to merge"})`; addToast(msg, "success"); }) .catch((err: any) => { addToast(err.message, "error"); }); }, [task.id, onMergeTask, onClose, addToast]); const handleUpload = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setUploading(true); try { const attachment = await uploadAttachment(task.id, file); setAttachments((prev) => [...prev, attachment]); addToast("Screenshot attached", "success"); } catch (err: any) { addToast(err.message, "error"); } finally { setUploading(false); if (fileInputRef.current) fileInputRef.current.value = ""; } }, [task.id, addToast]); const handleDeleteAttachment = useCallback(async (filename: string) => { try { await deleteAttachment(task.id, filename); setAttachments((prev) => prev.filter((a) => a.filename !== filename)); addToast("Attachment deleted", "info"); } catch (err: any) { addToast(err.message, "error"); } }, [task.id, addToast]); const transitions = VALID_TRANSITIONS[task.column] || []; return (
{task.id} {COLUMN_LABELS[task.column]}

{task.title || task.id}

Created {new Date(task.createdAt).toLocaleDateString()} · Updated{" "} {new Date(task.updatedAt).toLocaleDateString()}
{task.prompt ? (
{task.prompt.replace(/^#\s+[^\n]*\n+/, "")}
) : (
(no prompt)
)}

Attachments

{attachments.length > 0 ? (
{attachments.map((a) => (
{a.originalName}
{a.originalName} ({formatBytes(a.size)})
))}
) : (
(no attachments)
)}
{task.dependencies && task.dependencies.length > 0 && (

Dependencies

    {task.dependencies.map((dep) => (
  • {dep}
  • ))}
)}

Activity

{task.log && task.log.length > 0 ? (
{[...task.log].reverse().map((entry, i) => (
{formatTimestamp(entry.timestamp)} {entry.action}
{entry.outcome && (
{entry.outcome}
)}
))}
) : (
(no activity)
)}
{task.column === "in-review" ? ( <> ) : ( transitions.map((col) => ( )) )}
); }