@@ -21,6 +21,7 @@ import {
2121 AttachmentTrigger ,
2222} from "#/components/ui/attachment" ;
2323import { Dialog , DialogContent , DialogHeader , DialogTitle } from "#/components/ui/dialog" ;
24+ import { Skeleton } from "#/components/ui/skeleton" ;
2425import { Spinner } from "#/components/ui/spinner" ;
2526import type {
2627 AttachmentData ,
@@ -51,7 +52,7 @@ export function AiChatAttachmentItem({
5152 data : AttachmentData ;
5253 onRemove ?: ( ) => void ;
5354} ) {
54- if ( isPreviewableImageAttachment ( data ) ) {
55+ if ( isImageAttachment ( data ) ) {
5556 return < AiChatImageAttachment data = { data } onRemove = { onRemove } /> ;
5657 }
5758
@@ -64,57 +65,63 @@ export function AiChatAttachmentItem({
6465 ) ;
6566}
6667
67- function isPreviewableImageAttachment (
68- data : AttachmentData ,
69- ) : data is FileAttachmentData & { status : "ready" ; url : string } {
70- return (
71- data . type === "file" &&
72- data . status === "ready" &&
73- getMediaCategory ( data ) === "image" &&
74- Boolean ( data . url )
75- ) ;
68+ function isImageAttachment ( data : AttachmentData ) : data is FileAttachmentData {
69+ return data . type === "file" && getMediaCategory ( data ) === "image" ;
7670}
7771
7872function AiChatImageAttachment ( {
7973 data,
8074 onRemove,
8175} : {
82- data : FileAttachmentData & { status : "ready" ; url : string } ;
76+ data : FileAttachmentData ;
8377 onRemove ?: ( ) => void ;
8478} ) {
8579 const [ isOpen , setIsOpen ] = useState ( false ) ;
8680 const label = getAttachmentLabel ( data ) ;
81+ const imageUrl = data . status === "ready" ? data . url : undefined ;
8782
8883 return (
8984 < >
9085 < Attachment
91- className = "cursor-zoom-in focus-within:ring-2"
86+ className = { imageUrl ? "cursor-zoom-in focus-within:ring-2" : undefined }
9287 orientation = "vertical"
9388 size = "default"
89+ state = { getAttachmentState ( data ) }
9490 >
9591 < AttachmentMedia variant = "image" >
96- < img
97- alt = { label }
98- className = "size-full object-cover"
99- height = { 96 }
100- src = { data . url }
101- width = { 96 }
102- />
92+ { imageUrl ? (
93+ < img
94+ alt = { label }
95+ className = "size-full object-cover"
96+ height = { 96 }
97+ src = { imageUrl }
98+ width = { 96 }
99+ />
100+ ) : (
101+ < >
102+ < Skeleton aria-hidden = "true" className = "size-full rounded-none bg-foreground/10" />
103+ < span className = "sr-only" > Preparing { label } </ span >
104+ </ >
105+ ) }
103106 </ AttachmentMedia >
104- < AttachmentTrigger aria-label = { `Preview ${ label } ` } onClick = { ( ) => setIsOpen ( true ) } />
107+ { imageUrl ? (
108+ < AttachmentTrigger aria-label = { `Preview ${ label } ` } onClick = { ( ) => setIsOpen ( true ) } />
109+ ) : null }
105110 < AiChatAttachmentRemoveAction data = { data } onRemove = { onRemove } />
106111 </ Attachment >
107112
108- < Dialog open = { isOpen } onOpenChange = { setIsOpen } >
109- < DialogContent className = "max-w-[min(96vw,900px)] gap-4 p-4 sm:max-w-4xl" >
110- < DialogHeader className = "pr-8" >
111- < DialogTitle className = "truncate text-base" > { label } </ DialogTitle >
112- </ DialogHeader >
113- < div className = "flex max-h-[78vh] min-h-0 items-center justify-center overflow-hidden rounded-lg bg-muted/40" >
114- < img alt = { label } className = "max-h-[78vh] max-w-full object-contain" src = { data . url } />
115- </ div >
116- </ DialogContent >
117- </ Dialog >
113+ { imageUrl ? (
114+ < Dialog open = { isOpen } onOpenChange = { setIsOpen } >
115+ < DialogContent className = "max-w-[min(96vw,900px)] gap-4 p-4 sm:max-w-4xl" >
116+ < DialogHeader className = "pr-8" >
117+ < DialogTitle className = "truncate text-base" > { label } </ DialogTitle >
118+ </ DialogHeader >
119+ < div className = "flex max-h-[78vh] min-h-0 items-center justify-center overflow-hidden rounded-lg bg-muted/40" >
120+ < img alt = { label } className = "max-h-[78vh] max-w-full object-contain" src = { imageUrl } />
121+ </ div >
122+ </ DialogContent >
123+ </ Dialog >
124+ ) : null }
118125 </ >
119126 ) ;
120127}
0 commit comments