ui-soxo-bootstrap-core 2.6.26 → 2.6.28
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/lib/components/global-header/global-header.js +3 -4
- package/core/lib/components/sidemenu/sidemenu.scss +1 -1
- 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/modules/steps/action-buttons.js +57 -47
- 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 +1030 -89
- package/core/modules/steps/steps.scss +546 -285
- package/core/modules/steps/voice-navigation.js +709 -0
- package/package.json +1 -1
|
@@ -21,7 +21,6 @@ import { Button } from '../../elements';
|
|
|
21
21
|
|
|
22
22
|
import GenericHeader from '../header/generic-header';
|
|
23
23
|
|
|
24
|
-
import * as AntIcons from '@ant-design/icons';
|
|
25
24
|
|
|
26
25
|
import { CustomerServiceOutlined, MenuOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
|
|
27
26
|
|
|
@@ -263,16 +262,16 @@ function GlobalHeaderContent({ loading, appSettings, children, isConnected, hist
|
|
|
263
262
|
{/* Help-desk-btn */}
|
|
264
263
|
{helpDeskSetting?.showSupportBtn
|
|
265
264
|
? (() => {
|
|
266
|
-
const HelpIcon = AntIcons[helpDeskSetting?.icon] ?? CustomerServiceOutlined;
|
|
267
265
|
return (
|
|
268
266
|
<Tooltip title={helpDeskSetting?.toolTipText} overlayClassName="modern-tooltip">
|
|
269
267
|
<span>
|
|
270
268
|
<Button
|
|
271
269
|
onClick={() => window.open(helpDeskSetting?.helpDeskLink ?? '', '_blank')}
|
|
272
|
-
icon={<HelpIcon />}
|
|
273
270
|
type="default"
|
|
274
271
|
size="small"
|
|
275
|
-
|
|
272
|
+
>
|
|
273
|
+
{helpDeskSetting?.buttonText || 'Help'}
|
|
274
|
+
</Button>
|
|
276
275
|
</span>
|
|
277
276
|
</Tooltip>
|
|
278
277
|
);
|
|
@@ -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
|
+
}
|
|
@@ -10,7 +10,7 @@ import './action-buttons.scss';
|
|
|
10
10
|
|
|
11
11
|
export default function ActionButtons({
|
|
12
12
|
loading,
|
|
13
|
-
steps,
|
|
13
|
+
steps = [],
|
|
14
14
|
activeStep,
|
|
15
15
|
isStepCompleted,
|
|
16
16
|
isFullscreen,
|
|
@@ -22,60 +22,70 @@ export default function ActionButtons({
|
|
|
22
22
|
handleFinish,
|
|
23
23
|
handleStartNextProcess,
|
|
24
24
|
nextProcessId,
|
|
25
|
-
timelineCollapsed,
|
|
26
25
|
}) {
|
|
27
26
|
const [showNextProcess, setShowNextProcess] = useState(false);
|
|
27
|
+
const currentStep = steps[activeStep];
|
|
28
|
+
const isLastStep = steps.length > 0 && activeStep >= steps.length - 1;
|
|
29
|
+
const isEndStep = currentStep?.order_seqtype === 'E';
|
|
30
|
+
|
|
28
31
|
useEffect(() => {
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
if (!isEndStep) {
|
|
33
|
+
setShowNextProcess(false);
|
|
34
|
+
}
|
|
35
|
+
}, [isEndStep]);
|
|
36
|
+
|
|
31
37
|
return (
|
|
32
|
-
<div className="action-buttons-
|
|
33
|
-
<div className="action-
|
|
34
|
-
<div className="action-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
<div className="action-buttons-shell">
|
|
39
|
+
<div className="action-body">{loading ? <Skeleton active /> : typeof renderDynamicComponent === 'function' ? renderDynamicComponent() : null}</div>
|
|
40
|
+
<div className="action-footer">
|
|
41
|
+
<div className="action-buttons-container">
|
|
42
|
+
{/* Back button */}
|
|
43
|
+
<Button disabled={activeStep <= 0} onClick={handlePrevious}>
|
|
44
|
+
Back
|
|
45
|
+
</Button>
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
{typeof onToggleFullscreen === 'function' && (
|
|
48
|
+
<Button type="default" onClick={onToggleFullscreen}>
|
|
49
|
+
{isFullscreen ? 'Exit Full Screen' : 'Switch to Full Screen'}
|
|
50
|
+
</Button>
|
|
51
|
+
)}
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
{/* Skip button */}
|
|
54
|
+
{steps.length > 0 && currentStep?.allow_skip === 'Y' && (
|
|
55
|
+
<Button type="default" onClick={handleSkip} disabled={isLastStep}>
|
|
56
|
+
Skip
|
|
57
|
+
</Button>
|
|
58
|
+
)}
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
{/* Next / Finish / Start Next */}
|
|
61
|
+
{isEndStep ? (
|
|
62
|
+
<>
|
|
63
|
+
{!showNextProcess && (
|
|
64
|
+
<Button
|
|
65
|
+
type="primary"
|
|
66
|
+
onClick={async () => {
|
|
67
|
+
const success = typeof handleFinish === 'function' ? await handleFinish() : false;
|
|
68
|
+
if (success && nextProcessId?.next_process_id) {
|
|
69
|
+
setShowNextProcess(true);
|
|
70
|
+
}
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
Finish
|
|
74
|
+
</Button>
|
|
75
|
+
)}
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
{showNextProcess && nextProcessId?.next_process_id && (
|
|
78
|
+
<Button type="primary" onClick={handleStartNextProcess}>
|
|
79
|
+
Start {nextProcessId.next_process_name}
|
|
80
|
+
</Button>
|
|
81
|
+
)}
|
|
82
|
+
</>
|
|
83
|
+
) : (
|
|
84
|
+
<Button type="primary" disabled={steps.length === 0 || isLastStep || !isStepCompleted} onClick={handleNext}>
|
|
85
|
+
Next →
|
|
86
|
+
</Button>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
79
89
|
</div>
|
|
80
90
|
</div>
|
|
81
91
|
);
|