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,
rehypePlugins,
remarkPlugins,
} from "@/components/shared/markdown-components";
import { withMarkdownMathSanitizeSchema } from "@/lib/markdown/math-sanitize-schema";
import { useTranslation } from "react-i18next";
import { t } from "@/lib/i18n";
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 t("github:timeAgoJustNow");
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return t("github:timeAgoJustNow");
if (minutes < 60) return t("github:timeAgoMinutes", { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t("github:timeAgoHours", { count: hours });
const days = Math.floor(hours / 24);
if (days < 7) return t("github:timeAgoDays", { count: days });
if (days < 30) return t("github:timeAgoWeeks", { count: Math.floor(days / 7) });
if (days < 365) return t("github:timeAgoMonths", { count: Math.floor(days / 30) });
return t("github:timeAgoYears", { count: Math.floor(days / 365) });
}
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[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 { t } = useTranslation();
const [open, setOpen] = useState(defaultOpen ?? true);
return (
{onAddAll && count > 0 && open && (
{addAllLabel ?? t("github:addAllToChatContext")}
)}
{subtitle && open &&
{subtitle}
}
{open &&
{children}
}
);
}
export function AddToContextButton({
onClick,
tooltip,
}: {
onClick: () => void;
tooltip?: string;
}) {
const { t } = useTranslation();
return (
{tooltip ?? t("github:addToChatContext")}
);
}
/** Sanitization schema: default safe tags + details/summary for collapsible sections. */
const sanitizeSchema = withMarkdownMathSanitizeSchema({
...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 { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
return (
);
}
export function FeedbackItemRow({
author,
authorAvatar,
body,
createdAt,
metaBadge,
onAddAsContext,
trailingAction,
isReply,
}: {
author: string;
authorAvatar: string;
body?: string;
createdAt: string;
metaBadge?: React.ReactNode;
onAddAsContext?: () => void;
trailingAction?: React.ReactNode;
isReply?: boolean;
}) {
return (
{metaBadge}
{formatTimeAgo(createdAt)}
{onAddAsContext &&
}
{trailingAction}
{body &&
}
);
}