@aslaluroba/help-center-react 3.2.16 → 3.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/shared/Button/button.d.ts +1 -1
- package/dist/components/shared/Card/card.d.ts +1 -4
- package/dist/components/ui/agent-response/agent-response.d.ts +2 -1
- package/dist/index.css +1424 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.esm.js +19248 -38614
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +19269 -38635
- package/dist/index.js.map +1 -1
- package/dist/lib/LanguageContext.d.ts +1 -1
- package/dist/lib/custom-hooks/useAblyConnection.d.ts +25 -0
- package/dist/lib/custom-hooks/useActionHandler.d.ts +1 -7
- package/dist/lib/custom-hooks/useChatSession.d.ts +37 -0
- package/dist/lib/custom-hooks/useMessageQueue.d.ts +16 -0
- package/dist/lib/custom-hooks/useReview.d.ts +14 -0
- package/dist/lib/index.d.ts +1 -2
- package/dist/services.d.ts +9 -6
- package/dist/services.esm.js +1 -14195
- package/dist/services.esm.js.map +1 -1
- package/dist/services.js +19 -14191
- package/dist/services.js.map +1 -1
- package/dist/ui/chatbot-popup/chat-window-screen/footer.d.ts +1 -1
- package/dist/ui/chatbot-popup/chat-window-screen/in-chat-review.d.ts +1 -1
- package/dist/ui/chatbot-popup/chat-window-screen/index.d.ts +2 -2
- package/dist/ui/chatbot-popup/options-list-screen/helpscreen-list.d.ts +1 -1
- package/dist/ui/chatbot-popup/options-list-screen/helpscreen-option.d.ts +1 -1
- package/dist/ui/chatbot-popup/options-list-screen/index.d.ts +1 -1
- package/dist/ui/help-center.d.ts +1 -1
- package/dist/ui/help-popup.d.ts +4 -27
- package/dist/ui/review-dialog/index.d.ts +1 -1
- package/package.json +31 -45
- package/postcss.config.js +5 -0
- package/rollup.config.mjs +34 -0
- package/tsconfig.json +5 -6
- package/dist/core/AblyService.d.ts +0 -16
- package/dist/core/ApiService.d.ts +0 -16
- package/dist/core/api.d.ts +0 -10
- package/dist/core/token-service.d.ts +0 -10
- package/dist/i18n.d.ts +0 -3
- package/dist/lib/config.d.ts +0 -18
- package/dist/lib/theme-utils.d.ts +0 -10
- package/dist/lib/types.d.ts +0 -145
- package/dist/lib/utils.d.ts +0 -2
- package/src/assets/animatedLogo.gif +0 -0
- package/src/assets/logo.svg +0 -5
- package/src/assets/seperator.svg +0 -5
- package/src/components/index.ts +0 -1
- package/src/components/shared/Button/button.tsx +0 -38
- package/src/components/shared/Button/index.ts +0 -1
- package/src/components/shared/Card/card.tsx +0 -44
- package/src/components/shared/Card/index.ts +0 -1
- package/src/components/shared/index.ts +0 -2
- package/src/components/ui/agent-response/agent-response.tsx +0 -57
- package/src/components/ui/agent-response/doc.md +0 -88
- package/src/components/ui/image-attachment.tsx +0 -119
- package/src/components/ui/image-preview-dialog.tsx +0 -400
- package/src/components/ui/index.ts +0 -3
- package/src/core/AblyService.ts +0 -243
- package/src/core/ApiService.ts +0 -116
- package/src/core/api.ts +0 -278
- package/src/core/token-service.ts +0 -35
- package/src/globals.css +0 -268
- package/src/i18n.ts +0 -21
- package/src/index.ts +0 -19
- package/src/lib/LanguageContext.tsx +0 -28
- package/src/lib/config.ts +0 -52
- package/src/lib/custom-hooks/useActionHandler.ts +0 -102
- package/src/lib/custom-hooks/useTypewriter.ts +0 -26
- package/src/lib/index.ts +0 -4
- package/src/lib/theme-utils.ts +0 -56
- package/src/lib/types.ts +0 -158
- package/src/lib/utils.ts +0 -6
- package/src/locales/ar.json +0 -45
- package/src/locales/en.json +0 -45
- package/src/services.ts +0 -14
- package/src/types/icons.d.ts +0 -6
- package/src/types/svg.d.ts +0 -5
- package/src/types.d.ts +0 -9
- package/src/ui/chatbot-popup/active-chat-actions.tsx +0 -39
- package/src/ui/chatbot-popup/chat-window-screen/action-button.tsx +0 -37
- package/src/ui/chatbot-popup/chat-window-screen/footer.tsx +0 -313
- package/src/ui/chatbot-popup/chat-window-screen/header.tsx +0 -53
- package/src/ui/chatbot-popup/chat-window-screen/in-chat-review.tsx +0 -116
- package/src/ui/chatbot-popup/chat-window-screen/index.tsx +0 -366
- package/src/ui/chatbot-popup/chat-window-screen/typing-indicator.tsx +0 -31
- package/src/ui/chatbot-popup/error-screen/index.tsx +0 -22
- package/src/ui/chatbot-popup/loading-screen/index.tsx +0 -21
- package/src/ui/chatbot-popup/options-list-screen/company-card.tsx +0 -39
- package/src/ui/chatbot-popup/options-list-screen/header.tsx +0 -23
- package/src/ui/chatbot-popup/options-list-screen/helpscreen-intro.tsx +0 -32
- package/src/ui/chatbot-popup/options-list-screen/helpscreen-list.tsx +0 -57
- package/src/ui/chatbot-popup/options-list-screen/helpscreen-option.tsx +0 -56
- package/src/ui/chatbot-popup/options-list-screen/index.tsx +0 -70
- package/src/ui/confirmation-modal/index.tsx +0 -62
- package/src/ui/floating-message.tsx +0 -28
- package/src/ui/help-button.tsx +0 -24
- package/src/ui/help-center.tsx +0 -448
- package/src/ui/help-popup.tsx +0 -367
- package/src/ui/powered-by.tsx +0 -62
- package/src/ui/review-dialog/index.tsx +0 -149
- package/src/ui/review-dialog/rating.tsx +0 -79
- package/src/useLocalTranslation.ts +0 -15
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './card'
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { useTypewriter } from "@/lib/custom-hooks/useTypewriter";
|
|
2
|
-
import Markdown from "react-markdown";
|
|
3
|
-
import type { Element } from "hast";
|
|
4
|
-
|
|
5
|
-
interface AgentResponseProps {
|
|
6
|
-
messageContent: string;
|
|
7
|
-
senderType: number;
|
|
8
|
-
messageId: number;
|
|
9
|
-
onType?: () => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const seenMessagesRef = new Set<number>();
|
|
13
|
-
|
|
14
|
-
const AgentResponse = ({
|
|
15
|
-
senderType,
|
|
16
|
-
messageContent,
|
|
17
|
-
messageId,
|
|
18
|
-
onType,
|
|
19
|
-
}: AgentResponseProps) => {
|
|
20
|
-
// Ensure messageContent is always a string to prevent errors
|
|
21
|
-
const safeMessageContent = messageContent ?? "";
|
|
22
|
-
const shouldAnimate =
|
|
23
|
-
(senderType === 2 || senderType === 3) && !seenMessagesRef.has(messageId);
|
|
24
|
-
const animatedText = useTypewriter(safeMessageContent, 20, onType);
|
|
25
|
-
const finalMessage = shouldAnimate ? animatedText : safeMessageContent;
|
|
26
|
-
|
|
27
|
-
// Mark message as "seen" after full animation
|
|
28
|
-
if (shouldAnimate && finalMessage === safeMessageContent) {
|
|
29
|
-
seenMessagesRef.add(messageId);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<div
|
|
34
|
-
dir="auto"
|
|
35
|
-
className={`babylai:rounded-2xl babylai:p-4 ${
|
|
36
|
-
senderType === 1
|
|
37
|
-
? "babylai:bg-primary-500 babylai:text-black-white-50 babylai:max-w-[220px]"
|
|
38
|
-
: "babylai:bg-card"
|
|
39
|
-
}`}
|
|
40
|
-
>
|
|
41
|
-
<Markdown
|
|
42
|
-
components={{
|
|
43
|
-
p: ({ node, ...props }: { node?: Element; [key: string]: any }) => (
|
|
44
|
-
<p
|
|
45
|
-
className="babylai:m-0 babylai:leading-snug babylai:text-sm babylai:font-sans babylai:wrap-break-word"
|
|
46
|
-
{...props}
|
|
47
|
-
/>
|
|
48
|
-
),
|
|
49
|
-
}}
|
|
50
|
-
>
|
|
51
|
-
{finalMessage}
|
|
52
|
-
</Markdown>
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export default AgentResponse;
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# AgentResponse Component
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
The `AgentResponse` component renders a chat message in a conversational interface, supporting user and agent messages with optional typewriter animation for agent responses. It uses `react-markdown` to render the message content, allowing Markdown formatting, and applies conditional styling based on the sender type.
|
|
6
|
-
|
|
7
|
-
## Props
|
|
8
|
-
|
|
9
|
-
| Prop Name | Type | Required | Description |
|
|
10
|
-
|------------------|--------|----------|-----------------------------------------------------------------------------|
|
|
11
|
-
| `messageContent` | string | Yes | The content of the message to display, supports Markdown formatting. |
|
|
12
|
-
| `senderType` | number | Yes | The type of sender: `1` for user, `2` or `3` for agents (triggers animation). |
|
|
13
|
-
| `messageId` | number | Yes | A unique identifier for the message, used to track seen messages. |
|
|
14
|
-
| `onType` | () => void | No | A Callback function, used to trigger scroll on typing. |
|
|
15
|
-
|
|
16
|
-
## Features
|
|
17
|
-
|
|
18
|
-
- **Conditional Styling**:
|
|
19
|
-
- User messages (`senderType === 1`): Rendered with a primary background (`babylai-bg-primary-500`), white text (`!babylai-text-black-white-50`), and aligned to the right (`babylai-self-end`).
|
|
20
|
-
- Agent messages (`senderType === 2` or `3`): Rendered with a neutral background (`babylai-bg-black-white-50`).
|
|
21
|
-
- **Typewriter Animation**:
|
|
22
|
-
- Applied to agent messages (`senderType === 2` or `3`) on first render (when `messageId` is not in `seenMessagesRef`).
|
|
23
|
-
- Uses the `useTypewriter` hook with a 20ms delay per character.
|
|
24
|
-
- Animation is skipped for subsequent renders of the same `messageId`.
|
|
25
|
-
- **Markdown Support**:
|
|
26
|
-
- Renders `messageContent` using `react-markdown`.
|
|
27
|
-
- Customizes `<p>` tags with classes for consistent styling (`babylai-m-0`, `babylai-leading-6`, `babylai-text-sm`, `babylai-text-right`, `babylai-font-sans`).
|
|
28
|
-
- **Seen Message Tracking**:
|
|
29
|
-
- Maintains a global `seenMessagesRef` Set to track which `messageId`s have been fully animated.
|
|
30
|
-
- Adds `messageId` to `seenMessagesRef` when animation completes (`animatedText === messageContent`).
|
|
31
|
-
|
|
32
|
-
## Usage
|
|
33
|
-
|
|
34
|
-
### Example
|
|
35
|
-
|
|
36
|
-
```tsx
|
|
37
|
-
import { AgentResponse } from './agent-response';
|
|
38
|
-
|
|
39
|
-
function Chat() {
|
|
40
|
-
return (
|
|
41
|
-
<div>
|
|
42
|
-
{/* User message */}
|
|
43
|
-
<AgentResponse
|
|
44
|
-
senderType={1}
|
|
45
|
-
messageContent="Hello, how can I help?"
|
|
46
|
-
messageId={1}
|
|
47
|
-
/>
|
|
48
|
-
{/* Agent message with animation */}
|
|
49
|
-
<AgentResponse
|
|
50
|
-
senderType={2}
|
|
51
|
-
messageContent="Hi! I'm here to assist."
|
|
52
|
-
messageId={2}
|
|
53
|
-
/>
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Styling
|
|
60
|
-
|
|
61
|
-
The component uses the following Tailwind/custom classes:
|
|
62
|
-
- Container: `babylai-max-w-[80%] babylai-rounded-2xl babylai-p-4 babylai-text-right`
|
|
63
|
-
- User-specific: `babylai-bg-primary-500 !babylai-text-black-white-50 babylai-self-end`
|
|
64
|
-
- Agent-specific: `babylai-bg-black-white-50`
|
|
65
|
-
- Paragraph: `babylai-m-0 babylai-leading-6 babylai-text-sm babylai-text-right babylai-font-sans`
|
|
66
|
-
|
|
67
|
-
Ensure these classes are defined in your CSS (e.g., Tailwind configuration).
|
|
68
|
-
|
|
69
|
-
## Notes
|
|
70
|
-
|
|
71
|
-
- **Animation**: The typewriter effect is only applied to agent messages (`senderType` 2 or 3) on their first render. Once the animation completes, the `messageId` is marked as seen, and subsequent renders use the full message.
|
|
72
|
-
- **Markdown**: Supports basic Markdown (e.g., bold, italic). Complex Markdown features depend on `react-markdown` capabilities.
|
|
73
|
-
- **Performance**: The `seenMessagesRef` is a global `Set`, so ensure `messageId`s are unique to avoid conflicts across instances.
|
|
74
|
-
|
|
75
|
-
## Dependencies
|
|
76
|
-
|
|
77
|
-
- `react-markdown`: For rendering Markdown content.
|
|
78
|
-
- `@/lib/custom-hooks/useTypewriter`: Custom hook for typewriter animation.
|
|
79
|
-
|
|
80
|
-
## Testing
|
|
81
|
-
|
|
82
|
-
The component is tested with Jest and React Testing Library, covering:
|
|
83
|
-
- Rendering and styling for user (`senderType=1`) and agent (`senderType=2/3`) messages.
|
|
84
|
-
- Animation behavior for unseen and seen messages.
|
|
85
|
-
- `seenMessagesRef` updates when animation completes.
|
|
86
|
-
- Markdown rendering with custom paragraph styling.
|
|
87
|
-
|
|
88
|
-
See `agent-response.test.tsx` for the full test suite.
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import { presignDownload } from '@/core/api';
|
|
3
|
-
import { cn } from '@/lib/utils';
|
|
4
|
-
import { useLocalTranslation } from '../../useLocalTranslation';
|
|
5
|
-
import { ImagePreviewDialog } from './image-preview-dialog';
|
|
6
|
-
|
|
7
|
-
interface ImageAttachmentProps {
|
|
8
|
-
fileId?: string; // File ID (for user-sent messages, requires presignDownload)
|
|
9
|
-
imageUrl?: string; // Direct URL (for received messages from Ably)
|
|
10
|
-
className?: string;
|
|
11
|
-
enablePreview?: boolean;
|
|
12
|
-
onClick?: () => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const ImageAttachment: React.FC<ImageAttachmentProps> = ({
|
|
16
|
-
fileId,
|
|
17
|
-
imageUrl: propImageUrl,
|
|
18
|
-
className,
|
|
19
|
-
enablePreview = true,
|
|
20
|
-
onClick,
|
|
21
|
-
}) => {
|
|
22
|
-
const { i18n } = useLocalTranslation();
|
|
23
|
-
const [imageUrl, setImageUrl] = useState<string | null>(propImageUrl || null);
|
|
24
|
-
const [loading, setLoading] = useState(!propImageUrl && !!fileId);
|
|
25
|
-
const [error, setError] = useState(false);
|
|
26
|
-
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
// If we have a direct URL, use it immediately
|
|
30
|
-
if (propImageUrl) {
|
|
31
|
-
setImageUrl(propImageUrl);
|
|
32
|
-
setLoading(false);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// If we only have a fileId, fetch the URL using presignDownload
|
|
37
|
-
if (fileId) {
|
|
38
|
-
const fetchImageUrl = async () => {
|
|
39
|
-
try {
|
|
40
|
-
setLoading(true);
|
|
41
|
-
setError(false);
|
|
42
|
-
const response = await presignDownload(fileId, i18n.language as 'ar' | 'en');
|
|
43
|
-
setImageUrl(response.downloadUrl);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
setError(true);
|
|
46
|
-
} finally {
|
|
47
|
-
setLoading(false);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
fetchImageUrl();
|
|
52
|
-
}
|
|
53
|
-
}, [fileId, propImageUrl, i18n.language as 'ar' | 'en']);
|
|
54
|
-
|
|
55
|
-
const handleImageClick = () => {
|
|
56
|
-
if (onClick) {
|
|
57
|
-
onClick();
|
|
58
|
-
} else if (enablePreview && imageUrl) {
|
|
59
|
-
setIsPreviewOpen(true);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
if (loading) {
|
|
64
|
-
return (
|
|
65
|
-
<div
|
|
66
|
-
className={cn(
|
|
67
|
-
'babylai:flex babylai:items-center babylai:justify-center babylai:bg-black-white-100 babylai:rounded-lg babylai:w-16 babylai:h-16',
|
|
68
|
-
className
|
|
69
|
-
)}
|
|
70
|
-
>
|
|
71
|
-
<div className='babylai:animate-pulse babylai:text-xs babylai:text-black-white-500'>...</div>
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (error || !imageUrl) {
|
|
77
|
-
return (
|
|
78
|
-
<div
|
|
79
|
-
className={cn(
|
|
80
|
-
'babylai:flex babylai:items-center babylai:justify-center babylai:bg-black-white-100 babylai:rounded-lg babylai:w-16 babylai:h-16 babylai:border babylai:border-black-white-200',
|
|
81
|
-
className
|
|
82
|
-
)}
|
|
83
|
-
>
|
|
84
|
-
<div className='babylai:text-xs babylai:text-black-white-500'>!</div>
|
|
85
|
-
</div>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<>
|
|
91
|
-
<img
|
|
92
|
-
src={imageUrl}
|
|
93
|
-
alt='Attachment'
|
|
94
|
-
className={cn(
|
|
95
|
-
'babylai:w-16 babylai:h-16 babylai:object-cover babylai:rounded-lg babylai:border babylai:border-black-white-200 babylai:max-w-[50px]',
|
|
96
|
-
(enablePreview || onClick) && 'babylai:cursor-pointer babylai:hover:opacity-80 babylai:transition-opacity',
|
|
97
|
-
className
|
|
98
|
-
)}
|
|
99
|
-
onError={() => setError(true)}
|
|
100
|
-
onClick={handleImageClick}
|
|
101
|
-
role={enablePreview || onClick ? 'button' : undefined}
|
|
102
|
-
aria-label={enablePreview || onClick ? 'Click to preview image' : undefined}
|
|
103
|
-
/>
|
|
104
|
-
{enablePreview && !onClick && (
|
|
105
|
-
<ImagePreviewDialog
|
|
106
|
-
imageUrls={[imageUrl]}
|
|
107
|
-
initialIndex={0}
|
|
108
|
-
isOpen={isPreviewOpen}
|
|
109
|
-
onClose={() => setIsPreviewOpen(false)}
|
|
110
|
-
alt='Attachment preview'
|
|
111
|
-
/>
|
|
112
|
-
)}
|
|
113
|
-
</>
|
|
114
|
-
);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
ImageAttachment.displayName = 'ImageAttachment';
|
|
118
|
-
|
|
119
|
-
export default ImageAttachment;
|
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useCallback, useRef, useState } from 'react';
|
|
2
|
-
import { cn } from '@/lib/utils';
|
|
3
|
-
import { Button } from '@/components';
|
|
4
|
-
import MaterialSymbolsCloseSmallOutlineRounded from '~icons/material-symbols/close-small-outline-rounded'
|
|
5
|
-
import SolarArrowRightBoldDuotone from '~icons/solar/arrow-right-bold-duotone';
|
|
6
|
-
import { useLocalTranslation } from '../../useLocalTranslation';
|
|
7
|
-
|
|
8
|
-
interface ImagePreviewDialogProps {
|
|
9
|
-
imageUrls: string[];
|
|
10
|
-
initialIndex: number;
|
|
11
|
-
isOpen: boolean;
|
|
12
|
-
onClose: () => void;
|
|
13
|
-
alt?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const ImagePreviewDialog: React.FC<ImagePreviewDialogProps> = ({
|
|
17
|
-
imageUrls,
|
|
18
|
-
initialIndex,
|
|
19
|
-
isOpen,
|
|
20
|
-
onClose,
|
|
21
|
-
alt = 'Preview',
|
|
22
|
-
}) => {
|
|
23
|
-
const { dir } = useLocalTranslation();
|
|
24
|
-
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
25
|
-
const [zoomLevel, setZoomLevel] = useState(1);
|
|
26
|
-
const [imagePosition, setImagePosition] = useState({ x: 0, y: 0 });
|
|
27
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
28
|
-
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
|
29
|
-
const imageRef = useRef<HTMLImageElement>(null);
|
|
30
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
31
|
-
|
|
32
|
-
const currentImageUrl = imageUrls[currentIndex];
|
|
33
|
-
const hasMultipleImages = imageUrls.length > 1;
|
|
34
|
-
|
|
35
|
-
// Update current index when initialIndex changes
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
if (isOpen) {
|
|
38
|
-
setCurrentIndex(initialIndex);
|
|
39
|
-
setZoomLevel(1);
|
|
40
|
-
setImagePosition({ x: 0, y: 0 });
|
|
41
|
-
}
|
|
42
|
-
}, [initialIndex, isOpen]);
|
|
43
|
-
|
|
44
|
-
const handlePrevious = useCallback(() => {
|
|
45
|
-
if (currentIndex > 0) {
|
|
46
|
-
setCurrentIndex(currentIndex - 1);
|
|
47
|
-
setZoomLevel(1);
|
|
48
|
-
setImagePosition({ x: 0, y: 0 });
|
|
49
|
-
}
|
|
50
|
-
}, [currentIndex]);
|
|
51
|
-
|
|
52
|
-
const handleNext = useCallback(() => {
|
|
53
|
-
if (currentIndex < imageUrls.length - 1) {
|
|
54
|
-
setCurrentIndex(currentIndex + 1);
|
|
55
|
-
setZoomLevel(1);
|
|
56
|
-
setImagePosition({ x: 0, y: 0 });
|
|
57
|
-
}
|
|
58
|
-
}, [currentIndex, imageUrls.length]);
|
|
59
|
-
|
|
60
|
-
const handleZoomIn = useCallback(() => {
|
|
61
|
-
setZoomLevel((prev) => Math.min(prev + 0.25, 3));
|
|
62
|
-
}, []);
|
|
63
|
-
|
|
64
|
-
const handleZoomOut = useCallback(() => {
|
|
65
|
-
setZoomLevel((prev) => Math.max(prev - 0.25, 0.5));
|
|
66
|
-
}, []);
|
|
67
|
-
|
|
68
|
-
const handleResetZoom = useCallback(() => {
|
|
69
|
-
setZoomLevel(1);
|
|
70
|
-
setImagePosition({ x: 0, y: 0 });
|
|
71
|
-
}, []);
|
|
72
|
-
|
|
73
|
-
const handleDownload = useCallback(async () => {
|
|
74
|
-
if (!currentImageUrl) return;
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
// Fetch the image as a blob
|
|
78
|
-
const response = await fetch(currentImageUrl);
|
|
79
|
-
const blob = await response.blob();
|
|
80
|
-
|
|
81
|
-
// Create a temporary URL for the blob
|
|
82
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
83
|
-
|
|
84
|
-
// Extract filename from URL or use a default
|
|
85
|
-
const urlParts = currentImageUrl.split('/');
|
|
86
|
-
const filename = urlParts[urlParts.length - 1].split('?')[0] || 'image.png';
|
|
87
|
-
|
|
88
|
-
// Create a temporary anchor element and trigger download
|
|
89
|
-
const link = document.createElement('a');
|
|
90
|
-
link.href = blobUrl;
|
|
91
|
-
link.download = filename;
|
|
92
|
-
document.body.appendChild(link);
|
|
93
|
-
link.click();
|
|
94
|
-
|
|
95
|
-
// Cleanup
|
|
96
|
-
document.body.removeChild(link);
|
|
97
|
-
URL.revokeObjectURL(blobUrl);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
console.error('Failed to download image:', error);
|
|
100
|
-
// Fallback: open in new tab if download fails
|
|
101
|
-
window.open(currentImageUrl, '_blank');
|
|
102
|
-
}
|
|
103
|
-
}, [currentImageUrl]);
|
|
104
|
-
|
|
105
|
-
const handleClose = useCallback(() => {
|
|
106
|
-
setZoomLevel(1);
|
|
107
|
-
setImagePosition({ x: 0, y: 0 });
|
|
108
|
-
onClose();
|
|
109
|
-
}, [onClose]);
|
|
110
|
-
|
|
111
|
-
// Keyboard navigation
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
if (!isOpen) return;
|
|
114
|
-
|
|
115
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
116
|
-
if (e.key === 'ArrowLeft') {
|
|
117
|
-
e.preventDefault();
|
|
118
|
-
if (dir === 'rtl') {
|
|
119
|
-
handleNext();
|
|
120
|
-
} else {
|
|
121
|
-
handlePrevious();
|
|
122
|
-
}
|
|
123
|
-
} else if (e.key === 'ArrowRight') {
|
|
124
|
-
e.preventDefault();
|
|
125
|
-
if (dir === 'rtl') {
|
|
126
|
-
handlePrevious();
|
|
127
|
-
} else {
|
|
128
|
-
handleNext();
|
|
129
|
-
}
|
|
130
|
-
} else if (e.key === 'Escape') {
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
handleClose();
|
|
133
|
-
} else if (e.key === '+' || e.key === '=') {
|
|
134
|
-
e.preventDefault();
|
|
135
|
-
handleZoomIn();
|
|
136
|
-
} else if (e.key === '-') {
|
|
137
|
-
e.preventDefault();
|
|
138
|
-
handleZoomOut();
|
|
139
|
-
} else if (e.key === '0') {
|
|
140
|
-
e.preventDefault();
|
|
141
|
-
handleResetZoom();
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
146
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
147
|
-
}, [isOpen, currentIndex, imageUrls.length, dir, handlePrevious, handleNext, handleZoomIn, handleZoomOut, handleResetZoom, handleClose]);
|
|
148
|
-
|
|
149
|
-
// Mouse wheel zoom
|
|
150
|
-
const handleWheel = useCallback(
|
|
151
|
-
(e: React.WheelEvent) => {
|
|
152
|
-
if (e.ctrlKey || e.metaKey) {
|
|
153
|
-
e.preventDefault();
|
|
154
|
-
if (e.deltaY < 0) {
|
|
155
|
-
handleZoomIn();
|
|
156
|
-
} else {
|
|
157
|
-
handleZoomOut();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
[handleZoomIn, handleZoomOut]
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
// Drag to pan when zoomed
|
|
165
|
-
const handleMouseDown = useCallback(
|
|
166
|
-
(e: React.MouseEvent) => {
|
|
167
|
-
if (zoomLevel > 1) {
|
|
168
|
-
setIsDragging(true);
|
|
169
|
-
setDragStart({ x: e.clientX - imagePosition.x, y: e.clientY - imagePosition.y });
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
[zoomLevel, imagePosition]
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
const handleMouseMove = useCallback(
|
|
176
|
-
(e: React.MouseEvent) => {
|
|
177
|
-
if (isDragging && zoomLevel > 1) {
|
|
178
|
-
setImagePosition({
|
|
179
|
-
x: e.clientX - dragStart.x,
|
|
180
|
-
y: e.clientY - dragStart.y,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
[isDragging, zoomLevel, dragStart]
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const handleMouseUp = useCallback(() => {
|
|
188
|
-
setIsDragging(false);
|
|
189
|
-
}, []);
|
|
190
|
-
|
|
191
|
-
// Constrain image position when zoomed
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
if (zoomLevel <= 1) {
|
|
194
|
-
setImagePosition({ x: 0, y: 0 });
|
|
195
|
-
}
|
|
196
|
-
}, [zoomLevel]);
|
|
197
|
-
|
|
198
|
-
// Prevent body scroll when dialog is open
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
if (isOpen) {
|
|
201
|
-
document.body.style.overflow = 'hidden';
|
|
202
|
-
} else {
|
|
203
|
-
document.body.style.overflow = 'unset';
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return () => {
|
|
207
|
-
document.body.style.overflow = 'unset';
|
|
208
|
-
};
|
|
209
|
-
}, [isOpen]);
|
|
210
|
-
|
|
211
|
-
if (!isOpen || !currentImageUrl) return null;
|
|
212
|
-
|
|
213
|
-
return (
|
|
214
|
-
<div
|
|
215
|
-
className='babylai:fixed babylai:inset-0 babylai:z-9999 babylai:flex babylai:items-center babylai:justify-center'
|
|
216
|
-
onClick={handleClose}
|
|
217
|
-
role='dialog'
|
|
218
|
-
aria-modal='true'
|
|
219
|
-
aria-label='Image preview dialog'
|
|
220
|
-
>
|
|
221
|
-
{/* Backdrop */}
|
|
222
|
-
<div className='babylai:absolute babylai:inset-0 babylai:bg-black babylai:bg-opacity-95' />
|
|
223
|
-
|
|
224
|
-
{/* Dialog content */}
|
|
225
|
-
<div
|
|
226
|
-
ref={containerRef}
|
|
227
|
-
className='babylai:relative babylai:w-full babylai:h-full babylai:flex babylai:items-center babylai:justify-center babylai:overflow-hidden'
|
|
228
|
-
onClick={(e) => e.stopPropagation()}
|
|
229
|
-
onWheel={handleWheel}
|
|
230
|
-
onMouseDown={handleMouseDown}
|
|
231
|
-
onMouseMove={handleMouseMove}
|
|
232
|
-
onMouseUp={handleMouseUp}
|
|
233
|
-
onMouseLeave={handleMouseUp}
|
|
234
|
-
>
|
|
235
|
-
{/* Close Button */}
|
|
236
|
-
<Button
|
|
237
|
-
variant='ghost'
|
|
238
|
-
size='icon'
|
|
239
|
-
onClick={handleClose}
|
|
240
|
-
className={cn(
|
|
241
|
-
'babylai:absolute babylai:top-4 babylai:z-60',
|
|
242
|
-
dir === 'rtl' ? 'babylai:left-4' : 'babylai:right-4',
|
|
243
|
-
'babylai:text-white babylai:hover:text-white/80 babylai:hover:bg-white/10',
|
|
244
|
-
'babylai:h-10 babylai:w-10 babylai:rounded-full'
|
|
245
|
-
)}
|
|
246
|
-
aria-label='Close preview'
|
|
247
|
-
type='button'
|
|
248
|
-
>
|
|
249
|
-
<MaterialSymbolsCloseSmallOutlineRounded className='babylai:w-6 babylai:h-6' />
|
|
250
|
-
</Button>
|
|
251
|
-
|
|
252
|
-
{/* Navigation Buttons */}
|
|
253
|
-
{hasMultipleImages && (
|
|
254
|
-
<>
|
|
255
|
-
<Button
|
|
256
|
-
variant='ghost'
|
|
257
|
-
size='icon'
|
|
258
|
-
onClick={dir === 'rtl' ? handleNext : handlePrevious}
|
|
259
|
-
disabled={dir === 'rtl' ? currentIndex === imageUrls.length - 1 : currentIndex === 0}
|
|
260
|
-
className={cn(
|
|
261
|
-
'babylai:absolute babylai:top-1/2 babylai:-translate-y-1/2 babylai:z-60',
|
|
262
|
-
dir === 'rtl' ? 'babylai:right-4' : 'babylai:left-4',
|
|
263
|
-
'babylai:text-white babylai:hover:text-white/80 babylai:hover:bg-white/10',
|
|
264
|
-
'babylai:h-12 babylai:w-12 babylai:rounded-full',
|
|
265
|
-
'babylai:disabled:opacity-30'
|
|
266
|
-
)}
|
|
267
|
-
aria-label='Previous image'
|
|
268
|
-
type='button'
|
|
269
|
-
>
|
|
270
|
-
<SolarArrowRightBoldDuotone className={cn('babylai:w-8 babylai:h-8', dir === 'rtl' ? '' : 'babylai:rotate-180')} />
|
|
271
|
-
</Button>
|
|
272
|
-
<Button
|
|
273
|
-
variant='ghost'
|
|
274
|
-
size='icon'
|
|
275
|
-
onClick={dir === 'rtl' ? handlePrevious : handleNext}
|
|
276
|
-
disabled={dir === 'rtl' ? currentIndex === 0 : currentIndex === imageUrls.length - 1}
|
|
277
|
-
className={cn(
|
|
278
|
-
'babylai:absolute babylai:top-1/2 babylai:-translate-y-1/2 babylai:z-60',
|
|
279
|
-
dir === 'rtl' ? 'babylai:left-4' : 'babylai:right-4',
|
|
280
|
-
'babylai:text-white babylai:hover:text-white/80 babylai:hover:bg-white/10',
|
|
281
|
-
'babylai:h-12 babylai:w-12 babylai:rounded-full',
|
|
282
|
-
'babylai:disabled:opacity-30'
|
|
283
|
-
)}
|
|
284
|
-
aria-label='Next image'
|
|
285
|
-
type='button'
|
|
286
|
-
>
|
|
287
|
-
<SolarArrowRightBoldDuotone className={cn('babylai:w-8 babylai:h-8', dir === 'rtl' ? 'babylai:rotate-180' : '')} />
|
|
288
|
-
</Button>
|
|
289
|
-
</>
|
|
290
|
-
)}
|
|
291
|
-
|
|
292
|
-
{/* Zoom Controls */}
|
|
293
|
-
<div
|
|
294
|
-
className={cn(
|
|
295
|
-
'babylai:absolute babylai:bottom-4 babylai:z-60',
|
|
296
|
-
'babylai:flex babylai:items-center babylai:gap-2',
|
|
297
|
-
'babylai:bg-black/50 babylai:backdrop-blur-sm babylai:rounded-lg babylai:p-2',
|
|
298
|
-
dir === 'rtl' ? 'babylai:right-1/2 babylai:translate-x-1/2' : 'babylai:left-1/2 babylai:-translate-x-1/2'
|
|
299
|
-
)}
|
|
300
|
-
>
|
|
301
|
-
<Button
|
|
302
|
-
variant='ghost'
|
|
303
|
-
size='icon'
|
|
304
|
-
onClick={handleZoomOut}
|
|
305
|
-
disabled={zoomLevel <= 0.5}
|
|
306
|
-
className='babylai:text-white babylai:hover:text-white/80 babylai:hover:bg-white/10 babylai:h-9 babylai:w-9 babylai:disabled:opacity-30'
|
|
307
|
-
aria-label='Zoom out'
|
|
308
|
-
type='button'
|
|
309
|
-
>
|
|
310
|
-
<svg className='babylai:w-5 babylai:h-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
|
311
|
-
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7' />
|
|
312
|
-
</svg>
|
|
313
|
-
</Button>
|
|
314
|
-
<span className='babylai:text-white babylai:text-sm babylai:font-medium babylai:min-w-12 babylai:text-center'>
|
|
315
|
-
{Math.round(zoomLevel * 100)}%
|
|
316
|
-
</span>
|
|
317
|
-
<Button
|
|
318
|
-
variant='ghost'
|
|
319
|
-
size='icon'
|
|
320
|
-
onClick={handleZoomIn}
|
|
321
|
-
disabled={zoomLevel >= 3}
|
|
322
|
-
className='babylai:text-white babylai:hover:text-white/80 babylai:hover:bg-white/10 babylai:h-9 babylai:w-9 babylai:disabled:opacity-30'
|
|
323
|
-
aria-label='Zoom in'
|
|
324
|
-
type='button'
|
|
325
|
-
>
|
|
326
|
-
<svg className='babylai:w-5 babylai:h-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
|
327
|
-
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7' />
|
|
328
|
-
</svg>
|
|
329
|
-
</Button>
|
|
330
|
-
{zoomLevel !== 1 && (
|
|
331
|
-
<Button
|
|
332
|
-
variant='ghost'
|
|
333
|
-
size='sm'
|
|
334
|
-
onClick={handleResetZoom}
|
|
335
|
-
className='babylai:text-white babylai:hover:text-white/80 babylai:hover:bg-white/10 babylai:h-9 babylai:px-3 babylai:ml-2'
|
|
336
|
-
aria-label='Reset zoom'
|
|
337
|
-
type='button'
|
|
338
|
-
>
|
|
339
|
-
Reset
|
|
340
|
-
</Button>
|
|
341
|
-
)}
|
|
342
|
-
{/* Download Button */}
|
|
343
|
-
<div className='babylai:h-9 babylai:w-px babylai:bg-white/20 babylai:mx-1' />
|
|
344
|
-
<Button
|
|
345
|
-
variant='ghost'
|
|
346
|
-
size='icon'
|
|
347
|
-
onClick={handleDownload}
|
|
348
|
-
className='babylai:text-white babylai:hover:text-white/80 babylai:hover:bg-white/10 babylai:h-9 babylai:w-9'
|
|
349
|
-
aria-label='Download image'
|
|
350
|
-
type='button'
|
|
351
|
-
>
|
|
352
|
-
<svg className='babylai:w-5 babylai:h-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
|
353
|
-
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4' />
|
|
354
|
-
</svg>
|
|
355
|
-
</Button>
|
|
356
|
-
</div>
|
|
357
|
-
|
|
358
|
-
{/* Image Counter */}
|
|
359
|
-
{hasMultipleImages && (
|
|
360
|
-
<div
|
|
361
|
-
className={cn(
|
|
362
|
-
'babylai:absolute babylai:top-4 babylai:z-60',
|
|
363
|
-
'babylai:bg-black/50 babylai:backdrop-blur-sm babylai:rounded-lg babylai:px-4 babylai:py-2',
|
|
364
|
-
dir === 'rtl' ? 'babylai:right-1/2 babylai:translate-x-1/2' : 'babylai:left-1/2 babylai:-translate-x-1/2'
|
|
365
|
-
)}
|
|
366
|
-
>
|
|
367
|
-
<span className='babylai:text-white babylai:text-sm babylai:font-medium'>
|
|
368
|
-
{currentIndex + 1} / {imageUrls.length}
|
|
369
|
-
</span>
|
|
370
|
-
</div>
|
|
371
|
-
)}
|
|
372
|
-
|
|
373
|
-
{/* Image Container */}
|
|
374
|
-
<div
|
|
375
|
-
className='babylai:flex babylai:items-center babylai:justify-center'
|
|
376
|
-
style={{
|
|
377
|
-
transform: `translate(${imagePosition.x}px, ${imagePosition.y}px)`,
|
|
378
|
-
cursor: zoomLevel > 1 ? (isDragging ? 'grabbing' : 'grab') : 'default',
|
|
379
|
-
}}
|
|
380
|
-
>
|
|
381
|
-
<img
|
|
382
|
-
ref={imageRef}
|
|
383
|
-
src={currentImageUrl}
|
|
384
|
-
alt={alt}
|
|
385
|
-
className='babylai:max-w-[90vw] babylai:max-h-[85vh] babylai:object-contain babylai:select-none'
|
|
386
|
-
style={{
|
|
387
|
-
transform: `scale(${zoomLevel})`,
|
|
388
|
-
transformOrigin: 'center center',
|
|
389
|
-
}}
|
|
390
|
-
draggable={false}
|
|
391
|
-
/>
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
);
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
ImagePreviewDialog.displayName = 'ImagePreviewDialog';
|
|
399
|
-
|
|
400
|
-
export default ImagePreviewDialog;
|