widget-chatbot 1.0.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.
@@ -0,0 +1,287 @@
1
+ import React, { memo, useState, useMemo, useEffect } from 'react';
2
+ import { Button, Tooltip, Upload } from 'antd';
3
+ import TextareaAutosize from 'react-textarea-autosize';
4
+ import AttachmentIcon from 'assets/icons/AttachmentIcon';
5
+ import DeleteUploadIcon from 'assets/icons/DeleteUploadIcon';
6
+ import LoadingLiveChatIcon from 'assets/LoadingLiveChatIcon';
7
+ import Send from 'assets/send_button';
8
+ import isStringNull from './helpers/isStringNull';
9
+ import { useHelpdeskChatContext } from '../../HelpdeskChatContextProvider';
10
+ import useFetch from '../../../../../../hooks/useFetch';
11
+ import { useConversationContext } from '../../../../../../context/ConversationContextProvider';
12
+ import ModalResult from '../../../../../../helpers/ModalResult';
13
+ import getBase64 from '../../../../../../helpers/getBase64';
14
+ import getTextWidth from '../../../../../../helpers/getTextWidth';
15
+ import { useErrorBoundaryContext } from '../../../../../../context/ErrorBoundaryContextProvider';
16
+ import useDictionary from '../../../../../../hooks/useDictionary';
17
+ import { usePayloadContext } from '../../../../../../PayloadContextProvider';
18
+
19
+ export const WrapperFileName = ({ fileName, children }) => {
20
+ const widthFileName = getTextWidth(fileName);
21
+
22
+ if (widthFileName > 250) {
23
+ return <Tooltip title={fileName}>{children}</Tooltip>;
24
+ }
25
+
26
+ return children;
27
+ };
28
+
29
+ const AttachmentField = () => {
30
+ const { chatState, setChatState } = useHelpdeskChatContext();
31
+ const [urlImage, setUrlImg] = useState('');
32
+
33
+ const isImage = useMemo(() => {
34
+ return chatState?.fileInput?.type?.includes('image');
35
+ }, [chatState?.fileInput]);
36
+
37
+ const onConvertImgToBase64 = async () => {
38
+ const base64Img = await getBase64(chatState?.fileInput);
39
+
40
+ setUrlImg(base64Img);
41
+ };
42
+
43
+ useEffect(() => {
44
+ if (chatState?.fileInput?.uid) {
45
+ onConvertImgToBase64();
46
+ }
47
+ }, [chatState?.fileInput?.uid]);
48
+
49
+ useEffect(() => {
50
+ // Monitor socket connection status
51
+ if (socket) {
52
+ console.log("Socket connection status:", socket.connected);
53
+ }
54
+ }, [socket]);
55
+
56
+ return (
57
+ <div className="rw-file-name">
58
+ <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
59
+ {isImage ? (
60
+ <img
61
+ src={urlImage}
62
+ style={{ width: 52, cursor: 'pointer' }}
63
+ onClick={(e) => {
64
+ e?.stopPropagation();
65
+
66
+ setChatState((prev) => ({
67
+ ...prev,
68
+ isShowModalPreviewFile: true,
69
+ urlImagePreview: urlImage,
70
+ }));
71
+ }}
72
+ />
73
+ ) : (
74
+ <AttachmentIcon className="rw-attachment-icon-list" />
75
+ )}
76
+ <WrapperFileName fileName={chatState?.fileInput?.name}>
77
+ <p className="rw-file-name-text">{chatState?.fileInput?.name}</p>
78
+ </WrapperFileName>
79
+ </div>
80
+ <Button
81
+ style={{ color: 'rgba(0, 0, 0, 0.45)' }}
82
+ icon={<DeleteUploadIcon />}
83
+ onClick={() => {
84
+ setChatState((prev) => ({
85
+ ...prev,
86
+ fileInput: null,
87
+ }));
88
+ }}
89
+ />
90
+ </div>
91
+ );
92
+ };
93
+
94
+ const HelpdeskSender = () => {
95
+ const [inputVal, setInputVal] = useState('');
96
+ const { socket } = usePayloadContext(); // Tambahkan ini jika belum ada
97
+ const { taskIDSelect, connected, isBackClick } = useConversationContext();
98
+ const { setChatState, chatState } = useHelpdeskChatContext();
99
+ const { isErrorBoundary } = useErrorBoundaryContext();
100
+
101
+ const LabelTextFieldHint = useDictionary('TypeMessage', 'Type a message');
102
+
103
+ const inputValDOM = document.querySelector('.rw-new-message')?.value;
104
+
105
+ const fetch = useFetch();
106
+
107
+ const isDisabledInput = useMemo(() => {
108
+ return (
109
+ isErrorBoundary ||
110
+ !connected ||
111
+ ((isStringNull(inputValDOM) || !chatState?.fileInput) && chatState?.loadingSend)
112
+ );
113
+ }, [inputValDOM, chatState?.fileInput, chatState?.loadingSend, connected]);
114
+
115
+ const classNameHelpdesk = useMemo(() => {
116
+ if (isBackClick) {
117
+ return 'rw-container-helpdesk-sender-from-detail';
118
+ }
119
+ return 'rw-container-helpdesk-sender';
120
+ }, [isBackClick]);
121
+
122
+ // HelpdeskSender.js - onSendMessage yang direvisi
123
+ const onSendMessage = () => {
124
+ if (!socket?.connected) {
125
+ console.warn("Socket not connected!");
126
+ ModalResult({
127
+ type: 'error',
128
+ error: new Error("Connection lost. Please refresh the page."),
129
+ });
130
+ return;
131
+ }
132
+
133
+ setChatState((prev) => ({
134
+ ...prev,
135
+ loadingSend: true,
136
+ }));
137
+
138
+ fetch({
139
+ endpoint: 'v1/sf7/general/helpdesk/chat/submit',
140
+ data: {
141
+ TASK_ID: taskIDSelect,
142
+ ATTACHMENT: '',
143
+ FAQ_ID: null,
144
+ MESSAGE: inputVal,
145
+ },
146
+ })
147
+ ?.then(() => {
148
+ console.log("Message sent successfully");
149
+ setInputVal('');
150
+ // Clear input field
151
+ const messageInput = document.querySelector('.rw-new-message');
152
+ if (messageInput) {
153
+ messageInput.value = '';
154
+ }
155
+ // Reset loading state after successful send
156
+ setChatState((prev) => ({
157
+ ...prev,
158
+ loadingSend: false
159
+ }));
160
+ })
161
+ ?.catch((error) => {
162
+ console.error("Error sending message:", error);
163
+ ModalResult({
164
+ type: 'error',
165
+ error,
166
+ });
167
+ setChatState((prev) => ({
168
+ ...prev,
169
+ loadingSend: false,
170
+ }));
171
+ });
172
+ };
173
+
174
+ const onSendFile = (url) => {
175
+ fetch({
176
+ endpoint: 'v1/sf7/general/helpdesk/chat/submit',
177
+ data: {
178
+ ATTACHMENT: url,
179
+ FAQ_ID: null,
180
+ TASK_ID: taskIDSelect,
181
+ },
182
+ })?.catch((error) => {
183
+ ModalResult({
184
+ type: 'error',
185
+ error,
186
+ });
187
+ setChatState((prev) => ({
188
+ ...prev,
189
+ loadingSend: false,
190
+ }));
191
+ });
192
+ };
193
+
194
+ const onGetURL = () => {
195
+ setChatState((prev) => ({
196
+ ...prev,
197
+ loadingSend: true,
198
+ }));
199
+
200
+ const formData = new FormData();
201
+
202
+ formData?.append('FILE', chatState?.fileInput);
203
+ formData?.append('UPLOAD_CODE', 'helpdesk');
204
+ formData?.append('UPLOAD_TYPE', 3);
205
+
206
+ fetch({
207
+ endpoint: 'v1/sf7/hrm/upload/temp',
208
+ data: formData,
209
+ isFormData: true,
210
+ })?.then(({ data }) => {
211
+ const urlAttachment = data?.DATA?.LIST?.TEMP_PATH;
212
+ onSendFile(urlAttachment);
213
+ });
214
+ };
215
+
216
+ const onEnterSendMsg = (e) => {
217
+ if (!isErrorBoundary && e?.keyCode === 13 && !isStringNull(inputVal) && !e.shiftKey) {
218
+ e.preventDefault();
219
+ onSendMessage();
220
+ }
221
+ };
222
+
223
+ return (
224
+ <div className={`${classNameHelpdesk} rw-container-sender`}>
225
+ {chatState?.fileInput ? (
226
+ <div className="rw-filelist-container">
227
+ <AttachmentField />
228
+ </div>
229
+ ) : (
230
+ <TextareaAutosize
231
+ type="text"
232
+ minRows={2}
233
+ onKeyDown={onEnterSendMsg}
234
+ maxRows={5}
235
+ onChange={({ target: { value } }) => {
236
+ setInputVal(value);
237
+ }}
238
+ className="rw-new-message"
239
+ name="message"
240
+ autoFocus
241
+ autoComplete="off"
242
+ placeholder={`${LabelTextFieldHint}...`}
243
+ />
244
+ )}
245
+
246
+ <div className="rw-container-btn-send">
247
+ <Upload
248
+ className="btn_upload"
249
+ beforeUpload={() => false}
250
+ fileList={[]}
251
+ accept=".doc, .jpg, .ods, .png, .txt, .docx, .pdf"
252
+ onChange={(e) => {
253
+ setChatState((prev) => ({
254
+ ...prev,
255
+ fileInput: e?.file,
256
+ }));
257
+ }}
258
+ >
259
+ <div className="rw-new-topic">
260
+ <AttachmentIcon />
261
+ Attachment
262
+ </div>
263
+ </Upload>
264
+
265
+ <button
266
+ onClick={() => {
267
+ if (chatState?.fileInput) {
268
+ onGetURL();
269
+ } else if (!isStringNull(inputVal)) {
270
+ onSendMessage();
271
+ }
272
+ }}
273
+ type="submit"
274
+ className="rw-send"
275
+ disabled={isDisabledInput}
276
+ >
277
+ {chatState?.loadingSend ? (
278
+ <LoadingLiveChatIcon />
279
+ ) : (
280
+ <Send ready={!isDisabledInput} className="rw-send-icon" alt="send" />
281
+ )}
282
+ </button>
283
+ </div>
284
+ </div>
285
+ );
286
+ };
287
+ export default memo(HelpdeskSender);
@@ -0,0 +1,131 @@
1
+ import axios from 'axios';
2
+ import React, { useState, useMemo, useContext, createContext, useEffect } from 'react';
3
+ import { BASE_WEBSOCKET_URL } from './helpers/constant';
4
+ import { io } from 'socket.io-client';
5
+
6
+ /** @type {import('react').Context<PayloadContextData>} */
7
+ const PayloadContext = createContext({});
8
+
9
+ export const usePayloadContext = () => useContext(PayloadContext);
10
+
11
+ /**
12
+ *
13
+ * @param {object} props
14
+ * @param {string} props.initPayload
15
+ * @returns
16
+ */
17
+ const PayloadContextProvider = ({ children, initPayload, setInitPayload }) => {
18
+ /** @type {PayloadContextData} */
19
+ const objInitPayload = useMemo(() => {
20
+ return JSON.parse(initPayload?.replace('/get_started', '') || '{}');
21
+ }, [initPayload]);
22
+
23
+ const [isRefetch, setRefetch] = useState(false);
24
+ const [isVerifyHelpdesk, setVerifyHelpdesk] = useState(false);
25
+ const [isLoadedAccessWS, setLoadedAccessWS] = useState(false);
26
+ const [socket, setSocket] = useState(null);
27
+
28
+ // Initialize the Socket.io client
29
+ useEffect(() => {
30
+ const newSocket = io(BASE_WEBSOCKET_URL);
31
+ setSocket(newSocket);
32
+
33
+ return () => {
34
+ newSocket.disconnect();
35
+ };
36
+ }, []);
37
+
38
+ const onRefreshToken = async () => {
39
+ try {
40
+ const response = await axios({
41
+ url: objInitPayload?.uriBackend,
42
+ params: {
43
+ ofid: 'sfsystem.RefreshToken',
44
+ accname: objInitPayload?.companycode,
45
+ },
46
+ data: {
47
+ refreshtoken:
48
+ objInitPayload?.refresh_token ||
49
+ '4D8C30EE29749D0F06B4F6E2527516B397C09D4DB148D42214E2941C7A86139D3CB512D925E1B098C6090A45E6BCC545723222FA3BABDC2A',
50
+ },
51
+ method: 'POST',
52
+ });
53
+
54
+ if (response?.data?.DATA) {
55
+ const data = response?.data?.DATA;
56
+
57
+ let payload = '/get_started';
58
+
59
+ payload += JSON.stringify({
60
+ access_token: data?.JWT_TOKEN,
61
+ refresh_token: objInitPayload?.refresh_token,
62
+ uagent: objInitPayload?.uagent,
63
+ uriBackend: objInitPayload?.uriBackend,
64
+ uriService: objInitPayload?.uriService,
65
+ companyid: objInitPayload?.companyid,
66
+ companycode: objInitPayload?.companycode,
67
+ lang: objInitPayload?.lang,
68
+ uriBackendEnt: objInitPayload?.uriBackendEnt,
69
+ });
70
+
71
+ setInitPayload(payload);
72
+
73
+ return data?.JWT_TOKEN;
74
+ } else {
75
+ throw new Error('Failed to refresh token');
76
+ }
77
+ } catch (err) {
78
+ throw err;
79
+ }
80
+ };
81
+
82
+ // Handle Socket.io events
83
+ useEffect(() => {
84
+ if (!socket) return;
85
+
86
+ const { companyid, companycode, lang, uagent, access_token, uriBackendEnt, uriBackend } =
87
+ objInitPayload;
88
+
89
+ const messageHandshake = {
90
+ coid: companyid,
91
+ cocode: companycode,
92
+ lang,
93
+ uagent,
94
+ jwt: access_token,
95
+ uribackendent: uriBackendEnt,
96
+ accname: companycode?.toUpperCase(),
97
+ uribackendlucee: uriBackend,
98
+ };
99
+
100
+ socket.emit('access_ws', messageHandshake);
101
+
102
+ socket.on('access_ws_response', (data) => {
103
+ if (!isLoadedAccessWS) {
104
+ setLoadedAccessWS(true);
105
+ setVerifyHelpdesk(data?.result);
106
+ }
107
+ });
108
+
109
+ // Cleanup on component unmount
110
+ return () => {
111
+ socket.off('access_ws_response');
112
+ };
113
+ }, [socket, initPayload]);
114
+
115
+ return (
116
+ <PayloadContext.Provider
117
+ value={{
118
+ ...objInitPayload,
119
+ onRefreshToken,
120
+ isRefetch,
121
+ setRefetch,
122
+ socket,
123
+ isVerifyHelpdesk,
124
+ }}
125
+ >
126
+ {children}
127
+ </PayloadContext.Provider>
128
+ );
129
+ };
130
+
131
+ export default PayloadContextProvider;
@@ -0,0 +1,28 @@
1
+ import CryptoJS from 'crypto-js';
2
+ import isMobileUserAgent from './isMobileUserAgent';
3
+
4
+ export const IS_MOBILE = isMobileUserAgent();
5
+ // && isWorkplazeMobile();
6
+
7
+ // export const KEY = process?.env?.REACT_APP_KEY || '6Le0DgMTAAAANokdEEial';
8
+ export const KEY = '6Le0DgMTAAAANokdEEial';
9
+
10
+ export const APP_KEY_HEX = (keyEncrypt) =>
11
+ CryptoJS.enc.Hex.parse(CryptoJS.enc.Utf8.parse(keyEncrypt || KEY).toString());
12
+
13
+ export const CIPHER_OPTIONS = { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 };
14
+
15
+ export const ENCRYPTED_ROUTE_PREFIX = '|';
16
+ export const UNENCRYPTED_ROUTE_PREFIX = '#';
17
+
18
+ // export const = 'wss://sfchatbot.dataon.com:5005/livechat-he lpdesk/ws';
19
+
20
+ // export const BASE_WEBSOCKET_URL = 'ws://localhost:8002/livechat-helpdesk/ws';
21
+ export const BASE_WEBSOCKET_URL = "http://localhost:5100/livechat-helpdesk/ws";
22
+
23
+
24
+
25
+
26
+
27
+
28
+
@@ -0,0 +1,178 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { usePayloadContext } from '../PayloadContextProvider';
3
+ import sortArr from '../helpers/sortArr';
4
+ import { scrollToBottom } from '../components/Widget/components/Conversation/components/Messages';
5
+ import uniqueArrObj from '../helpers/uniqueArrObj';
6
+ import useDecodeJWT from './useDecodeJWT';
7
+ import formatChat, { deFormatChat } from '../helpers/formatChat';
8
+ import { useConversationContext } from '../context/ConversationContextProvider';
9
+ import { useHelpdeskChatContext } from '../components/Widget/components/Conversation/HelpdeskChatContextProvider';
10
+ import { useListTicketContext } from '../context/ListTicketContextProvider';
11
+ import ModalResult from '../helpers/ModalResult';
12
+
13
+ const useWSChatbot = () => {
14
+ const { socket } = usePayloadContext();
15
+ const { taskIDSelect } = useConversationContext();
16
+ const { ticketState, taskIDs, setTicketState } = useListTicketContext();
17
+ const { chatState, setChatState, isFetchPrevChat } = useHelpdeskChatContext();
18
+ const { EMP_ID } = useDecodeJWT();
19
+
20
+ // Refs untuk mengakses state terbaru
21
+ const chatStateRef = useRef(chatState);
22
+ const ticketStateRef = useRef(ticketState);
23
+ const isFetchPrevChatRef = useRef(isFetchPrevChat);
24
+
25
+ useEffect(() => {
26
+ chatStateRef.current = chatState;
27
+ }, [chatState]);
28
+
29
+ useEffect(() => {
30
+ ticketStateRef.current = ticketState;
31
+ }, [ticketState]);
32
+
33
+ useEffect(() => {
34
+ isFetchPrevChatRef.current = isFetchPrevChat;
35
+ }, [isFetchPrevChat]);
36
+
37
+ const handleNewNotif = (data) => {
38
+ console.log("New Notification Data:", data);
39
+ const newTicket = data?.filter((val) => val?.newChat);
40
+
41
+ setTimeout(() => {
42
+ setTicketState((prev) => ({
43
+ ...prev,
44
+ count: newTicket?.length,
45
+ }));
46
+ }, 250);
47
+ };
48
+
49
+ const handleNewChat = (data) => {
50
+ try {
51
+ console.log("Processing new chat data:", data);
52
+ if (!Array.isArray(data)) {
53
+ console.warn("Received non-array data in handleNewChat:", data);
54
+ return;
55
+ }
56
+
57
+ const newChat = data;
58
+ const deFormatOldChat = deFormatChat(chatStateRef.current?.list || []);
59
+ console.log("Current chat list:", deFormatOldChat);
60
+
61
+ const arrNewChat = [...deFormatOldChat, ...newChat];
62
+ console.log("Combined chat list:", arrNewChat);
63
+
64
+ const sortNewChat = uniqueArrObj(sortArr(arrNewChat, 'ID'), 'ID');
65
+ console.log("Sorted and deduplicated chat list:", sortNewChat);
66
+
67
+ const unreadChat = newChat?.filter(
68
+ (item) => item?.READ_BY === '' && item?.ACTOR?.EMP_ID !== EMP_ID
69
+ );
70
+
71
+ setTicketState((prev) => ({
72
+ ...prev,
73
+ count: unreadChat?.length,
74
+ }));
75
+
76
+ setChatState((prev) => {
77
+ console.log("Updating chat state with new list");
78
+ return {
79
+ ...prev,
80
+ loadingSend: false,
81
+ ...(prev.loadingInit && {
82
+ loadingInit: false,
83
+ }),
84
+ list: formatChat(sortNewChat),
85
+ fileInput: null,
86
+ };
87
+ });
88
+
89
+ if (!isFetchPrevChatRef.current) {
90
+ console.log("Scrolling to bottom");
91
+ scrollToBottom();
92
+ }
93
+ } catch (error) {
94
+ console.error("Error in handleNewChat:", error);
95
+ }
96
+ };
97
+
98
+ useEffect(() => {
99
+ if (!socket) return;
100
+
101
+ console.log("Setting up WebSocket listeners");
102
+
103
+ // Debug socket connection
104
+ socket.on('connect', () => {
105
+ console.log('WebSocket connected');
106
+ });
107
+
108
+ socket.on('disconnect', () => {
109
+ console.log('WebSocket disconnected');
110
+ });
111
+
112
+ // Listener untuk new_notif
113
+ socket.on('new_notif', handleNewNotif);
114
+
115
+ // Listener untuk new_chat
116
+ socket.on('new_chat', (data) => {
117
+ console.log("Received new_chat event:", data);
118
+ handleNewChat(data);
119
+ });
120
+
121
+ // Clean up listeners saat komponen unmount
122
+ return () => {
123
+ console.log("Cleaning up WebSocket listeners");
124
+ socket.off('new_notif', handleNewNotif);
125
+ socket.off('new_chat', handleNewChat);
126
+ };
127
+ }, [socket, EMP_ID]); // Hapus chatState, ticketState, dan isFetchPrevChat
128
+
129
+ const onWatchNotif = () => {
130
+ const params = { task_id_list: taskIDs };
131
+ console.log("Watching Notifications:", params);
132
+
133
+ if (!ticketState?.isInitNotif) {
134
+ socket?.emit('load_notif', params);
135
+ console.log("Notification Event Sent to Server:", params);
136
+
137
+ setTimeout(() => {
138
+ setTicketState((prev) => ({
139
+ ...prev,
140
+ isInitNotif: true,
141
+ }));
142
+ }, 350);
143
+ }
144
+ };
145
+
146
+ const onWatchLiveChat = () => {
147
+ const params = {
148
+ taskid: taskIDSelect,
149
+ list_chat_id: deFormatChat(chatStateRef.current?.list || [])?.map((item) => item?.ID),
150
+ };
151
+ console.log("Watching Live Chat:", params);
152
+
153
+ if (deFormatChat(chatStateRef.current?.list || [])?.length > 0) {
154
+ if (!chatStateRef.current?.isInitChat) {
155
+ setChatState((prev) => ({
156
+ ...prev,
157
+ isInitChat: true,
158
+ }));
159
+ }
160
+ socket?.emit('live_chat', params);
161
+ console.log("Live Chat Event Sent to Server:", params);
162
+ }
163
+ };
164
+
165
+ const onStopNotif = () => {
166
+ socket?.emit('stop_notif');
167
+ console.log("Stopping Notifications");
168
+ };
169
+
170
+ const onStopLiveChat = () => {
171
+ socket?.emit('stop_live_chat');
172
+ console.log("Stopping Live Chat");
173
+ };
174
+
175
+ return { onStopNotif, onStopLiveChat, onWatchNotif, onWatchLiveChat };
176
+ };
177
+
178
+ export default useWSChatbot;
package/npmrc.enc ADDED
Binary file