1+ "use client" ;
2+
3+ import { PropsWithChildren , useEffect , useState , type FC } from "react" ;
4+ import { CircleXIcon , FileIcon , PaperclipIcon } from "lucide-react" ;
5+ import {
6+ AttachmentPrimitive ,
7+ ComposerPrimitive ,
8+ MessagePrimitive ,
9+ useAttachment ,
10+ } from "@assistant-ui/react" ;
11+ import styled from "styled-components" ;
12+ import {
13+ Tooltip ,
14+ TooltipContent ,
15+ TooltipTrigger ,
16+ } from "./tooltip" ;
17+ import {
18+ Dialog ,
19+ DialogTitle ,
20+ DialogTrigger ,
21+ DialogOverlay ,
22+ DialogPortal ,
23+ DialogContent ,
24+ } from "./dialog" ;
25+ import { Avatar , AvatarImage , AvatarFallback } from "./avatar" ;
26+ import { TooltipIconButton } from "../assistant-ui/tooltip-icon-button" ;
27+
28+ // ============================================================================
29+ // STYLED COMPONENTS
30+ // ============================================================================
31+
32+ const StyledDialogTrigger = styled ( DialogTrigger ) `
33+ cursor: pointer;
34+ transition: background-color 0.2s;
35+ padding: 2px;
36+ border-radius: 4px;
37+
38+ &:hover {
39+ background-color: rgba(0, 0, 0, 0.05);
40+ }
41+ ` ;
42+
43+ const StyledAvatar = styled ( Avatar ) `
44+ background-color: #f1f5f9;
45+ display: flex;
46+ width: 40px;
47+ height: 40px;
48+ align-items: center;
49+ justify-content: center;
50+ border-radius: 8px;
51+ border: 1px solid #e2e8f0;
52+ font-size: 14px;
53+ ` ;
54+
55+ const AttachmentContainer = styled . div `
56+ display: flex;
57+ height: 48px;
58+ width: 160px;
59+ align-items: center;
60+ justify-content: center;
61+ gap: 8px;
62+ border-radius: 8px;
63+ border: 1px solid #e2e8f0;
64+ padding: 4px;
65+ ` ;
66+
67+ const AttachmentTextContainer = styled . div `
68+ flex-grow: 1;
69+ flex-basis: 0;
70+ overflow: hidden;
71+ ` ;
72+
73+ const AttachmentName = styled . p `
74+ color: #64748b;
75+ font-size: 12px;
76+ font-weight: bold;
77+ overflow: hidden;
78+ text-overflow: ellipsis;
79+ white-space: nowrap;
80+ word-break: break-all;
81+ margin: 0;
82+ line-height: 16px;
83+ ` ;
84+
85+ const AttachmentType = styled . p `
86+ color: #64748b;
87+ font-size: 12px;
88+ margin: 0;
89+ line-height: 16px;
90+ ` ;
91+
92+ const AttachmentRoot = styled ( AttachmentPrimitive . Root ) `
93+ position: relative;
94+ margin-top: 12px;
95+ ` ;
96+
97+ const StyledTooltipIconButton = styled ( TooltipIconButton ) `
98+ color: #64748b;
99+ position: absolute;
100+ right: -12px;
101+ top: -12px;
102+ width: 24px;
103+ height: 24px;
104+
105+ & svg {
106+ background-color: white;
107+ width: 16px;
108+ height: 16px;
109+ border-radius: 50%;
110+ }
111+ ` ;
112+
113+ const UserAttachmentsContainer = styled . div `
114+ display: flex;
115+ width: 100%;
116+ flex-direction: row;
117+ gap: 12px;
118+ grid-column: 1 / -1;
119+ grid-row-start: 1;
120+ justify-content: flex-end;
121+ ` ;
122+
123+ const ComposerAttachmentsContainer = styled . div `
124+ display: flex;
125+ width: 100%;
126+ flex-direction: row;
127+ gap: 12px;
128+ overflow-x: auto;
129+ ` ;
130+
131+ const StyledComposerButton = styled ( TooltipIconButton ) `
132+ margin: 10px 0;
133+ width: 32px;
134+ height: 32px;
135+ padding: 8px;
136+ transition: opacity 0.2s ease-in;
137+ ` ;
138+
139+ const ScreenReaderOnly = styled . span `
140+ position: absolute;
141+ left: -10000px;
142+ width: 1px;
143+ height: 1px;
144+ overflow: hidden;
145+ ` ;
146+
147+ // ============================================================================
148+ // UTILITY HOOKS
149+ // ============================================================================
150+
151+ // Simple replacement for useShallow (removes zustand dependency)
152+ const useShallow = < T , > ( selector : ( state : any ) => T ) : ( ( state : any ) => T ) => selector ;
153+
154+ const useFileSrc = ( file : File | undefined ) => {
155+ const [ src , setSrc ] = useState < string | undefined > ( undefined ) ;
156+
157+ useEffect ( ( ) => {
158+ if ( ! file ) {
159+ setSrc ( undefined ) ;
160+ return ;
161+ }
162+
163+ const objectUrl = URL . createObjectURL ( file ) ;
164+ setSrc ( objectUrl ) ;
165+
166+ return ( ) => {
167+ URL . revokeObjectURL ( objectUrl ) ;
168+ } ;
169+ } , [ file ] ) ;
170+
171+ return src ;
172+ } ;
173+
174+ const useAttachmentSrc = ( ) => {
175+ const { file, src } = useAttachment (
176+ useShallow ( ( a ) : { file ?: File ; src ?: string } => {
177+ if ( a . type !== "image" ) return { } ;
178+ if ( a . file ) return { file : a . file } ;
179+ const src = a . content ?. filter ( ( c : any ) => c . type === "image" ) [ 0 ] ?. image ;
180+ if ( ! src ) return { } ;
181+ return { src } ;
182+ } )
183+ ) ;
184+
185+ return useFileSrc ( file ) ?? src ;
186+ } ;
187+
188+ // ============================================================================
189+ // ATTACHMENT COMPONENTS
190+ // ============================================================================
191+
192+ type AttachmentPreviewProps = {
193+ src : string ;
194+ } ;
195+
196+ const AttachmentPreview : FC < AttachmentPreviewProps > = ( { src } ) => {
197+ const [ isLoaded , setIsLoaded ] = useState ( false ) ;
198+
199+ return (
200+ < img
201+ src = { src }
202+ style = { {
203+ width : "auto" ,
204+ height : "auto" ,
205+ maxWidth : "75dvh" ,
206+ maxHeight : "75dvh" ,
207+ display : isLoaded ? "block" : "none" ,
208+ overflow : "clip" ,
209+ } }
210+ onLoad = { ( ) => setIsLoaded ( true ) }
211+ alt = "Preview"
212+ />
213+ ) ;
214+ } ;
215+
216+ const AttachmentPreviewDialog : FC < PropsWithChildren > = ( { children } ) => {
217+ const src = useAttachmentSrc ( ) ;
218+
219+ if ( ! src ) return < > { children } </ > ;
220+
221+ return (
222+ < Dialog >
223+ < StyledDialogTrigger asChild >
224+ { children }
225+ </ StyledDialogTrigger >
226+ < AttachmentDialogContent >
227+ < DialogTitle >
228+ < ScreenReaderOnly > Image Attachment Preview</ ScreenReaderOnly >
229+ </ DialogTitle >
230+ < AttachmentPreview src = { src } />
231+ </ AttachmentDialogContent >
232+ </ Dialog >
233+ ) ;
234+ } ;
235+
236+ const AttachmentThumb : FC = ( ) => {
237+ const isImage = useAttachment ( ( a ) => a . type === "image" ) ;
238+ const src = useAttachmentSrc ( ) ;
239+ return (
240+ < StyledAvatar >
241+ < AvatarFallback delayMs = { isImage ? 200 : 0 } >
242+ < FileIcon />
243+ </ AvatarFallback >
244+ < AvatarImage src = { src } />
245+ </ StyledAvatar >
246+ ) ;
247+ } ;
248+
249+ const AttachmentUI : FC = ( ) => {
250+ const canRemove = useAttachment ( ( a ) => a . source !== "message" ) ;
251+ const typeLabel = useAttachment ( ( a ) => {
252+ const type = a . type ;
253+ switch ( type ) {
254+ case "image" :
255+ return "Image" ;
256+ case "document" :
257+ return "Document" ;
258+ case "file" :
259+ return "File" ;
260+ default :
261+ const _exhaustiveCheck : never = type ;
262+ throw new Error ( `Unknown attachment type: ${ _exhaustiveCheck } ` ) ;
263+ }
264+ } ) ;
265+
266+ return (
267+ < Tooltip >
268+ < AttachmentRoot >
269+ < AttachmentPreviewDialog >
270+ < TooltipTrigger asChild >
271+ < AttachmentContainer >
272+ < AttachmentThumb />
273+ < AttachmentTextContainer >
274+ < AttachmentName >
275+ < AttachmentPrimitive . Name />
276+ </ AttachmentName >
277+ < AttachmentType > { typeLabel } </ AttachmentType >
278+ </ AttachmentTextContainer >
279+ </ AttachmentContainer >
280+ </ TooltipTrigger >
281+ </ AttachmentPreviewDialog >
282+ { canRemove && < AttachmentRemove /> }
283+ </ AttachmentRoot >
284+ < TooltipContent side = "top" >
285+ < AttachmentPrimitive . Name />
286+ </ TooltipContent >
287+ </ Tooltip >
288+ ) ;
289+ } ;
290+
291+ const AttachmentRemove : FC = ( ) => {
292+ return (
293+ < AttachmentPrimitive . Remove asChild >
294+ < StyledTooltipIconButton
295+ tooltip = "Remove file"
296+ side = "top"
297+ >
298+ < CircleXIcon />
299+ </ StyledTooltipIconButton >
300+ </ AttachmentPrimitive . Remove >
301+ ) ;
302+ } ;
303+
304+ // ============================================================================
305+ // EXPORTED COMPONENTS
306+ // ============================================================================
307+
308+ export const UserMessageAttachments : FC = ( ) => {
309+ return (
310+ < UserAttachmentsContainer >
311+ < MessagePrimitive . Attachments components = { { Attachment : AttachmentUI } } />
312+ </ UserAttachmentsContainer >
313+ ) ;
314+ } ;
315+
316+ export const ComposerAttachments : FC = ( ) => {
317+ return (
318+ < ComposerAttachmentsContainer >
319+ < ComposerPrimitive . Attachments
320+ components = { { Attachment : AttachmentUI } }
321+ />
322+ </ ComposerAttachmentsContainer >
323+ ) ;
324+ } ;
325+
326+ export const ComposerAddAttachment : FC = ( ) => {
327+ return (
328+ < ComposerPrimitive . AddAttachment asChild >
329+ < StyledComposerButton
330+ tooltip = "Add Attachment"
331+ variant = "ghost"
332+ >
333+ < PaperclipIcon />
334+ </ StyledComposerButton >
335+ </ ComposerPrimitive . AddAttachment >
336+ ) ;
337+ } ;
338+
339+ const AttachmentDialogContent : FC < PropsWithChildren > = ( { children } ) => (
340+ < DialogPortal >
341+ < DialogOverlay />
342+ < DialogContent className = "aui-dialog-content" >
343+ { children }
344+ </ DialogContent >
345+ </ DialogPortal >
346+ ) ;
0 commit comments