conversation.tsx 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. 'use client';
  2. import { Button } from '@/components/ui/button';
  3. import { cn } from '@/lib/utils';
  4. import { ArrowDownIcon } from 'lucide-react';
  5. import type { ComponentProps } from 'react';
  6. import { useCallback } from 'react';
  7. import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
  8. export type ConversationProps = ComponentProps<typeof StickToBottom>;
  9. export const Conversation = ({ className, ...props }: ConversationProps) => (
  10. <StickToBottom
  11. className={cn('relative flex-1 overflow-y-hidden', className)}
  12. initial="smooth"
  13. resize="smooth"
  14. role="log"
  15. {...props}
  16. />
  17. );
  18. export type ConversationContentProps = ComponentProps<typeof StickToBottom.Content>;
  19. export const ConversationContent = ({ className, ...props }: ConversationContentProps) => (
  20. <StickToBottom.Content className={cn('flex flex-col gap-8 p-4', className)} {...props} />
  21. );
  22. export type ConversationEmptyStateProps = ComponentProps<'div'> & {
  23. title?: string;
  24. description?: string;
  25. icon?: React.ReactNode;
  26. };
  27. export const ConversationEmptyState = ({
  28. className,
  29. title = 'No messages yet',
  30. description = 'Start a conversation to see messages here',
  31. icon,
  32. children,
  33. ...props
  34. }: ConversationEmptyStateProps) => (
  35. <div
  36. className={cn(
  37. 'flex size-full flex-col items-center justify-center gap-3 p-8 text-center',
  38. className,
  39. )}
  40. {...props}
  41. >
  42. {children ?? (
  43. <>
  44. {icon && <div className="text-muted-foreground">{icon}</div>}
  45. <div className="space-y-1">
  46. <h3 className="font-medium text-sm">{title}</h3>
  47. {description && <p className="text-muted-foreground text-sm">{description}</p>}
  48. </div>
  49. </>
  50. )}
  51. </div>
  52. );
  53. export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
  54. export const ConversationScrollButton = ({
  55. className,
  56. ...props
  57. }: ConversationScrollButtonProps) => {
  58. const { isAtBottom, scrollToBottom } = useStickToBottomContext();
  59. const handleScrollToBottom = useCallback(() => {
  60. scrollToBottom();
  61. }, [scrollToBottom]);
  62. return (
  63. !isAtBottom && (
  64. <Button
  65. className={cn('absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full', className)}
  66. onClick={handleScrollToBottom}
  67. size="icon"
  68. type="button"
  69. variant="outline"
  70. {...props}
  71. >
  72. <ArrowDownIcon className="size-4" />
  73. </Button>
  74. )
  75. );
  76. };