diff --git a/client/src/components/Chat/Input/Files/FileRow.tsx b/client/src/components/Chat/Input/Files/FileRow.tsx index a7c07123cc..7792fc23f3 100644 --- a/client/src/components/Chat/Input/Files/FileRow.tsx +++ b/client/src/components/Chat/Input/Files/FileRow.tsx @@ -73,8 +73,9 @@ export default function FileRow({ } const renderFiles = () => { - // Inline style for RTL - const rowStyle = isRTL ? { display: 'flex', flexDirection: 'row-reverse' } : {}; + const rowStyle = isRTL + ? { display: 'flex', flexDirection: 'row-reverse', gap: '4px' } + : { display: 'flex', gap: '4px' }; return (
diff --git a/client/src/components/Chat/Input/Files/ImagePreview.tsx b/client/src/components/Chat/Input/Files/ImagePreview.tsx index 2876c2aef7..00adc643ce 100644 --- a/client/src/components/Chat/Input/Files/ImagePreview.tsx +++ b/client/src/components/Chat/Input/Files/ImagePreview.tsx @@ -1,3 +1,5 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Maximize2 } from 'lucide-react'; import { FileSources } from 'librechat-data-provider'; import ProgressCircle from './ProgressCircle'; import SourceIcon from './SourceIcon'; @@ -10,67 +12,199 @@ type styleProps = { backgroundRepeat?: string; }; +interface CloseModalEvent { + stopPropagation: () => void; + preventDefault: () => void; +} + const ImagePreview = ({ imageBase64, url, progress = 1, className = '', source, + alt = 'Preview image', }: { imageBase64?: string; url?: string; - progress?: number; // between 0 and 1 + progress?: number; className?: string; source?: FileSources; + alt?: string; }) => { - let style: styleProps = { + const [isModalOpen, setIsModalOpen] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const [previousActiveElement, setPreviousActiveElement] = useState(null); + + const openModal = useCallback(() => { + setPreviousActiveElement(document.activeElement); + setIsModalOpen(true); + }, []); + + const closeModal = useCallback( + (e: CloseModalEvent): void => { + setIsModalOpen(false); + e.stopPropagation(); + e.preventDefault(); + + if ( + previousActiveElement instanceof HTMLElement && + !previousActiveElement.closest('[data-skip-refocus="true"]') + ) { + previousActiveElement.focus(); + } + }, + [previousActiveElement], + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + closeModal(e); + } + }, + [closeModal], + ); + + useEffect(() => { + if (isModalOpen) { + document.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + const closeButton = document.querySelector('[aria-label="Close full view"]') as HTMLElement; + if (closeButton) { + setTimeout(() => closeButton.focus(), 0); + } + } + + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'unset'; + }; + }, [isModalOpen, handleKeyDown]); + + const baseStyle: styleProps = { backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat', }; - if (imageBase64) { - style = { - ...style, - backgroundImage: `url(${imageBase64})`, - }; - } else if (url) { - style = { - ...style, - backgroundImage: `url(${url})`, - }; - } - if (!style.backgroundImage) { + const imageUrl = imageBase64 ?? url ?? ''; + + const style: styleProps = imageUrl + ? { + ...baseStyle, + backgroundImage: `url(${imageUrl})`, + } + : baseStyle; + + if (typeof style.backgroundImage !== 'string' || style.backgroundImage.length === 0) { return null; } - const radius = 55; // Radius of the SVG circle + const radius = 55; const circumference = 2 * Math.PI * radius; - - // Calculate the offset based on the loading progress const offset = circumference - progress * circumference; const circleCSSProperties = { transition: 'stroke-dashoffset 0.3s linear', }; return ( -
-
+ + {isModalOpen && ( +
+
+ +
+ {alt} e.stopPropagation()} + /> +
+
+
)} - -
+ ); }; diff --git a/client/src/components/Chat/Input/Files/RemoveFile.tsx b/client/src/components/Chat/Input/Files/RemoveFile.tsx index 31dccf4e30..8eb5507c6f 100644 --- a/client/src/components/Chat/Input/Files/RemoveFile.tsx +++ b/client/src/components/Chat/Input/Files/RemoveFile.tsx @@ -2,7 +2,7 @@ export default function RemoveFile({ onRemove }: { onRemove: () => void }) { return ( - +
+ + + + +
{localize('com_ui_bookmarks_title')}
+
+ +
{localize('com_ui_bookmarks_count')}
+
+
+
+ {currentRows.map((row) => renderRow(row))} +
+
+
+
+ {localize('com_ui_page')} {pageIndex + 1} {localize('com_ui_of')}{' '} + {Math.ceil(filteredRows.length / pageSize)} +
+
+ + +
diff --git a/client/src/components/SidePanel/Files/PanelColumns.tsx b/client/src/components/SidePanel/Files/PanelColumns.tsx index 3fc6e94180..d8fc15f6c6 100644 --- a/client/src/components/SidePanel/Files/PanelColumns.tsx +++ b/client/src/components/SidePanel/Files/PanelColumns.tsx @@ -12,6 +12,7 @@ export const columns: ColumnDef[] = [ return ( + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const isFilenameCell = cell.column.id === 'filename'; + + return ( + { + if (isFilenameCell) { + const clickedElement = e.target as HTMLElement; + // Check if clicked element is within cell and not a button/link + if ( + clickedElement.closest('td') && + !clickedElement.closest('button, a') + ) { + e.preventDefault(); + e.stopPropagation(); + handleFileClick(row.original as TFile); + } + } + }} + onKeyDown={(e) => { + if (isFilenameCell && (e.key === 'Enter' || e.key === ' ')) { + const clickedElement = e.target as HTMLElement; + if ( + clickedElement.closest('td') && + !clickedElement.closest('button, a') + ) { + e.preventDefault(); + e.stopPropagation(); + handleFileClick(row.original as TFile); + } + } + }} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + + )) + ) : ( + + + {localize('com_files_no_results')} + + + )} + + -
+
+ +
+ + +
+
+ {`${pageIndex + 1} / ${table.getPageCount()}`} +
- + ); } diff --git a/client/src/components/ui/Button.tsx b/client/src/components/ui/Button.tsx index e86c261ac8..7ebe99e7ca 100644 --- a/client/src/components/ui/Button.tsx +++ b/client/src/components/ui/Button.tsx @@ -4,14 +4,14 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '~/utils'; const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/80', outline: - 'text-text-primary border border-input bg-background hover:bg-accent hover:text-accent-foreground', + 'text-text-primary border border-border-light bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', @@ -19,8 +19,8 @@ const buttonVariants = cva( }, size: { default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', + sm: 'h-9 rounded-lg px-3', + lg: 'h-11 rounded-lg px-8', icon: 'size-10', }, }, diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index 15e8c18070..3b309af9fd 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -438,6 +438,7 @@ export default { com_ui_no_conversation_id: 'No conversation ID found', com_ui_add_multi_conversation: 'Add multi-conversation', com_ui_duplicate_agent_confirm: 'Are you sure you want to duplicate this agent?', + com_ui_page: 'Page', com_auth_error_login: 'Unable to login with the information provided. Please check your credentials and try again.', com_auth_error_login_rl: diff --git a/package-lock.json b/package-lock.json index 7d6d252f43..34437fbf57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34920,9 +34920,10 @@ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" }, "node_modules/use-callback-ref": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", - "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -34930,8 +34931,8 @@ "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": {