ui-soxo-bootstrap-core 2.6.1-dev.17 → 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.
@@ -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
+ }
@@ -42,6 +42,10 @@
42
42
  }
43
43
 
44
44
  .actions {
45
+ display: flex;
46
+ gap: 6px;
47
+ align-items: center;
48
+ flex-wrap: wrap;
45
49
  }
46
50
  }
47
51
 
@@ -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, checkExpiryStatus, safeJSON } from '../../utils/common/common.utils';
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 = (user) => {
129
- const otherDetails = user?.other_details ? JSON.parse(user.other_details) : null;
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
- if (passwordStatus.message) {
157
- message.warning(passwordStatus.message);
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(user);
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.user);
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`.
@@ -305,6 +305,12 @@ class MenusAPI extends Base {
305
305
  // }
306
306
  ];
307
307
  };
308
+ // license summary api call
309
+ getSummary = () => {
310
+ return ApiUtils.get({
311
+ url: 'license/summary',
312
+ });
313
+ };
308
314
  }
309
315
 
310
316
  export default MenusAPI;