1- " use client" ;
1+ ' use client' ;
22
3- import { Button } from " @/components/ui/button" ;
3+ import { Button } from ' @/components/ui/button' ;
44import {
55 ButtonGroup ,
66 ButtonGroupText ,
7- } from " @/components/ui/button-group" ;
7+ } from ' @/components/ui/button-group' ;
88import {
99 Tooltip ,
1010 TooltipContent ,
1111 TooltipProvider ,
1212 TooltipTrigger ,
13- } from "@/components/ui/tooltip" ;
14- import { cn } from "@/lib/utils" ;
15- import { cjk } from "@streamdown/cjk" ;
16- import { code } from "@streamdown/code" ;
17- import { math } from "@streamdown/math" ;
18- import { mermaid } from "@streamdown/mermaid" ;
19- import type { UIMessage } from "ai" ;
20- import { ChevronLeftIcon , ChevronRightIcon } from "lucide-react" ;
21- import type { ComponentProps , HTMLAttributes , ReactElement } from "react" ;
22- import { createContext , memo , useContext , useEffect , useState } from "react" ;
23- import { Streamdown } from "streamdown" ;
13+ } from '@/components/ui/tooltip' ;
14+ import { cn } from '@/lib/utils' ;
15+ import type { FileUIPart , UIMessage } from 'ai' ;
16+ import {
17+ ChevronLeftIcon ,
18+ ChevronRightIcon ,
19+ PaperclipIcon ,
20+ XIcon ,
21+ } from 'lucide-react' ;
22+ import type { ComponentProps , HTMLAttributes , ReactElement } from 'react' ;
23+ import { createContext , memo , useContext , useEffect , useState } from 'react' ;
24+ import { Streamdown } from 'streamdown' ;
2425
2526export type MessageProps = HTMLAttributes < HTMLDivElement > & {
26- from : UIMessage [ " role" ] ;
27+ from : UIMessage [ ' role' ] ;
2728} ;
2829
2930export const Message = ( { className, from, ...props } : MessageProps ) => (
3031 < div
3132 className = { cn (
32- " group flex w-full max-w-[95%] flex-col gap-2" ,
33- from === " user" ? " is-user ml-auto justify-end" : " is-assistant" ,
34- className
33+ ' group flex w-full max-w-[95%] flex-col gap-2' ,
34+ from === ' user' ? ' is-user ml-auto justify-end' : ' is-assistant' ,
35+ className ,
3536 ) }
3637 { ...props }
3738 />
@@ -46,25 +47,25 @@ export const MessageContent = ({
4647} : MessageContentProps ) => (
4748 < div
4849 className = { cn (
49- " is-user:dark flex w-fit min -w-0 max -w-full flex-col gap-2 overflow-hidden text-sm" ,
50- " 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" ,
51- " group-[.is-assistant]:text-foreground" ,
52- className
50+ ' is-user:dark flex w-fit max -w-full min -w-0 flex-col gap-2 overflow-hidden text-sm' ,
51+ ' 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' ,
52+ ' group-[.is-assistant]:text-foreground' ,
53+ className ,
5354 ) }
5455 { ...props }
5556 >
5657 { children }
5758 </ div >
5859) ;
5960
60- export type MessageActionsProps = ComponentProps < " div" > ;
61+ export type MessageActionsProps = ComponentProps < ' div' > ;
6162
6263export const MessageActions = ( {
6364 className,
6465 children,
6566 ...props
6667} : MessageActionsProps ) => (
67- < div className = { cn ( " flex items-center gap-1" , className ) } { ...props } >
68+ < div className = { cn ( ' flex items-center gap-1' , className ) } { ...props } >
6869 { children }
6970 </ div >
7071) ;
@@ -78,8 +79,8 @@ export const MessageAction = ({
7879 tooltip,
7980 children,
8081 label,
81- variant = " ghost" ,
82- size = " icon-sm" ,
82+ variant = ' ghost' ,
83+ size = ' icon-sm' ,
8384 ...props
8485} : MessageActionProps ) => {
8586 const button = (
@@ -115,15 +116,15 @@ interface MessageBranchContextType {
115116}
116117
117118const MessageBranchContext = createContext < MessageBranchContextType | null > (
118- null
119+ null ,
119120) ;
120121
121122const useMessageBranch = ( ) => {
122123 const context = useContext ( MessageBranchContext ) ;
123124
124125 if ( ! context ) {
125126 throw new Error (
126- " MessageBranch components must be used within MessageBranch"
127+ ' MessageBranch components must be used within MessageBranch' ,
127128 ) ;
128129 }
129130
@@ -173,7 +174,7 @@ export const MessageBranch = ({
173174 return (
174175 < MessageBranchContext . Provider value = { contextValue } >
175176 < div
176- className = { cn ( " grid w-full gap-2 [&>div]:pb-0" , className ) }
177+ className = { cn ( ' grid w-full gap-2 [&>div]:pb-0' , className ) }
177178 { ...props }
178179 />
179180 </ MessageBranchContext . Provider >
@@ -199,8 +200,8 @@ export const MessageBranchContent = ({
199200 return childrenArray . map ( ( branch , index ) => (
200201 < div
201202 className = { cn (
202- " grid gap-2 overflow-hidden [&>div]:pb-0" ,
203- index === currentBranch ? " block" : " hidden"
203+ ' grid gap-2 overflow-hidden [&>div]:pb-0' ,
204+ index === currentBranch ? ' block' : ' hidden' ,
204205 ) }
205206 key = { branch . key }
206207 { ...props }
@@ -211,10 +212,12 @@ export const MessageBranchContent = ({
211212} ;
212213
213214export type MessageBranchSelectorProps = HTMLAttributes < HTMLDivElement > & {
214- from : UIMessage [ " role" ] ;
215+ from : UIMessage [ ' role' ] ;
215216} ;
216217
217218export const MessageBranchSelector = ( {
219+ className,
220+ from,
218221 ...props
219222} : MessageBranchSelectorProps ) => {
220223 const { totalBranches } = useMessageBranch ( ) ;
@@ -260,6 +263,7 @@ export type MessageBranchNextProps = ComponentProps<typeof Button>;
260263
261264export const MessageBranchNext = ( {
262265 children,
266+ className,
263267 ...props
264268} : MessageBranchNextProps ) => {
265269 const { goToNext, totalBranches } = useMessageBranch ( ) ;
@@ -290,8 +294,8 @@ export const MessageBranchPage = ({
290294 return (
291295 < ButtonGroupText
292296 className = { cn (
293- " border-none bg-transparent text-muted-foreground shadow-none" ,
294- className
297+ ' border-none bg-transparent text-muted-foreground shadow-none' ,
298+ className ,
295299 ) }
296300 { ...props }
297301 >
@@ -306,19 +310,126 @@ export const MessageResponse = memo(
306310 ( { className, ...props } : MessageResponseProps ) => (
307311 < Streamdown
308312 className = { cn (
309- " size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0" ,
310- className
313+ ' size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0' ,
314+ className ,
311315 ) }
312- plugins = { { code, mermaid, math, cjk } }
313316 { ...props }
314317 />
315318 ) ,
316- ( prevProps , nextProps ) => prevProps . children === nextProps . children
319+ ( prevProps , nextProps ) => prevProps . children === nextProps . children ,
317320) ;
318321
319- MessageResponse . displayName = "MessageResponse" ;
322+ MessageResponse . displayName = 'MessageResponse' ;
323+
324+ export type MessageAttachmentProps = HTMLAttributes < HTMLDivElement > & {
325+ data : FileUIPart ;
326+ className ?: string ;
327+ onRemove ?: ( ) => void ;
328+ } ;
329+
330+ export function MessageAttachment ( {
331+ data,
332+ className,
333+ onRemove,
334+ ...props
335+ } : MessageAttachmentProps ) {
336+ const filename = data . filename || '' ;
337+ const mediaType =
338+ data . mediaType ?. startsWith ( 'image/' ) && data . url ? 'image' : 'file' ;
339+ const isImage = mediaType === 'image' ;
340+ const attachmentLabel = filename || ( isImage ? 'Image' : 'Attachment' ) ;
341+
342+ return (
343+ < div
344+ className = { cn (
345+ 'group relative size-24 overflow-hidden rounded-lg' ,
346+ className ,
347+ ) }
348+ { ...props }
349+ >
350+ { isImage ? (
351+ < >
352+ < img
353+ alt = { filename || 'attachment' }
354+ className = "size-full object-cover"
355+ height = { 100 }
356+ src = { data . url }
357+ width = { 100 }
358+ />
359+ { onRemove && (
360+ < Button
361+ aria-label = "Remove attachment"
362+ 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"
363+ onClick = { ( e ) => {
364+ e . stopPropagation ( ) ;
365+ onRemove ( ) ;
366+ } }
367+ type = "button"
368+ variant = "ghost"
369+ >
370+ < XIcon />
371+ < span className = "sr-only" > Remove</ span >
372+ </ Button >
373+ ) }
374+ </ >
375+ ) : (
376+ < >
377+ < Tooltip >
378+ < TooltipTrigger asChild >
379+ < div className = "flex size-full shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground" >
380+ < PaperclipIcon className = "size-4" />
381+ </ div >
382+ </ TooltipTrigger >
383+ < TooltipContent >
384+ < p > { attachmentLabel } </ p >
385+ </ TooltipContent >
386+ </ Tooltip >
387+ { onRemove && (
388+ < Button
389+ aria-label = "Remove attachment"
390+ className = "size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity hover:bg-accent group-hover:opacity-100 [&>svg]:size-3"
391+ onClick = { ( e ) => {
392+ e . stopPropagation ( ) ;
393+ onRemove ( ) ;
394+ } }
395+ type = "button"
396+ variant = "ghost"
397+ >
398+ < XIcon />
399+ < span className = "sr-only" > Remove</ span >
400+ </ Button >
401+ ) }
402+ </ >
403+ ) }
404+ </ div >
405+ ) ;
406+ }
407+
408+ export type MessageAttachmentsProps = ComponentProps < 'div' > ;
409+
410+ export function MessageAttachments ( {
411+ children,
412+ className,
413+ ...props
414+ } : MessageAttachmentsProps ) {
415+ if ( ! children ) {
416+ return null ;
417+ }
418+
419+ return (
420+ < div
421+ className = { cn (
422+ 'ml-auto flex w-fit flex-wrap items-start gap-2' ,
423+ className ,
424+ ) }
425+ { ...props }
426+ >
427+ { children }
428+ </ div >
429+ ) ;
430+ }
320431
321- export type MessageToolbarProps = ComponentProps < " div" > ;
432+ export type MessageToolbarProps = ComponentProps < ' div' > ;
322433
323434export const MessageToolbar = ( {
324435 className,
@@ -327,8 +438,8 @@ export const MessageToolbar = ({
327438} : MessageToolbarProps ) => (
328439 < div
329440 className = { cn (
330- " mt-4 flex w-full items-center justify-between gap-4" ,
331- className
441+ ' mt-4 flex w-full items-center justify-between gap-4' ,
442+ className ,
332443 ) }
333444 { ...props }
334445 >
0 commit comments