import { useState } from "react"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; import { IconMessagePlus, IconChevronDown, IconChevronRight } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { markdownComponents, remarkPlugins } from "@/components/shared/markdown-components"; export function formatTimeAgo(dateStr: string): string { const date = new Date(dateStr); if (isNaN(date.getTime())) return ""; const diff = Date.now() - date.getTime(); if (diff < 0) return "just now"; const minutes = Math.floor(diff / 60000); if (minutes < 1) return "just now"; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d ago`; if (days < 30) return `${Math.floor(days / 7)}w ago`; if (days < 365) return `${Math.floor(days / 30)}mo ago`; return `${Math.floor(days / 365)}y ago`; } function formatMs(ms: number): string { const totalSeconds = Math.floor(ms / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (hours > 0) return `${hours}h ${minutes}m`; if (minutes > 0) return `${minutes}m ${seconds}s`; return `${seconds}s`; } export function formatDuration(startedAt: string, completedAt: string): string { return formatMs(new Date(completedAt).getTime() - new Date(startedAt).getTime()); } export function formatElapsed(startedAt: string): string { return formatMs(Date.now() - new Date(startedAt).getTime()); } export function AuthorAvatar({ src, author }: { src: string; author: string }) { const inner = src ? ( {author} ) : (
{author[0]?.toUpperCase() ?? "?"}
); return ( {inner} ); } export function AuthorLink({ author }: { author: string }) { return ( {author} ); } export function CollapsibleSection({ title, count, defaultOpen, subtitle, onAddAll, addAllLabel, children, }: { title: string; count: number; defaultOpen?: boolean; subtitle?: React.ReactNode; onAddAll?: () => void; addAllLabel?: string; children: React.ReactNode; }) { const [open, setOpen] = useState(defaultOpen ?? true); return (
{onAddAll && count > 0 && open && ( {addAllLabel ?? "Add all to chat context"} )}
{subtitle && open &&
{subtitle}
} {open &&
{children}
}
); } export function AddToContextButton({ onClick, tooltip, }: { onClick: () => void; tooltip?: string; }) { return ( {tooltip ?? "Add to chat context"} ); } /** Sanitization schema: default safe tags + details/summary for collapsible sections. */ const sanitizeSchema = { ...defaultSchema, tagNames: [...(defaultSchema.tagNames ?? []), "details", "summary"], }; export function PRMarkdownBody({ body }: { body: string }) { return (
{body}
); } export function getTimeAgoColor(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const hours = diff / (1000 * 60 * 60); if (hours < 24) return "text-green-600 dark:text-green-400"; if (hours < 48) return "text-muted-foreground"; return "text-orange-600 dark:text-orange-400"; } export function ExpandableBody({ body, className }: { body: string; className?: string }) { const [expanded, setExpanded] = useState(false); return (
); } export function FeedbackItemRow({ author, authorAvatar, body, createdAt, metaBadge, onAddAsContext, isReply, }: { author: string; authorAvatar: string; body?: string; createdAt: string; metaBadge?: React.ReactNode; onAddAsContext?: () => void; isReply?: boolean; }) { return (
{metaBadge} {formatTimeAgo(createdAt)} {onAddAsContext && }
{body && }
); }