|
2 | 2 |
|
3 | 3 | import type React from 'react' |
4 | 4 | import { useLayoutEffect, useRef, useState } from 'react' |
5 | | -import { Badge, Button, cn, Tooltip } from '@sim/emcn' |
| 5 | +import { |
| 6 | + Badge, |
| 7 | + Button, |
| 8 | + cn, |
| 9 | + scrollFadeAttributes, |
| 10 | + scrollFadeClass, |
| 11 | + Tooltip, |
| 12 | + useScrollEdges, |
| 13 | +} from '@sim/emcn' |
6 | 14 | import { ArrowUp, Paperclip, StopFilled, X } from '@sim/emcn/icons' |
7 | 15 | import { createLogger } from '@sim/logger' |
8 | 16 | import { generateId } from '@sim/utils/id' |
@@ -30,11 +38,13 @@ export const ChatInput: React.FC<{ |
30 | 38 | }> = ({ onSubmit, isStreaming = false, onStopStreaming }) => { |
31 | 39 | const fileInputRef = useRef<HTMLInputElement>(null) |
32 | 40 | const textareaRef = useRef<HTMLTextAreaElement>(null) |
| 41 | + const attachmentsRef = useRef<HTMLDivElement>(null) |
33 | 42 | const [inputValue, setInputValue] = useState('') |
34 | 43 | const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]) |
35 | 44 | const [uploadErrors, setUploadErrors] = useState<string[]>([]) |
36 | 45 | const [dragCounter, setDragCounter] = useState(0) |
37 | 46 | const isDragOver = dragCounter > 0 |
| 47 | + const attachmentEdges = useScrollEdges(attachmentsRef, { enabled: attachedFiles.length > 0 }) |
38 | 48 |
|
39 | 49 | useLayoutEffect(() => { |
40 | 50 | const el = textareaRef.current |
@@ -172,45 +182,54 @@ export const ChatInput: React.FC<{ |
172 | 182 | }} |
173 | 183 | > |
174 | 184 | {attachedFiles.length > 0 && ( |
175 | | - <div className='mb-1.5 flex flex-wrap gap-1.5 max-md:max-h-24 max-md:overflow-y-auto max-md:overscroll-contain'> |
176 | | - {attachedFiles.map((file) => ( |
177 | | - <Tooltip.Root key={file.id}> |
178 | | - <Tooltip.Trigger asChild> |
179 | | - <div className='group relative size-[56px] shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-3)]'> |
180 | | - {file.dataUrl ? ( |
181 | | - <img |
182 | | - src={file.dataUrl} |
183 | | - alt={file.name} |
184 | | - className='size-full object-cover' |
185 | | - /> |
186 | | - ) : ( |
187 | | - <div className='flex size-full flex-col items-center justify-center gap-0.5 text-[var(--text-muted)]'> |
188 | | - <Paperclip className='size-[18px]' /> |
189 | | - <span className='max-w-[48px] truncate px-[2px] text-[9px]'> |
190 | | - {file.name.split('.').pop()} |
| 185 | + <div |
| 186 | + ref={attachmentsRef} |
| 187 | + className={cn( |
| 188 | + 'mb-1.5 max-md:max-h-24 max-md:overflow-y-auto max-md:overscroll-contain', |
| 189 | + scrollFadeClass |
| 190 | + )} |
| 191 | + {...scrollFadeAttributes(attachmentEdges)} |
| 192 | + > |
| 193 | + <div className='flex flex-wrap gap-1.5'> |
| 194 | + {attachedFiles.map((file) => ( |
| 195 | + <Tooltip.Root key={file.id}> |
| 196 | + <Tooltip.Trigger asChild> |
| 197 | + <div className='group relative size-[56px] shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-3)]'> |
| 198 | + {file.dataUrl ? ( |
| 199 | + <img |
| 200 | + src={file.dataUrl} |
| 201 | + alt={file.name} |
| 202 | + className='size-full object-cover' |
| 203 | + /> |
| 204 | + ) : ( |
| 205 | + <div className='flex size-full flex-col items-center justify-center gap-0.5 text-[var(--text-muted)]'> |
| 206 | + <Paperclip className='size-[18px]' /> |
| 207 | + <span className='max-w-[48px] truncate px-[2px] text-[9px]'> |
| 208 | + {file.name.split('.').pop()} |
| 209 | + </span> |
| 210 | + </div> |
| 211 | + )} |
| 212 | + <Button |
| 213 | + variant='ghost' |
| 214 | + aria-label={`Remove ${file.name}`} |
| 215 | + onClick={(e) => { |
| 216 | + e.stopPropagation() |
| 217 | + handleRemoveFile(file.id) |
| 218 | + }} |
| 219 | + className='absolute top-[2px] right-[2px] size-11 items-start justify-end p-0 focus-visible:opacity-100 md:pointer-fine:size-[16px] md:pointer-fine:opacity-0 md:pointer-fine:group-hover:opacity-100' |
| 220 | + > |
| 221 | + <span className='flex size-7 items-center justify-center rounded-full bg-black/60 text-white md:pointer-fine:size-4'> |
| 222 | + <X className='size-[10px]' /> |
191 | 223 | </span> |
192 | | - </div> |
193 | | - )} |
194 | | - <Button |
195 | | - variant='ghost' |
196 | | - aria-label={`Remove ${file.name}`} |
197 | | - onClick={(e) => { |
198 | | - e.stopPropagation() |
199 | | - handleRemoveFile(file.id) |
200 | | - }} |
201 | | - className='absolute top-[2px] right-[2px] size-11 items-start justify-end p-0 focus-visible:opacity-100 md:pointer-fine:size-[16px] md:pointer-fine:opacity-0 md:pointer-fine:group-hover:opacity-100' |
202 | | - > |
203 | | - <span className='flex size-7 items-center justify-center rounded-full bg-black/60 text-white md:pointer-fine:size-4'> |
204 | | - <X className='size-[10px]' /> |
205 | | - </span> |
206 | | - </Button> |
207 | | - </div> |
208 | | - </Tooltip.Trigger> |
209 | | - <Tooltip.Content side='top'> |
210 | | - <p className='max-w-[200px] truncate'>{file.name}</p> |
211 | | - </Tooltip.Content> |
212 | | - </Tooltip.Root> |
213 | | - ))} |
| 224 | + </Button> |
| 225 | + </div> |
| 226 | + </Tooltip.Trigger> |
| 227 | + <Tooltip.Content side='top'> |
| 228 | + <p className='max-w-[200px] truncate'>{file.name}</p> |
| 229 | + </Tooltip.Content> |
| 230 | + </Tooltip.Root> |
| 231 | + ))} |
| 232 | + </div> |
214 | 233 | </div> |
215 | 234 | )} |
216 | 235 |
|
|
0 commit comments