import type { RouterOutput } from "@/libs/orpc/client"; import { t } from "@lingui/core/macro"; import { useLingui } from "@lingui/react"; import { Trans } from "@lingui/react/macro"; import { ArchiveIcon, ArrowLeftIcon, ChatCircleDotsIcon, DotsThreeVerticalIcon, PlusIcon, TrashIcon, } from "@phosphor-icons/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useNavigate } from "@tanstack/react-router"; import { useMemo } from "react"; import { toast } from "sonner"; import { Button } from "@reactive-resume/ui/components/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@reactive-resume/ui/components/dropdown-menu"; import { ScrollArea } from "@reactive-resume/ui/components/scroll-area"; import { cn } from "@reactive-resume/utils/style"; import { useConfirm } from "@/hooks/use-confirm"; import { getOrpcErrorMessage } from "@/libs/error-message"; import { orpc } from "@/libs/orpc/client"; type AgentThreadSummary = RouterOutput["agent"]["threads"]["list"][number]; const RELATIVE_TIME_DIVISIONS: Array<{ amount: number; unit: Intl.RelativeTimeFormatUnit }> = [ { amount: 31_536_000_000, unit: "year" }, { amount: 2_592_000_000, unit: "month" }, { amount: 604_800_000, unit: "week" }, { amount: 86_400_000, unit: "day" }, { amount: 3_600_000, unit: "hour" }, { amount: 60_000, unit: "minute" }, ]; function formatRelativeTime(value: Date | string, formatter: Intl.RelativeTimeFormat) { const date = value instanceof Date ? value : new Date(value); const diffMs = date.getTime() - Date.now(); const absMs = Math.abs(diffMs); if (absMs < 60_000) return formatter.format(0, "second"); const division = RELATIVE_TIME_DIVISIONS.find((candidate) => absMs >= candidate.amount); if (!division) return ""; return formatter.format(Math.round(diffMs / division.amount), division.unit); } function ThreadActions({ thread, activeThreadId }: { thread: AgentThreadSummary; activeThreadId: string | null }) { const navigate = useNavigate(); const confirm = useConfirm(); const queryClient = useQueryClient(); const archiveMutation = useMutation(orpc.agent.threads.archive.mutationOptions()); const deleteMutation = useMutation(orpc.agent.threads.delete.mutationOptions()); const isArchived = thread.status === "archived"; const handleArchive = () => { archiveMutation.mutate( { id: thread.id }, { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: orpc.agent.threads.list.queryKey() }); if (activeThreadId === thread.id) { await queryClient.invalidateQueries({ queryKey: orpc.agent.threads.get.queryKey({ input: { id: thread.id } }), }); } }, onError: (error) => toast.error(getOrpcErrorMessage(error, { fallback: t`Failed to archive thread.` })), }, ); }; const handleDelete = async () => { const confirmed = await confirm(t`Delete this agent thread?`, { description: t`This action cannot be undone. Messages and thread attachments will be removed.`, }); if (!confirmed) return; deleteMutation.mutate( { id: thread.id }, { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: orpc.agent.threads.list.queryKey() }); if (activeThreadId === thread.id) void navigate({ to: "/agent" }); }, onError: (error) => toast.error(getOrpcErrorMessage(error, { fallback: t`Failed to delete thread.` })), }, ); }; return ( Thread actions } /> {!isArchived ? ( Archive ) : null} void handleDelete()}> Delete ); } function ThreadRow({ thread, activeThreadId }: { thread: AgentThreadSummary; activeThreadId: string | null }) { const { i18n } = useLingui(); const relativeTimeFormatter = useMemo( () => Reflect.construct(Intl.RelativeTimeFormat, [i18n.locale, { numeric: "auto" }]) as Intl.RelativeTimeFormat, [i18n.locale], ); const isActive = thread.id === activeThreadId; const isArchived = thread.status === "archived"; const title = thread.title === thread.resumeName ? t`New thread` : thread.title; return (
{title}
{formatRelativeTime(thread.lastMessageAt, relativeTimeFormatter)}
); } export function AgentThreadSidebar({ activeThreadId = null, className, }: { activeThreadId?: string | null; className?: string; }) { const { data: threads, isLoading } = useQuery(orpc.agent.threads.list.queryOptions()); return ( ); }