11'use client'
22
33import { useCallback , useEffect , useRef , useState } from 'react'
4- import { ArrowUp , FileText , Loader2 , Mic , Paperclip , X } from 'lucide-react'
5- import { Button } from '@/components/emcn'
4+ import { ArrowUp , Loader2 , Mic , Paperclip , X } from 'lucide-react'
5+ import { Button , Tooltip } from '@/components/emcn'
6+ import {
7+ AudioIcon ,
8+ CsvIcon ,
9+ DocxIcon ,
10+ getDocumentIcon ,
11+ JsonIcon ,
12+ MarkdownIcon ,
13+ PdfIcon ,
14+ TxtIcon ,
15+ VideoIcon ,
16+ XlsxIcon ,
17+ } from '@/components/icons/document-icons'
618import { cn } from '@/lib/core/utils/cn'
719import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
820import { useFileAttachments } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'
@@ -23,7 +35,19 @@ const SEND_BUTTON_ACTIVE =
2335 'bg-[var(--c-383838)] hover:bg-[var(--c-575757)] dark:bg-[var(--c-E0E0E0)] dark:hover:bg-[var(--c-CFCFCF)]'
2436const SEND_BUTTON_DISABLED = 'bg-[var(--c-808080)] dark:bg-[var(--c-808080)]'
2537
26- const MAX_CHAT_TEXTAREA_HEIGHT = 200 // 8 lines × 24px line-height + 8px padding
38+ const MAX_CHAT_TEXTAREA_HEIGHT = 200
39+
40+ const DROP_OVERLAY_ICONS = [
41+ PdfIcon ,
42+ DocxIcon ,
43+ XlsxIcon ,
44+ CsvIcon ,
45+ TxtIcon ,
46+ MarkdownIcon ,
47+ JsonIcon ,
48+ AudioIcon ,
49+ VideoIcon ,
50+ ] as const
2751
2852function autoResizeTextarea ( e : React . FormEvent < HTMLTextAreaElement > , maxHeight : number ) {
2953 const target = e . target as HTMLTextAreaElement
@@ -189,9 +213,8 @@ export function UserInput({
189213 < div
190214 onClick = { handleContainerClick }
191215 className = { cn (
192- 'mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]' ,
193- isInitialView && 'shadow-sm' ,
194- files . isDragging && 'ring-[1.75px] ring-[var(--brand-secondary)]'
216+ 'relative mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]' ,
217+ isInitialView && 'shadow-sm'
195218 ) }
196219 onDragEnter = { files . handleDragEnter }
197220 onDragLeave = { files . handleDragLeave }
@@ -204,48 +227,52 @@ export function UserInput({
204227 { files . attachedFiles . map ( ( file ) => {
205228 const isImage = file . type . startsWith ( 'image/' )
206229 return (
207- < div
208- key = { file . id }
209- className = 'group relative h-[56px] w-[56px] flex-shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-5)] hover:bg-[var(--surface-4)]'
210- title = { `${ file . name } (${ files . formatFileSize ( file . size ) } )` }
211- onClick = { ( ) => files . handleFileClick ( file ) }
212- >
213- { isImage && file . previewUrl ? (
214- < img
215- src = { file . previewUrl }
216- alt = { file . name }
217- className = 'h-full w-full object-cover'
218- />
219- ) : (
220- < div className = 'flex h-full w-full flex-col items-center justify-center gap-[2px]' >
221- { file . type . includes ( 'pdf' ) ? (
222- < FileText className = 'h-[18px] w-[18px] text-red-500' />
230+ < Tooltip . Root key = { file . id } >
231+ < Tooltip . Trigger asChild >
232+ < div
233+ className = 'group relative h-[56px] w-[56px] flex-shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-5)] hover:bg-[var(--surface-4)]'
234+ onClick = { ( ) => files . handleFileClick ( file ) }
235+ >
236+ { isImage && file . previewUrl ? (
237+ < img
238+ src = { file . previewUrl }
239+ alt = { file . name }
240+ className = 'h-full w-full object-cover'
241+ />
223242 ) : (
224- < FileText className = 'h-[18px] w-[18px] text-blue-500' />
243+ < div className = 'flex h-full w-full flex-col items-center justify-center gap-[2px] text-[var(--text-icon)]' >
244+ { ( ( ) => {
245+ const Icon = getDocumentIcon ( file . type , file . name )
246+ return < Icon className = 'h-[18px] w-[18px]' />
247+ } ) ( ) }
248+ < span className = 'max-w-[48px] truncate px-[2px] text-[9px] text-[var(--text-muted)]' >
249+ { file . name . split ( '.' ) . pop ( ) }
250+ </ span >
251+ </ div >
252+ ) }
253+ { file . uploading && (
254+ < div className = 'absolute inset-0 flex items-center justify-center bg-black/50' >
255+ < Loader2 className = 'h-[14px] w-[14px] animate-spin text-white' />
256+ </ div >
257+ ) }
258+ { ! file . uploading && (
259+ < button
260+ type = 'button'
261+ onClick = { ( e ) => {
262+ e . stopPropagation ( )
263+ files . removeFile ( file . id )
264+ } }
265+ className = 'absolute top-[2px] right-[2px] flex h-[16px] w-[16px] items-center justify-center rounded-full bg-black/60 opacity-0 group-hover:opacity-100'
266+ >
267+ < X className = 'h-[10px] w-[10px] text-white' />
268+ </ button >
225269 ) }
226- < span className = 'max-w-[48px] truncate px-[2px] text-[9px] text-[var(--text-muted)]' >
227- { file . name . split ( '.' ) . pop ( ) }
228- </ span >
229- </ div >
230- ) }
231- { file . uploading && (
232- < div className = 'absolute inset-0 flex items-center justify-center bg-black/50' >
233- < Loader2 className = 'h-[14px] w-[14px] animate-spin text-white' />
234270 </ div >
235- ) }
236- { ! file . uploading && (
237- < button
238- type = 'button'
239- onClick = { ( e ) => {
240- e . stopPropagation ( )
241- files . removeFile ( file . id )
242- } }
243- className = 'absolute top-[2px] right-[2px] flex h-[16px] w-[16px] items-center justify-center rounded-full bg-black/60 opacity-0 group-hover:opacity-100'
244- >
245- < X className = 'h-[10px] w-[10px] text-white' />
246- </ button >
247- ) }
248- </ div >
271+ </ Tooltip . Trigger >
272+ < Tooltip . Content side = 'top' >
273+ < p className = 'max-w-[200px] truncate' > { file . name } </ p >
274+ </ Tooltip . Content >
275+ </ Tooltip . Root >
249276 )
250277 } ) }
251278 </ div >
@@ -257,7 +284,7 @@ export function UserInput({
257284 onChange = { ( e ) => setValue ( e . target . value ) }
258285 onKeyDown = { handleKeyDown }
259286 onInput = { handleInput }
260- placeholder = { files . isDragging ? 'Drop files here...' : placeholder }
287+ placeholder = { placeholder }
261288 rows = { 1 }
262289 className = { cn ( TEXTAREA_BASE_CLASSES , isInitialView ? 'max-h-[30vh]' : 'max-h-[200px]' ) }
263290 />
@@ -319,7 +346,6 @@ export function UserInput({
319346 </ div >
320347 </ div >
321348
322- { /* Hidden file input */ }
323349 < input
324350 ref = { files . fileInputRef }
325351 type = 'file'
@@ -328,6 +354,19 @@ export function UserInput({
328354 accept = { CHAT_ACCEPT_ATTRIBUTE }
329355 multiple
330356 />
357+
358+ { files . isDragging && (
359+ < div className = 'pointer-events-none absolute inset-[6px] z-10 flex items-center justify-center rounded-[14px] border-[1.5px] border-[var(--border-1)] border-dashed bg-[var(--white)] dark:bg-[var(--surface-4)]' >
360+ < div className = 'flex flex-col items-center gap-[8px]' >
361+ < span className = 'font-medium text-[13px] text-[var(--text-secondary)]' > Drop files</ span >
362+ < div className = 'flex items-center gap-[8px] text-[var(--text-icon)]' >
363+ { DROP_OVERLAY_ICONS . map ( ( Icon , i ) => (
364+ < Icon key = { i } className = 'h-[14px] w-[14px]' />
365+ ) ) }
366+ </ div >
367+ </ div >
368+ </ div >
369+ ) }
331370 </ div >
332371 )
333372}
0 commit comments