ui-soxo-bootstrap-core 2.6.1-dev.16 → 2.6.1-dev.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/core/components/landing-api/landing-api.js +29 -12
- package/core/components/license-management/license-alert.js +97 -0
- package/core/lib/components/global-header/animations.js +78 -4
- package/core/lib/components/global-header/global-header.js +49 -23
- package/core/lib/components/global-header/global-header.scss +162 -24
- package/core/lib/components/sidemenu/animations.js +84 -2
- package/core/lib/components/sidemenu/sidemenu.js +175 -55
- package/core/lib/components/sidemenu/sidemenu.scss +221 -14
- package/core/lib/models/process/components/process-dashboard/process-dashboard.js +469 -3
- package/core/lib/models/process/components/process-dashboard/process-dashboard.scss +4 -0
- package/core/lib/pages/login/login.js +20 -37
- package/core/lib/utils/common/common.utils.js +0 -35
- package/core/models/menus/menus.js +6 -0
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +0 -1
- package/core/modules/steps/action-buttons.js +60 -46
- package/core/modules/steps/action-buttons.scss +45 -34
- package/core/modules/steps/chat-assistant.js +141 -0
- package/core/modules/steps/openai-realtime.js +275 -0
- package/core/modules/steps/readme.md +167 -0
- package/core/modules/steps/steps.js +1063 -85
- package/core/modules/steps/steps.scss +462 -280
- package/core/modules/steps/voice-navigation.js +709 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
import React, { useState, useEffect, useContext } from 'react';
|
|
17
|
+
import React, { useState, useEffect, useContext, useRef } from 'react';
|
|
18
18
|
|
|
19
19
|
import { Timeline, Card, Skeleton, Button, Modal, Typography, Form, Select, message, Tag } from 'antd';
|
|
20
20
|
|
|
@@ -28,7 +28,7 @@ import TaskStatus from './../task-status/task-status';
|
|
|
28
28
|
|
|
29
29
|
import DateUtils from '../../../../utils/date/date.utils';
|
|
30
30
|
|
|
31
|
-
import { ClockCircleOutlined, CopyOutlined, CheckCircleOutlined, LoadingOutlined, EditOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
31
|
+
import { ClockCircleOutlined, CopyOutlined, CheckCircleOutlined, LoadingOutlined, EditOutlined, ReloadOutlined, SoundOutlined, PauseCircleOutlined } from '@ant-design/icons';
|
|
32
32
|
|
|
33
33
|
import { CopyToClipBoard } from '../../../..';
|
|
34
34
|
|
|
@@ -59,6 +59,340 @@ let stepMaster = {
|
|
|
59
59
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const GEMINI_TTS_MODEL =
|
|
63
|
+
process.env.GEMINI_TTS_MODEL || process.env.REACT_APP_GEMINI_TTS_MODEL || 'gemini-2.5-flash-preview-tts';
|
|
64
|
+
const GEMINI_TTS_VOICE = process.env.GEMINI_TTS_VOICE || process.env.REACT_APP_GEMINI_TTS_VOICE || 'Kore';
|
|
65
|
+
const GEMINI_TTS_API_BASE_URL =
|
|
66
|
+
process.env.GEMINI_API_BASE_URL || process.env.REACT_APP_GEMINI_API_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta';
|
|
67
|
+
const ELEVENLABS_TTS_API_BASE_URL =
|
|
68
|
+
process.env.ELEVENLABS_TTS_API_BASE_URL ||
|
|
69
|
+
process.env.REACT_APP_ELEVENLABS_TTS_API_BASE_URL ||
|
|
70
|
+
'https://api.elevenlabs.io/v1/text-to-speech';
|
|
71
|
+
const ELEVENLABS_MODEL_ID =
|
|
72
|
+
process.env.ELEVENLABS_MODEL_ID || process.env.REACT_APP_ELEVENLABS_MODEL_ID || 'eleven_multilingual_v2';
|
|
73
|
+
const ELEVENLABS_OUTPUT_FORMAT =
|
|
74
|
+
process.env.ELEVENLABS_OUTPUT_FORMAT || process.env.REACT_APP_ELEVENLABS_OUTPUT_FORMAT || 'mp3_44100_128';
|
|
75
|
+
const DEFAULT_ELEVENLABS_VOICE_ID =
|
|
76
|
+
process.env.ELEVENLABS_VOICE_ID ||
|
|
77
|
+
process.env.ELEVEN_LABS_VOICE_ID ||
|
|
78
|
+
process.env.REACT_APP_ELEVENLABS_VOICE_ID ||
|
|
79
|
+
'21m00Tcm4TlvDq8ikWAM';
|
|
80
|
+
|
|
81
|
+
function getFromStorage(storageKey) {
|
|
82
|
+
|
|
83
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return window.localStorage.getItem(storageKey);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getGeminiApiKey() {
|
|
95
|
+
|
|
96
|
+
if (process.env.GEMINI_API_KEY) {
|
|
97
|
+
return process.env.GEMINI_API_KEY;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (process.env.REACT_APP_GEMINI_API_KEY) {
|
|
101
|
+
return process.env.REACT_APP_GEMINI_API_KEY;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof window !== 'undefined') {
|
|
105
|
+
try {
|
|
106
|
+
if (window.localStorage) {
|
|
107
|
+
return window.localStorage.getItem('gemini_api_key');
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getElevenLabsApiKey() {
|
|
118
|
+
return (
|
|
119
|
+
process.env.ELEVEN_LABS_KEY ||
|
|
120
|
+
process.env.ELEVENLABS_API_KEY ||
|
|
121
|
+
process.env.REACT_APP_ELEVEN_LABS_KEY ||
|
|
122
|
+
process.env.REACT_APP_ELEVENLABS_API_KEY ||
|
|
123
|
+
getFromStorage('eleven_labs_key') ||
|
|
124
|
+
getFromStorage('elevenlabs_api_key') ||
|
|
125
|
+
getFromStorage('ELEVEN_LABS_KEY') ||
|
|
126
|
+
getFromStorage('REACT_APP_ELEVEN_LABS_KEY') ||
|
|
127
|
+
getFromStorage('REACT_APP_ELEVENLABS_API_KEY')
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getStepNarration({ step, step_transactions }) {
|
|
132
|
+
|
|
133
|
+
if (!step) {
|
|
134
|
+
return 'Step details are not available.';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const chunks = [];
|
|
138
|
+
|
|
139
|
+
chunks.push(`Step ${step.name || 'Unnamed step'}.`);
|
|
140
|
+
|
|
141
|
+
if (step.description) {
|
|
142
|
+
chunks.push(step.description);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const currentState = step.current_state || 'pending';
|
|
146
|
+
|
|
147
|
+
chunks.push(`Current status is ${currentState}.`);
|
|
148
|
+
|
|
149
|
+
if (step.roles && step.roles.length) {
|
|
150
|
+
const roleNames = step.roles.map((role) => role && role.name).filter(Boolean);
|
|
151
|
+
|
|
152
|
+
if (roleNames.length) {
|
|
153
|
+
chunks.push(`Pending with ${roleNames.join(', ')}.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (step_transactions && step_transactions.start_time) {
|
|
158
|
+
const startTime = DateUtils.displayFirestoreTime(step_transactions.start_time);
|
|
159
|
+
|
|
160
|
+
if (startTime) {
|
|
161
|
+
chunks.push(`Started at ${startTime}.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (step_transactions && step_transactions.completed && step_transactions.end_time) {
|
|
166
|
+
const endTime = DateUtils.displayFirestoreTime(step_transactions.end_time);
|
|
167
|
+
|
|
168
|
+
if (endTime) {
|
|
169
|
+
chunks.push(`Completed at ${endTime}.`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return chunks.join(' ');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function extractGeminiAudio(payload) {
|
|
177
|
+
const candidates = payload && payload.candidates ? payload.candidates : [];
|
|
178
|
+
|
|
179
|
+
for (const candidate of candidates) {
|
|
180
|
+
const parts = candidate && candidate.content && candidate.content.parts ? candidate.content.parts : [];
|
|
181
|
+
|
|
182
|
+
for (const part of parts) {
|
|
183
|
+
const inlineData = part.inlineData || part.inline_data || part.audio;
|
|
184
|
+
|
|
185
|
+
if (inlineData && inlineData.data) {
|
|
186
|
+
return {
|
|
187
|
+
mimeType: inlineData.mimeType || inlineData.mime_type || 'audio/wav',
|
|
188
|
+
data: inlineData.data
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function synthesizeGeminiAudio(text) {
|
|
198
|
+
|
|
199
|
+
const apiKey = getGeminiApiKey();
|
|
200
|
+
|
|
201
|
+
if (!apiKey) {
|
|
202
|
+
throw new Error('Gemini API key is missing');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const endpoint = `${GEMINI_TTS_API_BASE_URL}/models/${GEMINI_TTS_MODEL}:generateContent?key=${apiKey}`;
|
|
206
|
+
|
|
207
|
+
const response = await fetch(endpoint, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
'Content-Type': 'application/json'
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
contents: [{
|
|
214
|
+
role: 'user',
|
|
215
|
+
parts: [{ text }]
|
|
216
|
+
}],
|
|
217
|
+
generationConfig: {
|
|
218
|
+
responseModalities: ['AUDIO'],
|
|
219
|
+
speechConfig: {
|
|
220
|
+
voiceConfig: {
|
|
221
|
+
prebuiltVoiceConfig: {
|
|
222
|
+
voiceName: GEMINI_TTS_VOICE
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
throw new Error(`Gemini TTS request failed with status ${response.status}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const payload = await response.json();
|
|
235
|
+
|
|
236
|
+
const audio = extractGeminiAudio(payload);
|
|
237
|
+
|
|
238
|
+
if (!audio || !audio.data) {
|
|
239
|
+
throw new Error('Gemini did not return audio data');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return `data:${audio.mimeType};base64,${audio.data}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function synthesizeElevenLabsAudio(text) {
|
|
246
|
+
|
|
247
|
+
const apiKey = getElevenLabsApiKey();
|
|
248
|
+
|
|
249
|
+
if (!apiKey) {
|
|
250
|
+
throw new Error('ElevenLabs API key is missing');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const endpoint = `${ELEVENLABS_TTS_API_BASE_URL}/${encodeURIComponent(DEFAULT_ELEVENLABS_VOICE_ID)}/stream?output_format=${encodeURIComponent(
|
|
254
|
+
ELEVENLABS_OUTPUT_FORMAT
|
|
255
|
+
)}`;
|
|
256
|
+
|
|
257
|
+
const response = await fetch(endpoint, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: {
|
|
260
|
+
'Content-Type': 'application/json',
|
|
261
|
+
Accept: 'audio/mpeg',
|
|
262
|
+
'xi-api-key': apiKey
|
|
263
|
+
},
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
text,
|
|
266
|
+
model_id: ELEVENLABS_MODEL_ID
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
throw new Error(`ElevenLabs TTS request failed with status ${response.status}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return response.blob();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function playAudioDataUri(dataUri, audioReference) {
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
const audio = new Audio(dataUri);
|
|
280
|
+
|
|
281
|
+
audioReference.current = audio;
|
|
282
|
+
|
|
283
|
+
const clean = () => {
|
|
284
|
+
if (audioReference.current === audio) {
|
|
285
|
+
audioReference.current = null;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
audio.onended = () => {
|
|
290
|
+
clean();
|
|
291
|
+
resolve();
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
audio.onpause = () => {
|
|
295
|
+
clean();
|
|
296
|
+
resolve();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
audio.onerror = () => {
|
|
300
|
+
clean();
|
|
301
|
+
reject(new Error('Audio playback failed'));
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
audio.play().catch((error) => {
|
|
305
|
+
clean();
|
|
306
|
+
reject(error);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function playAudioBlob(audioBlob, audioReference, audioUrlReference) {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
if (typeof window === 'undefined' || !window.Audio || !window.URL) {
|
|
314
|
+
reject(new Error('Audio playback is not available'));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (audioUrlReference.current) {
|
|
319
|
+
window.URL.revokeObjectURL(audioUrlReference.current);
|
|
320
|
+
audioUrlReference.current = null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const audioUrl = window.URL.createObjectURL(audioBlob);
|
|
324
|
+
const audio = new Audio(audioUrl);
|
|
325
|
+
|
|
326
|
+
audioReference.current = audio;
|
|
327
|
+
audioUrlReference.current = audioUrl;
|
|
328
|
+
|
|
329
|
+
const clean = () => {
|
|
330
|
+
if (audioReference.current === audio) {
|
|
331
|
+
audioReference.current = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (audioUrlReference.current === audioUrl) {
|
|
335
|
+
window.URL.revokeObjectURL(audioUrl);
|
|
336
|
+
audioUrlReference.current = null;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
audio.onended = () => {
|
|
341
|
+
clean();
|
|
342
|
+
resolve();
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
audio.onpause = () => {
|
|
346
|
+
clean();
|
|
347
|
+
resolve();
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
audio.onerror = () => {
|
|
351
|
+
clean();
|
|
352
|
+
reject(new Error('Audio playback failed'));
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
audio.play().catch((error) => {
|
|
356
|
+
clean();
|
|
357
|
+
reject(error);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function speakWithBrowser(text, speechReference) {
|
|
363
|
+
return new Promise((resolve, reject) => {
|
|
364
|
+
if (typeof window === 'undefined' || !window.speechSynthesis || !window.SpeechSynthesisUtterance) {
|
|
365
|
+
reject(new Error('Speech synthesis is not available'));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const utterance = new window.SpeechSynthesisUtterance(text);
|
|
370
|
+
|
|
371
|
+
utterance.lang = process.env.REACT_APP_TTS_LANG || 'en-US';
|
|
372
|
+
|
|
373
|
+
speechReference.current = utterance;
|
|
374
|
+
|
|
375
|
+
utterance.onend = () => {
|
|
376
|
+
if (speechReference.current === utterance) {
|
|
377
|
+
speechReference.current = null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
resolve();
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
utterance.onerror = () => {
|
|
384
|
+
if (speechReference.current === utterance) {
|
|
385
|
+
speechReference.current = null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
reject(new Error('Browser narration failed'));
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
window.speechSynthesis.cancel();
|
|
392
|
+
window.speechSynthesis.speak(utterance);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
62
396
|
/**
|
|
63
397
|
*
|
|
64
398
|
*/
|
|
@@ -257,8 +591,125 @@ function StepTimelineItem({ id, color, Icon, step, step_transactions, callback }
|
|
|
257
591
|
|
|
258
592
|
const [loading, setLoading] = useState(false);
|
|
259
593
|
|
|
594
|
+
const [narrating, setNarrating] = useState(false);
|
|
595
|
+
|
|
260
596
|
const { user = { locations: [] } } = useContext(GlobalContext);
|
|
261
597
|
|
|
598
|
+
const audioReference = useRef(null);
|
|
599
|
+
|
|
600
|
+
const audioUrlReference = useRef(null);
|
|
601
|
+
|
|
602
|
+
const speechReference = useRef(null);
|
|
603
|
+
|
|
604
|
+
const narrationSessionReference = useRef(0);
|
|
605
|
+
|
|
606
|
+
const fallbackNoticeShownReference = useRef(false);
|
|
607
|
+
|
|
608
|
+
useEffect(() => {
|
|
609
|
+
return () => {
|
|
610
|
+
stopNarration();
|
|
611
|
+
}
|
|
612
|
+
}, [])
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Stop active narration audio
|
|
616
|
+
*/
|
|
617
|
+
function stopNarration() {
|
|
618
|
+
narrationSessionReference.current += 1;
|
|
619
|
+
|
|
620
|
+
if (audioReference.current) {
|
|
621
|
+
audioReference.current.pause();
|
|
622
|
+
audioReference.current.src = '';
|
|
623
|
+
audioReference.current = null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (audioUrlReference.current && typeof window !== 'undefined' && window.URL) {
|
|
627
|
+
window.URL.revokeObjectURL(audioUrlReference.current);
|
|
628
|
+
audioUrlReference.current = null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
|
632
|
+
window.speechSynthesis.cancel();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
speechReference.current = null;
|
|
636
|
+
setNarrating(false);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Narrate the current step with ElevenLabs/Gemini TTS only (no browser fallback).
|
|
641
|
+
*/
|
|
642
|
+
async function narrateStep() {
|
|
643
|
+
const narration = getStepNarration({ step, step_transactions });
|
|
644
|
+
|
|
645
|
+
if (!narration) {
|
|
646
|
+
message.warning('Narration text is not available for this step.');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
stopNarration();
|
|
651
|
+
|
|
652
|
+
const sessionId = narrationSessionReference.current;
|
|
653
|
+
const hasElevenLabsKey = Boolean(getElevenLabsApiKey());
|
|
654
|
+
|
|
655
|
+
setNarrating(true);
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
let aiNarrationComplete = false;
|
|
659
|
+
let aiNarrationError = null;
|
|
660
|
+
|
|
661
|
+
if (hasElevenLabsKey) {
|
|
662
|
+
try {
|
|
663
|
+
const audioBlob = await synthesizeElevenLabsAudio(narration);
|
|
664
|
+
|
|
665
|
+
if (sessionId !== narrationSessionReference.current) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
await playAudioBlob(audioBlob, audioReference, audioUrlReference);
|
|
670
|
+
aiNarrationComplete = true;
|
|
671
|
+
} catch (elevenLabsError) {
|
|
672
|
+
aiNarrationError = elevenLabsError;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!aiNarrationComplete) {
|
|
677
|
+
try {
|
|
678
|
+
const dataUri = await synthesizeGeminiAudio(narration);
|
|
679
|
+
|
|
680
|
+
if (sessionId !== narrationSessionReference.current) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
await playAudioDataUri(dataUri, audioReference);
|
|
685
|
+
aiNarrationComplete = true;
|
|
686
|
+
} catch (geminiError) {
|
|
687
|
+
aiNarrationError = geminiError;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!aiNarrationComplete) {
|
|
692
|
+
throw aiNarrationError || new Error('AI narration unavailable');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (sessionId !== narrationSessionReference.current) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (!fallbackNoticeShownReference.current) {
|
|
701
|
+
message.warning(`${hasElevenLabsKey ? 'ElevenLabs/Gemini' : 'Gemini'} narration unavailable.`);
|
|
702
|
+
fallbackNoticeShownReference.current = true;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
message.error(error?.message || 'Unable to play narration for this step.');
|
|
706
|
+
} finally {
|
|
707
|
+
if (sessionId === narrationSessionReference.current) {
|
|
708
|
+
setNarrating(false);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
262
713
|
/**
|
|
263
714
|
* Function to open editModal for Step
|
|
264
715
|
*
|
|
@@ -347,6 +798,21 @@ function StepTimelineItem({ id, color, Icon, step, step_transactions, callback }
|
|
|
347
798
|
{/* Actions for a step */}
|
|
348
799
|
<div className="actions">
|
|
349
800
|
|
|
801
|
+
<Button size={'small'} onClick={() => {
|
|
802
|
+
if (narrating) {
|
|
803
|
+
stopNarration();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
narrateStep();
|
|
808
|
+
}}>
|
|
809
|
+
|
|
810
|
+
{narrating ? <PauseCircleOutlined /> : <SoundOutlined />}
|
|
811
|
+
|
|
812
|
+
{narrating ? 'Stop Narration' : 'Narrate'}
|
|
813
|
+
|
|
814
|
+
</Button>
|
|
815
|
+
|
|
350
816
|
{user.isAdmin && <>
|
|
351
817
|
|
|
352
818
|
<CopyToClipBoard record={step} id={step.id} />
|
|
@@ -599,4 +1065,4 @@ function RevertProcessTransaction({ id, process = [], callback }) {
|
|
|
599
1065
|
</Form>
|
|
600
1066
|
</>)
|
|
601
1067
|
|
|
602
|
-
}
|
|
1068
|
+
}
|
|
@@ -30,7 +30,7 @@ import { getAccessToken, getRefreshToken } from '../../utils/http/auth.helper';
|
|
|
30
30
|
|
|
31
31
|
import { Location } from '../../utils';
|
|
32
32
|
|
|
33
|
-
import { checkLicenseStatus, formatMobile,
|
|
33
|
+
import { checkLicenseStatus, formatMobile, safeJSON } from '../../utils/common/common.utils';
|
|
34
34
|
|
|
35
35
|
import { MailOutlined, MessageOutlined, WhatsAppOutlined } from '@ant-design/icons';
|
|
36
36
|
|
|
@@ -50,8 +50,6 @@ const tailLayout = {
|
|
|
50
50
|
|
|
51
51
|
const LICENSE_EXPIRY = '2026-12-12';
|
|
52
52
|
|
|
53
|
-
//password valdity expire
|
|
54
|
-
const PASSWORD_VALIDITY_DAYS = 90;
|
|
55
53
|
|
|
56
54
|
const headers = {
|
|
57
55
|
db_ptr: 'nuraho',
|
|
@@ -115,47 +113,32 @@ function LoginPhone({ history, appSettings }) {
|
|
|
115
113
|
*
|
|
116
114
|
* - Parses `last_password_change` from user.other_details.
|
|
117
115
|
* - Calculates expiry using PASSWORD_VALIDITY_DAYS.
|
|
118
|
-
* - Uses `checkExpiryStatus()` to determine status.
|
|
119
116
|
* - Shows Ant Design warning message if expired or within warning period.
|
|
120
117
|
* - Warning message includes navigation to `/change-password`.
|
|
121
118
|
*
|
|
122
119
|
* Requires:
|
|
123
120
|
* - PASSWORD_VALIDITY_DAYS constant
|
|
124
|
-
* - checkExpiryStatus utility
|
|
125
121
|
* - React Router history
|
|
126
122
|
* - antd message component
|
|
127
123
|
*/
|
|
128
|
-
const handlePasswordExpiryCheck = (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const lastPasswordChange = otherDetails?.last_password_change;
|
|
132
|
-
|
|
133
|
-
if (lastPasswordChange) {
|
|
134
|
-
const onlyDate = new Date(lastPasswordChange).toISOString().split('T')[0];
|
|
135
|
-
const passwordExpiryDate = new Date(onlyDate);
|
|
136
|
-
passwordExpiryDate.setDate(passwordExpiryDate.getDate() + PASSWORD_VALIDITY_DAYS);
|
|
137
|
-
|
|
138
|
-
const passwordStatus = checkExpiryStatus({
|
|
139
|
-
expiryDate: passwordExpiryDate,
|
|
140
|
-
warningDays: 2,
|
|
141
|
-
expiredMessage: 'Your password has expired. Please reset it.',
|
|
142
|
-
warningMessage: (d) => (
|
|
143
|
-
<span>
|
|
144
|
-
Your password will expire in {d} day(s).{' '}
|
|
145
|
-
<a
|
|
146
|
-
onClick={() => {
|
|
147
|
-
history.push('/change-password');
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
150
|
-
Click here to update.
|
|
151
|
-
</a>
|
|
152
|
-
</span>
|
|
153
|
-
),
|
|
154
|
-
});
|
|
124
|
+
const handlePasswordExpiryCheck = (expiryDays) => {
|
|
125
|
+
if (expiryDays == null) return;
|
|
155
126
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
127
|
+
if (expiryDays <= 0) {
|
|
128
|
+
message.error('Your password has expired. Please reset it.');
|
|
129
|
+
|
|
130
|
+
setExpiredPassword(true);
|
|
131
|
+
setShowResetpassword(true);
|
|
132
|
+
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (expiryDays <= 2) {
|
|
137
|
+
message.warning(
|
|
138
|
+
<span>
|
|
139
|
+
Your password will expire in {expiryDays} day(s). <a onClick={() => history.push('/change-password')}>Click here to update.</a>
|
|
140
|
+
</span>
|
|
141
|
+
);
|
|
159
142
|
}
|
|
160
143
|
};
|
|
161
144
|
|
|
@@ -196,7 +179,7 @@ function LoginPhone({ history, appSettings }) {
|
|
|
196
179
|
if (insider_token) localStorage.insider_token = insider_token;
|
|
197
180
|
|
|
198
181
|
if (result.success) {
|
|
199
|
-
handlePasswordExpiryCheck(
|
|
182
|
+
handlePasswordExpiryCheck(result.expiry_in_days);
|
|
200
183
|
|
|
201
184
|
//two_factor_authentication variable is present then proceed Two factor authentication
|
|
202
185
|
if (result.data && result.data.two_factor_authentication) {
|
|
@@ -421,7 +404,7 @@ function LoginPhone({ history, appSettings }) {
|
|
|
421
404
|
// set user info into local storage
|
|
422
405
|
localStorage.setItem('userInfo', JSON.stringify(userInfo));
|
|
423
406
|
|
|
424
|
-
handlePasswordExpiryCheck(result.
|
|
407
|
+
handlePasswordExpiryCheck(result.expiry_in_days);
|
|
425
408
|
|
|
426
409
|
setTimeout(() => history.push('/'), 500);
|
|
427
410
|
} else {
|
|
@@ -123,41 +123,6 @@ export const checkLicenseStatus = (expiryDate) => {
|
|
|
123
123
|
return { valid: true, daysLeft, message: null, level: null };
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
/**
|
|
127
|
-
* Checks password expiry status.
|
|
128
|
-
*
|
|
129
|
-
* @param {string|Date} expiryDate
|
|
130
|
-
* @param {number} warningDays
|
|
131
|
-
* @param {string} expiredMessage
|
|
132
|
-
* @param {(daysLeft: number) => string} warningMessage
|
|
133
|
-
*
|
|
134
|
-
* @returns {{ valid: boolean, daysLeft: number, message: string|null, level: "error"|"warning"|null }}
|
|
135
|
-
*/
|
|
136
|
-
|
|
137
|
-
export const checkExpiryStatus = ({ expiryDate, warningDays, expiredMessage, warningMessage }) => {
|
|
138
|
-
const expiry = new Date(expiryDate);
|
|
139
|
-
|
|
140
|
-
if (isNaN(expiry)) {
|
|
141
|
-
return { valid: false, daysLeft: 0, message: 'Invalid date', level: 'error' };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
expiry.setHours(0, 0, 0, 0);
|
|
145
|
-
const today = new Date();
|
|
146
|
-
today.setHours(0, 0, 0, 0);
|
|
147
|
-
|
|
148
|
-
const msDiff = expiry.getTime() - today.getTime();
|
|
149
|
-
const daysLeft = Math.ceil(msDiff / (1000 * 60 * 60 * 24));
|
|
150
|
-
|
|
151
|
-
if (daysLeft < 0) {
|
|
152
|
-
return { valid: false, daysLeft, message: expiredMessage, level: 'error' };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (daysLeft <= warningDays) {
|
|
156
|
-
return { valid: true, daysLeft, message: warningMessage(daysLeft), level: 'warning' };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { valid: true, daysLeft, message: null, level: null };
|
|
160
|
-
};
|
|
161
126
|
|
|
162
127
|
/**
|
|
163
128
|
* Masks a mobile number by hiding all but the last `visibleDigits`.
|
|
@@ -724,7 +724,6 @@ function GuestList({
|
|
|
724
724
|
const [single, setSingle] = useState({});
|
|
725
725
|
const otherDetails = config.other_details1 ? JSON.parse(config.other_details1) : {};
|
|
726
726
|
|
|
727
|
-
const otherDetails = config.other_details1 ? JSON.parse(config.other_details1) : {};
|
|
728
727
|
// const [view, setView] = useState(isMobile ? true : false); //Need to check this condition
|
|
729
728
|
const cols = buildDisplayColumns({
|
|
730
729
|
columns,
|