| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- 'use client';
- import { Button } from '@/components/ui/button';
- import { ButtonGroup, ButtonGroupText } from '@/components/ui/button-group';
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
- import { cn } from '@/lib/utils';
- import type { FileUIPart, UIMessage } from 'ai';
- import { ChevronLeftIcon, ChevronRightIcon, PaperclipIcon, XIcon } from 'lucide-react';
- import type { ComponentProps, HTMLAttributes, ReactElement } from 'react';
- import { createContext, memo, useContext, useEffect, useMemo, useState } from 'react';
- import { Streamdown } from 'streamdown';
- export type MessageProps = HTMLAttributes<HTMLDivElement> & {
- from: UIMessage['role'];
- };
- export const Message = ({ className, from, ...props }: MessageProps) => (
- <div
- className={cn(
- 'group flex w-full max-w-[95%] flex-col gap-2',
- from === 'user' ? 'is-user ml-auto justify-end' : 'is-assistant',
- className,
- )}
- {...props}
- />
- );
- export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
- export const MessageContent = ({ children, className, ...props }: MessageContentProps) => (
- <div
- className={cn(
- 'is-user:dark flex w-fit max-w-full min-w-0 flex-col gap-2 overflow-hidden text-sm',
- 'group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground',
- 'group-[.is-assistant]:text-foreground',
- className,
- )}
- {...props}
- >
- {children}
- </div>
- );
- export type MessageActionsProps = ComponentProps<'div'>;
- export const MessageActions = ({ className, children, ...props }: MessageActionsProps) => (
- <div className={cn('flex items-center gap-1', className)} {...props}>
- {children}
- </div>
- );
- export type MessageActionProps = ComponentProps<typeof Button> & {
- tooltip?: string;
- label?: string;
- };
- export const MessageAction = ({
- tooltip,
- children,
- label,
- variant = 'ghost',
- size = 'icon-sm',
- ...props
- }: MessageActionProps) => {
- const button = (
- <Button size={size} type="button" variant={variant} {...props}>
- {children}
- <span className="sr-only">{label || tooltip}</span>
- </Button>
- );
- if (tooltip) {
- return (
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>{button}</TooltipTrigger>
- <TooltipContent>
- <p>{tooltip}</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- );
- }
- return button;
- };
- type MessageBranchContextType = {
- currentBranch: number;
- totalBranches: number;
- goToPrevious: () => void;
- goToNext: () => void;
- branches: ReactElement[];
- setBranches: (branches: ReactElement[]) => void;
- };
- const MessageBranchContext = createContext<MessageBranchContextType | null>(null);
- const useMessageBranch = () => {
- const context = useContext(MessageBranchContext);
- if (!context) {
- throw new Error('MessageBranch components must be used within MessageBranch');
- }
- return context;
- };
- export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
- defaultBranch?: number;
- onBranchChange?: (branchIndex: number) => void;
- };
- export const MessageBranch = ({
- defaultBranch = 0,
- onBranchChange,
- className,
- ...props
- }: MessageBranchProps) => {
- const [currentBranch, setCurrentBranch] = useState(defaultBranch);
- const [branches, setBranches] = useState<ReactElement[]>([]);
- const handleBranchChange = (newBranch: number) => {
- setCurrentBranch(newBranch);
- onBranchChange?.(newBranch);
- };
- const goToPrevious = () => {
- const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
- handleBranchChange(newBranch);
- };
- const goToNext = () => {
- const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
- handleBranchChange(newBranch);
- };
- const contextValue: MessageBranchContextType = {
- currentBranch,
- totalBranches: branches.length,
- goToPrevious,
- goToNext,
- branches,
- setBranches,
- };
- return (
- <MessageBranchContext.Provider value={contextValue}>
- <div className={cn('grid w-full gap-2 [&>div]:pb-0', className)} {...props} />
- </MessageBranchContext.Provider>
- );
- };
- export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
- export const MessageBranchContent = ({ children, ...props }: MessageBranchContentProps) => {
- const { currentBranch, setBranches, branches } = useMessageBranch();
- const childrenArray = useMemo(
- () => (Array.isArray(children) ? children : [children]),
- [children],
- );
- // Use useEffect to update branches when they change
- useEffect(() => {
- if (branches.length !== childrenArray.length) {
- setBranches(childrenArray);
- }
- }, [childrenArray, branches, setBranches]);
- return childrenArray.map((branch, index) => (
- <div
- className={cn(
- 'grid gap-2 overflow-hidden [&>div]:pb-0',
- index === currentBranch ? 'block' : 'hidden',
- )}
- key={branch.key}
- {...props}
- >
- {branch}
- </div>
- ));
- };
- export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
- from: UIMessage['role'];
- };
- export const MessageBranchSelector = ({
- className: _className,
- from: _from,
- ...props
- }: MessageBranchSelectorProps) => {
- const { totalBranches } = useMessageBranch();
- // Don't render if there's only one branch
- if (totalBranches <= 1) {
- return null;
- }
- return (
- <ButtonGroup
- className="[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md"
- orientation="horizontal"
- {...props}
- />
- );
- };
- export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
- export const MessageBranchPrevious = ({ children, ...props }: MessageBranchPreviousProps) => {
- const { goToPrevious, totalBranches } = useMessageBranch();
- return (
- <Button
- aria-label="Previous branch"
- disabled={totalBranches <= 1}
- onClick={goToPrevious}
- size="icon-sm"
- type="button"
- variant="ghost"
- {...props}
- >
- {children ?? <ChevronLeftIcon size={14} />}
- </Button>
- );
- };
- export type MessageBranchNextProps = ComponentProps<typeof Button>;
- export const MessageBranchNext = ({
- children,
- className: _className,
- ...props
- }: MessageBranchNextProps) => {
- const { goToNext, totalBranches } = useMessageBranch();
- return (
- <Button
- aria-label="Next branch"
- disabled={totalBranches <= 1}
- onClick={goToNext}
- size="icon-sm"
- type="button"
- variant="ghost"
- {...props}
- >
- {children ?? <ChevronRightIcon size={14} />}
- </Button>
- );
- };
- export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
- export const MessageBranchPage = ({ className, ...props }: MessageBranchPageProps) => {
- const { currentBranch, totalBranches } = useMessageBranch();
- return (
- <ButtonGroupText
- className={cn('border-none bg-transparent text-muted-foreground shadow-none', className)}
- {...props}
- >
- {currentBranch + 1} of {totalBranches}
- </ButtonGroupText>
- );
- };
- export type MessageResponseProps = ComponentProps<typeof Streamdown>;
- export const MessageResponse = memo(
- ({ className, ...props }: MessageResponseProps) => (
- <Streamdown
- className={cn('size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0', className)}
- {...props}
- />
- ),
- (prevProps, nextProps) => prevProps.children === nextProps.children,
- );
- MessageResponse.displayName = 'MessageResponse';
- export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
- data: FileUIPart;
- className?: string;
- onRemove?: () => void;
- };
- export function MessageAttachment({ data, className, onRemove, ...props }: MessageAttachmentProps) {
- const filename = data.filename || '';
- const mediaType = data.mediaType?.startsWith('image/') && data.url ? 'image' : 'file';
- const isImage = mediaType === 'image';
- const attachmentLabel = filename || (isImage ? 'Image' : 'Attachment');
- return (
- <div className={cn('group relative size-24 overflow-hidden rounded-lg', className)} {...props}>
- {isImage ? (
- <>
- <img
- alt={filename || 'attachment'}
- className="size-full object-cover"
- height={100}
- src={data.url}
- width={100}
- />
- {onRemove && (
- <Button
- aria-label="Remove attachment"
- className="absolute top-2 right-2 size-6 rounded-full bg-background/80 p-0 opacity-0 backdrop-blur-sm transition-opacity hover:bg-background group-hover:opacity-100 [&>svg]:size-3"
- onClick={(e) => {
- e.stopPropagation();
- onRemove();
- }}
- type="button"
- variant="ghost"
- >
- <XIcon />
- <span className="sr-only">Remove</span>
- </Button>
- )}
- </>
- ) : (
- <>
- <Tooltip>
- <TooltipTrigger asChild>
- <div className="flex size-full shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
- <PaperclipIcon className="size-4" />
- </div>
- </TooltipTrigger>
- <TooltipContent>
- <p>{attachmentLabel}</p>
- </TooltipContent>
- </Tooltip>
- {onRemove && (
- <Button
- aria-label="Remove attachment"
- className="size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity hover:bg-accent group-hover:opacity-100 [&>svg]:size-3"
- onClick={(e) => {
- e.stopPropagation();
- onRemove();
- }}
- type="button"
- variant="ghost"
- >
- <XIcon />
- <span className="sr-only">Remove</span>
- </Button>
- )}
- </>
- )}
- </div>
- );
- }
- export type MessageAttachmentsProps = ComponentProps<'div'>;
- export function MessageAttachments({ children, className, ...props }: MessageAttachmentsProps) {
- if (!children) {
- return null;
- }
- return (
- <div className={cn('ml-auto flex w-fit flex-wrap items-start gap-2', className)} {...props}>
- {children}
- </div>
- );
- }
- export type MessageToolbarProps = ComponentProps<'div'>;
- export const MessageToolbar = ({ className, children, ...props }: MessageToolbarProps) => (
- <div className={cn('mt-4 flex w-full items-center justify-between gap-4', className)} {...props}>
- {children}
- </div>
- );
|