diff --git a/.gitignore b/.gitignore
index 1b0f2b0171..97b1efa248 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,4 +59,4 @@ dist-ssr
.aider*
.coverage
-backend/README.md
+backend/README.md
\ No newline at end of file
diff --git a/backend/chainlit/config.py b/backend/chainlit/config.py
index ad9c1aaf88..fbe3af60b2 100644
--- a/backend/chainlit/config.py
+++ b/backend/chainlit/config.py
@@ -87,6 +87,9 @@
# Allow users to edit their own messages
edit_message = true
+# Enable lightbox for images - making it possible to view images in full screen
+image_lightbox = true
+
# Authorize users to spontaneously upload files with messages
[features.spontaneous_file_upload]
enabled = true
@@ -241,6 +244,7 @@ class FeaturesSettings(DataClassJsonMixin):
unsafe_allow_html: bool = False
auto_tag_thread: bool = True
edit_message: bool = True
+ image_lightbox: bool = True
@dataclass()
diff --git a/frontend/package.json b/frontend/package.json
index 635dbed1ce..5c98ab934a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -50,6 +50,7 @@
"unist-util-visit": "^5.0.0",
"usehooks-ts": "^2.9.1",
"uuid": "^9.0.0",
+ "yet-another-react-lightbox": "^3.21.6",
"yup": "^1.2.0"
},
"devDependencies": {
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 06dfed2047..c03cda4032 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -131,6 +131,9 @@ importers:
uuid:
specifier: ^9.0.0
version: 9.0.0
+ yet-another-react-lightbox:
+ specifier: ^3.21.6
+ version: 3.21.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
yup:
specifier: ^1.2.0
version: 1.2.0
@@ -3918,6 +3921,13 @@ packages:
engines: {node: '>= 14'}
hasBin: true
+ yet-another-react-lightbox@3.21.6:
+ resolution: {integrity: sha512-uKcRmmezsj1Fbj38B6hFOGwbAu94fPr8d5H6I0+1FmcToX56freEGXXXtdA1oRo6036ug+UgrKZzzvsw/MIM/w==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
yocto-queue@1.0.0:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
@@ -8156,6 +8166,11 @@ snapshots:
yaml@2.4.1: {}
+ yet-another-react-lightbox@3.21.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+
yocto-queue@1.0.0: {}
yup@1.2.0:
diff --git a/frontend/src/components/atoms/elements/Image.tsx b/frontend/src/components/atoms/elements/Image.tsx
index 201e450567..dfc1a0df9a 100644
--- a/frontend/src/components/atoms/elements/Image.tsx
+++ b/frontend/src/components/atoms/elements/Image.tsx
@@ -1,77 +1,90 @@
-import { useState } from 'react';
+import { Suspense, lazy, useState } from 'react';
import Skeleton from '@mui/material/Skeleton';
-import { type IImageElement } from 'client-types/';
+import { type IImageElement, useConfig } from '@chainlit/react-client';
import { FrameElement } from './Frame';
+// Lazy load the Lightbox component and its dependencies
+const LightboxWrapper = lazy(() => import('./LightboxWrapper'));
+
interface Props {
element: IImageElement;
}
-const handleImageClick = (name: string, src: string) => {
- const width = window.innerWidth / 2;
- const height = window.innerHeight / 2;
- const left = window.innerWidth / 4;
- const top = window.innerHeight / 4;
-
- const newWindow = window.open(
- '',
- '_blank',
- `width=${width},height=${height},left=${left},top=${top}`
- );
- if (newWindow) {
- newWindow.document.write(`
-
-
- ${name}
-
-
-
-
-
- Download
-
-
- `);
- newWindow.document.close();
- }
-};
-
const ImageElement = ({ element }: Props) => {
const [loading, setLoading] = useState(true);
+ const [lightboxOpen, setLightboxOpen] = useState(false);
+ const config = useConfig();
if (!element.url) {
return null;
}
+ const enableLightbox =
+ config.config?.features.image_lightbox && element.display === 'inline';
+
+ const handleImageClick = () => {
+ if (enableLightbox) {
+ setLightboxOpen(true);
+ } else {
+ // Fall back to popup window behavior
+ const width = window.innerWidth / 2;
+ const height = window.innerHeight / 2;
+ const left = window.innerWidth / 4;
+ const top = window.innerHeight / 4;
+
+ const newWindow = window.open(
+ '',
+ '_blank',
+ `width=${width},height=${height},left=${left},top=${top}`
+ );
+ if (newWindow) {
+ newWindow.document.write(`
+
+
+ ${element.name}
+
+
+
+
+
+ Download
+
+
+ `);
+ newWindow.document.close();
+ }
+ }
+ };
+
return (
{loading && }
@@ -79,23 +92,28 @@ const ImageElement = ({ element }: Props) => {
className={`${element.display}-image`}
src={element.url}
onLoad={() => setLoading(false)}
- onClick={() => {
- if (element.display === 'inline') {
- const name = `${element.name}.png`;
- handleImageClick(name, element.url!);
- }
- }}
+ onClick={handleImageClick}
style={{
objectFit: 'cover',
maxWidth: '100%',
margin: 'auto',
height: 'auto',
display: 'block',
- cursor: element.display === 'inline' ? 'pointer' : 'default'
+ cursor: enableLightbox ? 'pointer' : 'default'
}}
alt={element.name}
loading="lazy"
/>
+ {enableLightbox && lightboxOpen && (
+ Loading...}>
+ setLightboxOpen(false)}
+ imageUrl={element.url}
+ imageName={element.name}
+ />
+
+ )}
);
};
diff --git a/frontend/src/components/atoms/elements/LightboxWrapper.tsx b/frontend/src/components/atoms/elements/LightboxWrapper.tsx
new file mode 100644
index 0000000000..3d125b1f74
--- /dev/null
+++ b/frontend/src/components/atoms/elements/LightboxWrapper.tsx
@@ -0,0 +1,52 @@
+import Lightbox from 'yet-another-react-lightbox';
+import Download from 'yet-another-react-lightbox/plugins/download';
+import Zoom from 'yet-another-react-lightbox/plugins/zoom';
+
+import 'yet-another-react-lightbox/styles.css';
+
+interface LightboxWrapperProps {
+ isOpen: boolean;
+ onClose: () => void;
+ imageUrl: string;
+ imageName: string;
+}
+
+const LightboxWrapper = ({
+ isOpen,
+ onClose,
+ imageUrl,
+ imageName
+}: LightboxWrapperProps) => {
+ return (
+ null, buttonNext: () => null }}
+ plugins={[Zoom, Download]}
+ zoom={{
+ maxZoomPixelRatio: 5,
+ zoomInMultiplier: 2
+ }}
+ download={{
+ download: async ({ slide }) => {
+ try {
+ const response = await fetch(slide.src, { mode: 'cors' });
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = imageName || 'image';
+ link.click();
+ window.URL.revokeObjectURL(url);
+ } catch (error) {
+ console.error('Failed to download image:', error);
+ }
+ }
+ }}
+ />
+ );
+};
+
+export default LightboxWrapper;
diff --git a/libs/react-client/src/types/config.ts b/libs/react-client/src/types/config.ts
index 0184e35c4e..0dae4df4ba 100644
--- a/libs/react-client/src/types/config.ts
+++ b/libs/react-client/src/types/config.ts
@@ -49,6 +49,7 @@ export interface IChainlitConfig {
unsafe_allow_html?: boolean;
latex?: boolean;
edit_message?: boolean;
+ image_lightbox?: boolean;
};
debugUrl?: string;
userEnv: string[];
diff --git a/package.json b/package.json
index 4d1d2b640a..6da2b6e657 100644
--- a/package.json
+++ b/package.json
@@ -34,4 +34,4 @@
"micromatch@<4.0.8": ">=4.0.8"
}
}
-}
+}
\ No newline at end of file