quickblox-react-ui-kit 0.1.8 → 0.2.0
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/Presentation/Views/Base/BaseViewModel.d.ts +2 -1
- package/dist/Presentation/components/UI/Dialogs/MessagesView/AIWidgets/AIWidget.d.ts +8 -0
- package/dist/Presentation/components/UI/Dialogs/MessagesView/AIWidgets/ErrorMessageIcon.d.ts +11 -0
- package/dist/Presentation/components/UI/Dialogs/MessagesView/AIWidgets/UseDefaultAIAssistAnswerWidgetWithProxy.d.ts +9 -0
- package/dist/Presentation/components/UI/Dialogs/MessagesView/AIWidgets/UseDefaultTextInputWidget.d.ts +2 -0
- package/dist/Presentation/components/UI/Dialogs/MessagesView/AIWidgets/useDefaultVoiceInputWidget.d.ts +2 -0
- package/dist/Presentation/components/UI/Dialogs/MessagesView/ContextMenu.d.ts +10 -0
- package/dist/Presentation/components/UI/Dialogs/MessagesView/MessagesView.d.ts +4 -0
- package/dist/Presentation/components/layouts/Desktop/QuickBloxUIKitDesktopLayout.d.ts +9 -0
- package/dist/index-ui.js +47 -3
- package/dist/utils/utils.d.ts +3 -0
- package/package.json +1 -1
- package/src/App.tsx +0 -19
- package/src/Presentation/Views/Base/BaseViewModel.ts +11 -3
- package/src/Presentation/Views/Dialogs/Dialogs.tsx +0 -1
- package/src/Presentation/components/UI/Dialogs/MessagesView/AIWidgets/AIWidget.ts +13 -0
- package/src/Presentation/components/UI/Dialogs/MessagesView/AIWidgets/ErrorMessageIcon.tsx +98 -0
- package/src/Presentation/components/UI/Dialogs/MessagesView/AIWidgets/UseDefaultAIAssistAnswerWidgetWithProxy.tsx +136 -0
- package/src/Presentation/components/UI/Dialogs/MessagesView/{InputWidget → AIWidgets}/UseDefaultTextInputWidget.tsx +4 -4
- package/src/Presentation/components/UI/Dialogs/MessagesView/{InputWidget → AIWidgets}/useDefaultVoiceInputWidget.tsx +4 -4
- package/src/Presentation/components/UI/Dialogs/MessagesView/ContextMenu.tsx +96 -0
- package/src/Presentation/components/UI/Dialogs/MessagesView/MessagesView.tsx +117 -62
- package/src/Presentation/components/layouts/Desktop/QuickBloxUIKitDesktopLayout.tsx +68 -36
- package/src/QBconfig.ts +3 -4
- package/src/utils/utils.ts +39 -0
- package/dist/Presentation/components/UI/Dialogs/MessagesView/InputWidget/InputWidget.d.ts +0 -8
- package/dist/Presentation/components/UI/Dialogs/MessagesView/InputWidget/UseDefaultTextInputWidget.d.ts +0 -2
- package/dist/Presentation/components/UI/Dialogs/MessagesView/InputWidget/useDefaultVoiceInputWidget.d.ts +0 -2
- package/src/Presentation/components/UI/Dialogs/MessagesView/InputWidget/InputWidget.ts +0 -15
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -194,18 +194,6 @@ function App() {
|
|
|
194
194
|
prepareSDK(currentUser).catch();
|
|
195
195
|
}, []);
|
|
196
196
|
|
|
197
|
-
// const { apiKey } = QBConfig.configAIApi.AIAnswerAssistWidgetConfig;
|
|
198
|
-
// // eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
199
|
-
// const openAIConfiguration: Configuration = new Configuration({
|
|
200
|
-
// apiKey,
|
|
201
|
-
// });
|
|
202
|
-
//
|
|
203
|
-
// // eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
204
|
-
// const openAIApi: OpenAIApi = new OpenAIApi(openAIConfiguration);
|
|
205
|
-
// const defaultIncomingMessageWidget = UseDefaultIncomingMessageWidget({
|
|
206
|
-
// openAIApi,
|
|
207
|
-
// });
|
|
208
|
-
|
|
209
197
|
// todo: uncomment authSecret
|
|
210
198
|
return (
|
|
211
199
|
<QuickBloxUIKitProvider
|
|
@@ -233,13 +221,6 @@ function App() {
|
|
|
233
221
|
<Route
|
|
234
222
|
path="/desktop-test-mock"
|
|
235
223
|
element={
|
|
236
|
-
// <QuickBloxUIKitDesktopLayout
|
|
237
|
-
// theme={new DefaultTheme()}
|
|
238
|
-
// IncomingMessageWidgetToRightPlaceHolder={
|
|
239
|
-
// defaultIncomingMessageWidget
|
|
240
|
-
// }
|
|
241
|
-
// />
|
|
242
|
-
|
|
243
224
|
<QuickBloxUIKitDesktopLayout theme={new DefaultTheme()} />
|
|
244
225
|
}
|
|
245
226
|
/>
|
|
@@ -69,7 +69,15 @@ export type FunctionTypeDialogEntityToBoolean = (
|
|
|
69
69
|
) => Promise<boolean>;
|
|
70
70
|
export type FunctionTypeFileToFileEntity = (file: File) => Promise<FileEntity>;
|
|
71
71
|
export type FunctionTypeJSXElement = () => JSX.Element;
|
|
72
|
-
export type FunctionTypeChatMessagesToVoid = (
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
// export type FunctionTypeChatMessagesToVoid = (
|
|
73
|
+
// lastMessage: string,
|
|
74
|
+
// messages: IChatMessage,
|
|
75
|
+
// ) => void;
|
|
76
|
+
export type FunctionTypeFileWithContextToToVoid = (
|
|
77
|
+
file: File,
|
|
78
|
+
context: IChatMessage[],
|
|
79
|
+
) => void;
|
|
80
|
+
export type FunctionTypeStringWithContextToVoid = (
|
|
81
|
+
value: string,
|
|
82
|
+
context: IChatMessage[],
|
|
75
83
|
) => void;
|
|
@@ -314,7 +314,6 @@ const DialogsComponent: React.FC<DialogsProps> = ({
|
|
|
314
314
|
<ColumnContainer>
|
|
315
315
|
{useUpContent && upHeaderContent}
|
|
316
316
|
{useHeader && HeaderContent}
|
|
317
|
-
{/* {showSearchDialogs ? renderSearchDialogs() : null} */}
|
|
318
317
|
{useSubContent && subHeaderContent}
|
|
319
318
|
{dialogsViewModel?.loading && (
|
|
320
319
|
// <div style={{ maxHeight: '44px', minHeight: '44px', height: '44px' }}>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FunctionTypeFileWithContextToToVoid,
|
|
3
|
+
FunctionTypeJSXElement,
|
|
4
|
+
FunctionTypeStringWithContextToVoid,
|
|
5
|
+
} from '../../../../../Views/Base/BaseViewModel';
|
|
6
|
+
|
|
7
|
+
export interface AIWidget {
|
|
8
|
+
renderWidget: FunctionTypeJSXElement;
|
|
9
|
+
textToWidget: FunctionTypeStringWithContextToVoid;
|
|
10
|
+
fileToWidget: FunctionTypeFileWithContextToToVoid;
|
|
11
|
+
textToContent: string | undefined;
|
|
12
|
+
fileToContent: File | undefined;
|
|
13
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { useState, CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
type ErrorDescription = {
|
|
4
|
+
title: string;
|
|
5
|
+
action: () => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type ErrorMessageIconProps = {
|
|
9
|
+
errorMessageText: string;
|
|
10
|
+
errorsDescriptions?: ErrorDescription[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const errorMessageIconStyles: { [key: string]: CSSProperties } = {
|
|
14
|
+
errorIcon: {
|
|
15
|
+
display: 'inline-block',
|
|
16
|
+
position: 'relative',
|
|
17
|
+
width: '21px', // Уменьшен размер круга
|
|
18
|
+
height: '21px', // Уменьшен размер круга
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
},
|
|
21
|
+
circle: {
|
|
22
|
+
width: '21px', // Уменьшен размер круга
|
|
23
|
+
height: '21px', // Уменьшен размер круга
|
|
24
|
+
backgroundColor: 'red',
|
|
25
|
+
borderRadius: '50%',
|
|
26
|
+
display: 'flex',
|
|
27
|
+
justifyContent: 'center',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
},
|
|
30
|
+
exclamationMark: {
|
|
31
|
+
color: 'white',
|
|
32
|
+
fontSize: '12px', // Уменьшен размер восклицательного знака
|
|
33
|
+
},
|
|
34
|
+
errorMessage: {
|
|
35
|
+
position: 'absolute',
|
|
36
|
+
bottom: '100%', // Изменено выравнивание, чтобы текст был выше
|
|
37
|
+
left: '50%',
|
|
38
|
+
transform: 'translateX(-50%)',
|
|
39
|
+
backgroundColor: 'lightgray',
|
|
40
|
+
border: '1px solid gray',
|
|
41
|
+
padding: '6px',
|
|
42
|
+
borderRadius: '8px',
|
|
43
|
+
whiteSpace: 'nowrap',
|
|
44
|
+
display: 'inline-block',
|
|
45
|
+
zIndex: 1, // Установлен zIndex, чтобы текст был выше других элементов
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function ErrorMessageIcon({
|
|
50
|
+
errorMessageText,
|
|
51
|
+
errorsDescriptions,
|
|
52
|
+
}: ErrorMessageIconProps) {
|
|
53
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
54
|
+
|
|
55
|
+
const handleMouseEnter = () => {
|
|
56
|
+
setIsHovered(true);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleMouseLeave = () => {
|
|
60
|
+
setIsHovered(false);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
style={{
|
|
66
|
+
...errorMessageIconStyles.errorIcon,
|
|
67
|
+
...(isHovered ? { backgroundColor: 'yellow' } : {}),
|
|
68
|
+
}}
|
|
69
|
+
onMouseEnter={handleMouseEnter}
|
|
70
|
+
onMouseLeave={handleMouseLeave}
|
|
71
|
+
>
|
|
72
|
+
<div style={errorMessageIconStyles.circle}>
|
|
73
|
+
<span style={errorMessageIconStyles.exclamationMark}>!</span>
|
|
74
|
+
</div>
|
|
75
|
+
{isHovered && (
|
|
76
|
+
<div style={errorMessageIconStyles.errorMessage}>
|
|
77
|
+
{errorMessageText}
|
|
78
|
+
{errorsDescriptions?.map((item, index) => (
|
|
79
|
+
<div
|
|
80
|
+
key={index}
|
|
81
|
+
style={{
|
|
82
|
+
padding: '4px',
|
|
83
|
+
cursor: 'pointer',
|
|
84
|
+
}}
|
|
85
|
+
onClick={() => {
|
|
86
|
+
item.action();
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{item.title}
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default ErrorMessageIcon;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { AIWidget } from './AIWidget';
|
|
3
|
+
import AIWidgetIcon from '../../../svgs/Icons/Media/AIWidget';
|
|
4
|
+
import { IChatMessage } from '../../../../../Views/Base/BaseViewModel';
|
|
5
|
+
import { stringifyError } from '../../../../../../utils/parse';
|
|
6
|
+
import ErrorMessageIcon from './ErrorMessageIcon';
|
|
7
|
+
|
|
8
|
+
interface MessageWidgetProps {
|
|
9
|
+
// https://api.openai.com/v1/chat/completions'
|
|
10
|
+
// api: 'v1/chat/completions',
|
|
11
|
+
// servername: 'https://myproxy.com',
|
|
12
|
+
// https://func270519800.azurewebsites.net/api/TranslateTextToEng
|
|
13
|
+
servername: string;
|
|
14
|
+
api: string;
|
|
15
|
+
port: string;
|
|
16
|
+
sessionToken: string;
|
|
17
|
+
}
|
|
18
|
+
export default function UseDefaultAIAssistAnswerWidgetWithProxy({
|
|
19
|
+
servername,
|
|
20
|
+
api,
|
|
21
|
+
port,
|
|
22
|
+
sessionToken,
|
|
23
|
+
}: MessageWidgetProps): AIWidget {
|
|
24
|
+
const [errorMessage, setErrorMessage] = useState<string>('');
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
|
|
27
|
+
const fileToWidget = (file: File, context: IChatMessage[]): void => {};
|
|
28
|
+
|
|
29
|
+
const renderWidget = (): JSX.Element => {
|
|
30
|
+
if (errorMessage && errorMessage.length > 0) {
|
|
31
|
+
const errorsDescriptions:
|
|
32
|
+
| { title: string; action: () => void }[]
|
|
33
|
+
| undefined = [];
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ErrorMessageIcon
|
|
37
|
+
errorMessageText={errorMessage}
|
|
38
|
+
errorsDescriptions={errorsDescriptions}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return <AIWidgetIcon applyZoom color="green" />;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// async function getData(
|
|
47
|
+
// textToSend: string,
|
|
48
|
+
// dialogMessages: ChatCompletionRequestMessage[],
|
|
49
|
+
// ): Promise<string> {
|
|
50
|
+
// //
|
|
51
|
+
// const apiEndpoint = 'https://api.openai.com/v1/chat/completions';
|
|
52
|
+
// const { apiKey } = QBConfig.configAIApi.AIAnswerAssistWidgetConfig; // Замените на ваш реальный ключ API
|
|
53
|
+
// const model = 'gpt-3.5-turbo';
|
|
54
|
+
// const requestOptions = {
|
|
55
|
+
// method: 'POST',
|
|
56
|
+
// headers: {
|
|
57
|
+
// 'Content-Type': 'application/json',
|
|
58
|
+
// Authorization: `Bearer ${apiKey}`,
|
|
59
|
+
// },
|
|
60
|
+
// body: JSON.stringify({
|
|
61
|
+
// messages: [...dialogMessages, { role: 'user', content: textToSend }],
|
|
62
|
+
// model,
|
|
63
|
+
// temperature: 0.5,
|
|
64
|
+
// }),
|
|
65
|
+
// };
|
|
66
|
+
//
|
|
67
|
+
async function getData(
|
|
68
|
+
textToSend: string,
|
|
69
|
+
dialogMessages: IChatMessage[],
|
|
70
|
+
): Promise<string> {
|
|
71
|
+
let outputMessage = '';
|
|
72
|
+
const apiEndpoint = `${servername}${port}${api}`;
|
|
73
|
+
const apiKey = sessionToken; // Замените на ваш реальный ключ API
|
|
74
|
+
const model = 'gpt-3.5-turbo';
|
|
75
|
+
const prompt = `Respond as a knowledgeable customer support specialist with access to ChatGPT features, and provide a simple and informative response to the inquiry :"${textToSend}"`;
|
|
76
|
+
|
|
77
|
+
const requestOptions = {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
Authorization: `Bearer ${apiKey}`,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
messages: [...dialogMessages, { role: 'user', content: prompt }],
|
|
85
|
+
model,
|
|
86
|
+
temperature: 0.5,
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(apiEndpoint, requestOptions);
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
|
|
95
|
+
outputMessage = data.choices[0].message?.content || '';
|
|
96
|
+
} catch (err) {
|
|
97
|
+
outputMessage = stringifyError(err);
|
|
98
|
+
setErrorMessage(outputMessage);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return outputMessage;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const [textFromWidgetToContent, setTextFromWidgetToContent] = useState('');
|
|
105
|
+
// const textToWidget = (value: string, context: IChatMessage[]): void => {
|
|
106
|
+
// if (value && value.length > 0) {
|
|
107
|
+
// // eslint-disable-next-line promise/catch-or-return
|
|
108
|
+
// getOpenAIApiData(value, context as ChatCompletionRequestMessage[]).then(
|
|
109
|
+
// // eslint-disable-next-line promise/always-return
|
|
110
|
+
// (data) => {
|
|
111
|
+
// setTextFromWidgetToContent(data);
|
|
112
|
+
// },
|
|
113
|
+
// );
|
|
114
|
+
// }
|
|
115
|
+
// };
|
|
116
|
+
|
|
117
|
+
const textToWidget = (value: string, context: IChatMessage[]): void => {
|
|
118
|
+
if (value && value.length > 0) {
|
|
119
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
120
|
+
getData(value, context).then(
|
|
121
|
+
// eslint-disable-next-line promise/always-return
|
|
122
|
+
(data) => {
|
|
123
|
+
setTextFromWidgetToContent(data);
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
fileToContent: undefined,
|
|
131
|
+
textToContent: textFromWidgetToContent,
|
|
132
|
+
fileToWidget,
|
|
133
|
+
renderWidget,
|
|
134
|
+
textToWidget,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { AIWidget } from './AIWidget';
|
|
3
3
|
|
|
4
|
-
export default function useDefaultTextInputWidget():
|
|
4
|
+
export default function useDefaultTextInputWidget(): AIWidget {
|
|
5
5
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
|
|
6
6
|
const fileToWidget = (file: File): void => {};
|
|
7
7
|
|
|
@@ -51,8 +51,8 @@ export default function useDefaultTextInputWidget(): InputWidget {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
fileToContent: undefined,
|
|
55
|
+
textToContent: textFromWidgetToInput,
|
|
56
56
|
fileToWidget,
|
|
57
57
|
renderWidget,
|
|
58
58
|
textToWidget,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { AIWidget } from './AIWidget';
|
|
3
3
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4
4
|
import VoiceIcon from '../../../svgs/Icons/Actions/Voice';
|
|
5
5
|
|
|
6
|
-
export default function useDefaultVoiceInputWidget():
|
|
6
|
+
export default function useDefaultVoiceInputWidget(): AIWidget {
|
|
7
7
|
const renderWidget = (): JSX.Element => {
|
|
8
8
|
// return <VoiceIcon width="21" height="18" applyZoom color="red" />;
|
|
9
9
|
return (
|
|
@@ -77,8 +77,8 @@ export default function useDefaultVoiceInputWidget(): InputWidget {
|
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
return {
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
fileToContent: audioFromWidgetToInput,
|
|
81
|
+
textToContent: textFromWidgetToInput,
|
|
82
82
|
fileToWidget,
|
|
83
83
|
renderWidget,
|
|
84
84
|
textToWidget,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useState, CSSProperties, useRef, useEffect } from 'react';
|
|
2
|
+
import EditDots from '../../svgs/Icons/Actions/EditDots';
|
|
3
|
+
|
|
4
|
+
type MenuItem = {
|
|
5
|
+
title: string;
|
|
6
|
+
action: () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type ContextMenuProps = {
|
|
10
|
+
items?: MenuItem[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ContextMenuStyles: { [key: string]: CSSProperties } = {
|
|
14
|
+
contextMenuIcon: {
|
|
15
|
+
display: 'inline-block',
|
|
16
|
+
position: 'relative',
|
|
17
|
+
width: '42px',
|
|
18
|
+
height: '42px',
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
},
|
|
21
|
+
contextMenuContent: {
|
|
22
|
+
position: 'absolute',
|
|
23
|
+
top: '0',
|
|
24
|
+
left: '50%',
|
|
25
|
+
transform: 'translateX(-50%)',
|
|
26
|
+
backgroundColor: 'white',
|
|
27
|
+
border: '1px solid gray',
|
|
28
|
+
borderRadius: '8px',
|
|
29
|
+
padding: '4px',
|
|
30
|
+
zIndex: 1,
|
|
31
|
+
width: 'max-content',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function ContextMenu({ items }: ContextMenuProps) {
|
|
36
|
+
const [menuVisible, setMenuVisible] = useState(false);
|
|
37
|
+
const contextMenuRef = useRef<HTMLDivElement | null>(null);
|
|
38
|
+
|
|
39
|
+
const handleClick = () => {
|
|
40
|
+
setMenuVisible(!menuVisible);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleMenuItemClick = (action: () => void) => {
|
|
44
|
+
action();
|
|
45
|
+
setMenuVisible(false); // Закрыть контекстное меню после клика на пункт меню
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
function handleClickOutside(event: MouseEvent) {
|
|
50
|
+
if (
|
|
51
|
+
contextMenuRef.current &&
|
|
52
|
+
!contextMenuRef.current.contains(event.target as Node)
|
|
53
|
+
) {
|
|
54
|
+
setMenuVisible(false);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
style={{
|
|
68
|
+
...ContextMenuStyles.contextMenuIcon,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<div onClick={handleClick}>
|
|
72
|
+
<EditDots />
|
|
73
|
+
</div>
|
|
74
|
+
{menuVisible && (
|
|
75
|
+
<div ref={contextMenuRef} style={ContextMenuStyles.contextMenuContent}>
|
|
76
|
+
{items?.map((item, index) => (
|
|
77
|
+
<div
|
|
78
|
+
key={index}
|
|
79
|
+
style={{
|
|
80
|
+
padding: '4px',
|
|
81
|
+
cursor: 'pointer',
|
|
82
|
+
}}
|
|
83
|
+
onClick={() => {
|
|
84
|
+
handleMenuItemClick(item.action); // Используем новую функцию обработчика
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{item.title}
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default ContextMenu;
|