-
Notifications
You must be signed in to change notification settings - Fork 20
feat(frontend): better ui for long loading files #983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b42742c
5517ec0
14376e4
5eaebab
cd31a45
ec3c413
530cddf
1e4ab99
1a2a35d
6636648
8142f70
292b7ec
86ae55d
25de073
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -35,7 +35,6 @@ export function ChatInput({ textareaRef, chatId, configuration, isDisabled, isEm | |||||||||||||||||||||||||
| const { updateContext, context } = useExtensionContext(chatId); | ||||||||||||||||||||||||||
| const [defaultValues, setDefaultValues] = useState<UserArgumentDefaultValueByExtensionIDAndName>({}); | ||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||
| uploadingFiles, | ||||||||||||||||||||||||||
| fullFileSlots, | ||||||||||||||||||||||||||
| allowedFileNameExtensions, | ||||||||||||||||||||||||||
| chatFiles, | ||||||||||||||||||||||||||
|
|
@@ -202,12 +201,41 @@ export function ChatInput({ textareaRef, chatId, configuration, isDisabled, isEm | |||||||||||||||||||||||||
| {isEmpty && <Suggestions configuration={configuration} theme={theme} onSelect={setInput} />} | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <div className="flex flex-wrap gap-2"> | ||||||||||||||||||||||||||
| {chatFiles.map((file) => ( | ||||||||||||||||||||||||||
| <FileItemComponent key={file.id} file={file} onRemove={remove} /> | ||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||
| {uploadingFiles.map((file, n) => ( | ||||||||||||||||||||||||||
| <FileItemComponent key={`${n}-${file.name}`} file={{ fileName: file.name }} loading={true} /> | ||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||
| {(() => { | ||||||||||||||||||||||||||
| const uploadingFilesWithMetadata = uploadMutations | ||||||||||||||||||||||||||
| .filter((m) => m.status === 'pending') | ||||||||||||||||||||||||||
| .map((m, index) => ({ | ||||||||||||||||||||||||||
| fileName: m.variables?.file?.name || '', | ||||||||||||||||||||||||||
| isUploading: true, | ||||||||||||||||||||||||||
| originalFile: m.variables?.file, | ||||||||||||||||||||||||||
| uploadTime: new Date(), | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| uploadTime: new Date(), | |
| uploadTime: new Date(m.submittedAt || 0), |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uniqueKey includes the index from the filtered pending mutations list. When one pending upload settles, indices of the remaining items can shift, changing keys and remounting FileItemComponent (which will reset the “show after N seconds” animation/timer). Prefer a key derived only from stable mutation identity (e.g., submittedAt plus file name) rather than the array index.
| .map((m, index) => ({ | |
| fileName: m.variables?.file?.name || '', | |
| isUploading: true, | |
| originalFile: m.variables?.file, | |
| uploadTime: new Date(), | |
| uniqueKey: `${m.submittedAt}-${index}`, | |
| .map((m) => ({ | |
| fileName: m.variables?.file?.name || '', | |
| isUploading: true, | |
| originalFile: m.variables?.file, | |
| uploadTime: new Date(), | |
| uniqueKey: `${m.submittedAt}-${m.variables?.file?.name || ''}`, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| import { IconFile, IconRotate2, IconTrash } from '@tabler/icons-react'; | ||
| import React from 'react'; | ||
| import React, { useState } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { FileDto } from 'src/api'; | ||
| import { cn } from 'src/lib'; | ||
| import { extractType } from 'src/pages/utils'; | ||
| import { texts } from 'src/texts'; | ||
|
|
||
| type FileItemProps = { | ||
| file: FileDto | { fileName: string }; | ||
|
|
@@ -10,26 +13,35 @@ type FileItemProps = { | |
| }; | ||
|
|
||
| export const FileItemComponent = ({ file, onRemove, loading }: FileItemProps) => { | ||
| const [isDeleting, setIsDeleting] = useState(false); | ||
| const { i18n } = useTranslation(); | ||
| const fileName = file.fileName; | ||
| const fileType = 'mimeType' in file ? extractType(file) : undefined; | ||
|
|
||
| const handleRemove = (e: React.MouseEvent) => { | ||
| e.preventDefault(); | ||
| if (onRemove && 'id' in file) { | ||
| if (onRemove && 'id' in file && !isDeleting) { | ||
| setIsDeleting(true); | ||
| onRemove(file); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| className="group relative flex w-48 items-center gap-2 overflow-clip rounded-md bg-gray-100 p-2 shadow-sm" | ||
| className={cn( | ||
| 'group relative flex w-48 items-center gap-2 overflow-clip rounded-md p-2 shadow-sm transition-all', | ||
| isDeleting ? 'bg-gray-50' : 'bg-gray-100', | ||
| )} | ||
| data-testid="file-chip" | ||
| > | ||
| <div className="flex w-full items-center gap-2" data-testid={loading ? 'file-chip-uploading' : 'file-chip-uploaded'}> | ||
| <div className="relative h-6 w-6 flex-shrink-0"> | ||
| {loading ? <IconRotate2 className="loading w-6" /> : <IconFile />} | ||
| <div | ||
| className={cn('flex w-full items-center gap-2', isDeleting && 'opacity-50')} | ||
| data-testid={loading ? 'file-chip-uploading' : 'file-chip-uploaded'} | ||
| > | ||
| <div className="relative h-10 w-10 flex-shrink-0 p-1"> | ||
| {loading ? <IconRotate2 className="loading h-full w-full" /> : <IconFile className="h-full w-full" />} | ||
| {fileType && ( | ||
| <span className="absolute -right-1 -bottom-1 truncate rounded-md bg-black px-[3px] py-[1px] text-[8px] text-white"> | ||
| <span className="absolute right-0 bottom-0 truncate rounded-md bg-black px-[3px] py-[1px] text-[8px] text-white"> | ||
| {fileType} | ||
| </span> | ||
| )} | ||
|
|
@@ -38,15 +50,36 @@ export const FileItemComponent = ({ file, onRemove, loading }: FileItemProps) => | |
| <div className="min-w-0 flex-grow"> | ||
| <div className="flex flex-col"> | ||
| <span className="truncate text-sm font-medium">{fileName}</span> | ||
| {loading && ( | ||
| <div className="animate-show-after-7s relative h-5"> | ||
| <span className="animate-alternate-first absolute top-0 right-0 left-0 truncate text-sm font-medium text-orange-800 italic"> | ||
| {texts.files.waitingMessage1} | ||
| </span> | ||
| <span className="animate-alternate-second absolute top-0 right-0 left-0 truncate text-sm font-medium text-orange-800 italic"> | ||
| {texts.files.waitingMessage2} | ||
| </span> | ||
|
Comment on lines
+54
to
+60
|
||
| </div> | ||
| )} | ||
|
Comment on lines
+53
to
+62
|
||
| {!loading && 'uploadedAt' in file && file.uploadedAt && ( | ||
| <div className="text-xs text-gray-500"> | ||
| {new Date(file.uploadedAt).toLocaleString(i18n.language === 'de' ? 'de-DE' : undefined)} | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| {!loading && onRemove && ( | ||
| <div className="absolute top-0 right-0 bottom-0 flex items-center bg-gray-100 p-1 pr-2 opacity-0 transition-all group-hover:opacity-100"> | ||
| <button className="text-red-500" onClick={handleRemove}> | ||
| <IconTrash className="h-4 w-4" /> | ||
| </button> | ||
| </div> | ||
| <button | ||
| className={cn( | ||
| 'absolute top-0 right-0 bottom-0 flex items-center bg-gray-100 p-1 pr-2 text-red-500 opacity-0 transition-all group-hover:opacity-100 hover:bg-gray-200', | ||
| isDeleting && 'cursor-not-allowed opacity-50', | ||
| )} | ||
| onClick={handleRemove} | ||
| disabled={isDeleting} | ||
| aria-label="Delete file" | ||
| > | ||
| <IconTrash className="h-4 w-4" /> | ||
| </button> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CSS class name
animate-show-after-7simplies a 7s delay, but it currently runsshow-after-delayfor 5.5s (with the element only becoming visible at the end). This will show the “long upload” hint earlier than intended and is likely out of sync with the 7s requirement; consider implementing an actual 7s delay (e.g., viaanimation-delay: 7s) and aligning the keyframes accordingly.