ui-soxo-bootstrap-core 2.6.32-dev.6 → 2.6.32-dev.7
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/modules/steps/narration.js +192 -0
- package/core/modules/steps/progress-storage.js +140 -0
- package/core/modules/steps/steps.js +145 -254
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narration helpers for ProcessStepsPage.
|
|
3
|
+
*
|
|
4
|
+
* Owns the per-step "guest guide" copy plus the credential lookup, audio
|
|
5
|
+
* decoding, and seed-based content picking used by the TTS providers. The
|
|
6
|
+
* step component imports from here so that file can stay focused on
|
|
7
|
+
* component lifecycle and layout.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const STEP_WELCOME_LINES = [
|
|
11
|
+
'Welcome to your AI Automated Consultation process.',
|
|
12
|
+
'You are in the right place for a smooth and guided health journey.',
|
|
13
|
+
'This experience is designed to keep your consultation easy and stress-free.',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const STEP_INTRO_LINES = [
|
|
17
|
+
'In this process, we will walk you through a seamless and friendly AI interaction experience.',
|
|
18
|
+
'Each step is simple, guided, and focused on helping you feel prepared.',
|
|
19
|
+
'We will guide you through each stage so you always know what happens next.',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const STEP_EXPECTATION_LINES = [
|
|
23
|
+
'A care specialist may ask quick clarification questions to ensure your details are accurate.',
|
|
24
|
+
'This step focuses on collecting clear inputs so the care team can support you faster.',
|
|
25
|
+
'You can expect a guided workflow with minimal waiting and clear instructions.',
|
|
26
|
+
'The goal in this step is to keep your consultation organized and easy to follow.',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const STEP_COMFORT_LINES = [
|
|
30
|
+
'Take your time. There is no rush and assistance is always available nearby.',
|
|
31
|
+
'If anything feels unclear, the next prompt will guide you before you continue.',
|
|
32
|
+
'You can pause and review information before moving to the next stage.',
|
|
33
|
+
'Your responses here help personalize the rest of your consultation flow.',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const STEP_TIP_LINES = [
|
|
37
|
+
'Tip: Keep your previous reports or test details ready for quicker progress.',
|
|
38
|
+
'Tip: Follow on-screen prompts one at a time for the smoothest experience.',
|
|
39
|
+
'Tip: If you are unsure about a question, answer what you know and continue.',
|
|
40
|
+
'Tip: Stay relaxed; this process is built to be simple and patient-friendly.',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function getFromStorage(storageKey) {
|
|
44
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return window.localStorage.getItem(storageKey);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getElevenLabsApiKey() {
|
|
56
|
+
return (
|
|
57
|
+
process.env.ELEVEN_LABS_KEY ||
|
|
58
|
+
process.env.ELEVENLABS_API_KEY ||
|
|
59
|
+
process.env.REACT_APP_ELEVEN_LABS_KEY ||
|
|
60
|
+
process.env.REACT_APP_ELEVENLABS_API_KEY ||
|
|
61
|
+
getFromStorage('eleven_labs_key') ||
|
|
62
|
+
getFromStorage('elevenlabs_api_key') ||
|
|
63
|
+
getFromStorage('ELEVEN_LABS_KEY') ||
|
|
64
|
+
getFromStorage('REACT_APP_ELEVEN_LABS_KEY') ||
|
|
65
|
+
getFromStorage('REACT_APP_ELEVENLABS_API_KEY')
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getGeminiApiKey() {
|
|
70
|
+
return (
|
|
71
|
+
process.env.GEMINI_API_KEY ||
|
|
72
|
+
process.env.REACT_APP_GEMINI_API_KEY ||
|
|
73
|
+
getFromStorage('gemini_api_key') ||
|
|
74
|
+
getFromStorage('GEMINI_API_KEY') ||
|
|
75
|
+
getFromStorage('REACT_APP_GEMINI_API_KEY')
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getOpenAIApiKey() {
|
|
80
|
+
return (
|
|
81
|
+
process.env.OPEN_AI_KEY ||
|
|
82
|
+
process.env.OPENAI_API_KEY ||
|
|
83
|
+
process.env.REACT_APP_OPEN_AI_KEY ||
|
|
84
|
+
process.env.REACT_APP_OPENAI_API_KEY ||
|
|
85
|
+
getFromStorage('open_ai_key') ||
|
|
86
|
+
getFromStorage('openai_api_key') ||
|
|
87
|
+
getFromStorage('OPEN_AI_KEY') ||
|
|
88
|
+
getFromStorage('OPENAI_API_KEY') ||
|
|
89
|
+
getFromStorage('REACT_APP_OPEN_AI_KEY') ||
|
|
90
|
+
getFromStorage('REACT_APP_OPENAI_API_KEY')
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getSarvamApiKey() {
|
|
95
|
+
return (
|
|
96
|
+
process.env.SARVAM_API_KEY ||
|
|
97
|
+
process.env.REACT_APP_SARVAM_API_KEY ||
|
|
98
|
+
getFromStorage('sarvam_api_key') ||
|
|
99
|
+
getFromStorage('REACT_APP_SARVAM_API_KEY')
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function base64AudioToBlob(base64Audio = '', mimeType = 'audio/wav') {
|
|
104
|
+
const cleanedBase64 = base64Audio.includes(',') ? base64Audio.split(',').pop() : base64Audio;
|
|
105
|
+
const binaryString = typeof window !== 'undefined' && window.atob ? window.atob(cleanedBase64) : atob(cleanedBase64);
|
|
106
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
107
|
+
|
|
108
|
+
for (let index = 0; index < binaryString.length; index += 1) {
|
|
109
|
+
bytes[index] = binaryString.charCodeAt(index);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new Blob([bytes], { type: mimeType });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function extractGeminiAudio(payload) {
|
|
116
|
+
const candidates = payload && payload.candidates ? payload.candidates : [];
|
|
117
|
+
|
|
118
|
+
for (const candidate of candidates) {
|
|
119
|
+
const parts = candidate && candidate.content && candidate.content.parts ? candidate.content.parts : [];
|
|
120
|
+
|
|
121
|
+
for (const part of parts) {
|
|
122
|
+
const inlineData = part.inlineData || part.inline_data || part.audio;
|
|
123
|
+
|
|
124
|
+
if (inlineData && inlineData.data) {
|
|
125
|
+
return {
|
|
126
|
+
mimeType: inlineData.mimeType || inlineData.mime_type || 'audio/wav',
|
|
127
|
+
data: inlineData.data,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function hashText(value = '') {
|
|
137
|
+
let hash = 0;
|
|
138
|
+
|
|
139
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
140
|
+
hash = (hash << 5) - hash + value.charCodeAt(index);
|
|
141
|
+
hash |= 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Math.abs(hash);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function pickBySeed(items = [], seed = 0) {
|
|
148
|
+
if (!items.length) {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return items[seed % items.length];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildGuestStepGuide(step, index, total) {
|
|
156
|
+
if (!step) {
|
|
157
|
+
return {
|
|
158
|
+
welcome: STEP_WELCOME_LINES[0],
|
|
159
|
+
intro: STEP_INTRO_LINES[0],
|
|
160
|
+
expectation: 'We are preparing your consultation journey.',
|
|
161
|
+
comfort: STEP_COMFORT_LINES[0],
|
|
162
|
+
tip: STEP_TIP_LINES[0],
|
|
163
|
+
narration: '',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const stepName = step.step_name || `Step ${index + 1}`;
|
|
168
|
+
const seedSource = `${step.step_id || step.id || index}-${stepName}`;
|
|
169
|
+
const seed = hashText(seedSource);
|
|
170
|
+
|
|
171
|
+
const welcome = index === 0 ? STEP_WELCOME_LINES[0] : `Now entering ${stepName}.`;
|
|
172
|
+
const intro = index === 0 ? STEP_INTRO_LINES[0] : pickBySeed(STEP_INTRO_LINES, seed + 1);
|
|
173
|
+
const expectation = step.step_description || pickBySeed(STEP_EXPECTATION_LINES, seed + 2);
|
|
174
|
+
const comfort = pickBySeed(STEP_COMFORT_LINES, seed + 3);
|
|
175
|
+
const tip = pickBySeed(STEP_TIP_LINES, seed + 4);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
welcome,
|
|
179
|
+
intro,
|
|
180
|
+
expectation,
|
|
181
|
+
comfort,
|
|
182
|
+
tip,
|
|
183
|
+
narration: [
|
|
184
|
+
`Step ${index + 1} of ${total}. ${stepName}.`,
|
|
185
|
+
welcome,
|
|
186
|
+
intro,
|
|
187
|
+
`What to expect: ${expectation}`,
|
|
188
|
+
`Comfort note: ${comfort}`,
|
|
189
|
+
`Helpful tip: ${tip}`,
|
|
190
|
+
].join(' '),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-guest progress persistence helpers for ProcessStepsPage.
|
|
3
|
+
*
|
|
4
|
+
* Storage shape:
|
|
5
|
+
* key = `processTimings_<processId>_<guestRef>` or
|
|
6
|
+
* `processActiveStep_<processId>_<guestRef>`
|
|
7
|
+
* value = `{ savedAt: <ms>, value: { guestRef, ... } }`
|
|
8
|
+
*
|
|
9
|
+
* The key scopes by guest AND the inner value carries the guest reference,
|
|
10
|
+
* so reads can reject any entry that does not belong to the current guest —
|
|
11
|
+
* guest A's progress can never be applied to guest B even if a key collision
|
|
12
|
+
* somehow occurred.
|
|
13
|
+
*
|
|
14
|
+
* The TTL covers a working shift: a guest who walks away in the morning is
|
|
15
|
+
* not coming back the same evening to resume. The sweep on mount drops any
|
|
16
|
+
* entry past the TTL or stored in the legacy unwrapped shape.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const PROGRESS_STORAGE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
20
|
+
export const PROGRESS_STORAGE_PREFIXES = ['processTimings_', 'processActiveStep_'];
|
|
21
|
+
|
|
22
|
+
function hasLocalStorage() {
|
|
23
|
+
return typeof window !== 'undefined' && !!window.localStorage;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Drop progress keys that are past their TTL or stored in the legacy shape
|
|
28
|
+
* (which has no `savedAt` and therefore an unknowable age). Safe to call
|
|
29
|
+
* during mount and again after a quota error to free room for a retry.
|
|
30
|
+
*/
|
|
31
|
+
export function sweepStaleProgressKeys() {
|
|
32
|
+
if (!hasLocalStorage()) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const cutoff = Date.now() - PROGRESS_STORAGE_TTL_MS;
|
|
38
|
+
const toRemove = [];
|
|
39
|
+
|
|
40
|
+
for (let index = 0; index < window.localStorage.length; index += 1) {
|
|
41
|
+
const key = window.localStorage.key(index);
|
|
42
|
+
if (!key) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!PROGRESS_STORAGE_PREFIXES.some((prefix) => key.startsWith(prefix))) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const raw = window.localStorage.getItem(key);
|
|
51
|
+
if (!raw) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.savedAt !== 'number' || parsed.savedAt < cutoff) {
|
|
56
|
+
toRemove.push(key);
|
|
57
|
+
}
|
|
58
|
+
} catch (parseError) {
|
|
59
|
+
toRemove.push(key);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toRemove.forEach((key) => window.localStorage.removeItem(key));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn('Unable to sweep stale progress keys from local storage.', error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read a stored progress entry. Returns the unwrapped inner `value` only
|
|
71
|
+
* when the entry is present, well-formed, and within TTL; otherwise returns
|
|
72
|
+
* `null` and removes the bad key as a side effect.
|
|
73
|
+
*
|
|
74
|
+
* Callers must additionally verify `value.guestRef === currentGuestRef`
|
|
75
|
+
* before applying the result — the helper does not know the current guest.
|
|
76
|
+
*/
|
|
77
|
+
export function readProgressEntry(key) {
|
|
78
|
+
if (!hasLocalStorage()) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const raw = window.localStorage.getItem(key);
|
|
84
|
+
if (!raw) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const parsed = JSON.parse(raw);
|
|
88
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.savedAt !== 'number') {
|
|
89
|
+
window.localStorage.removeItem(key);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (Date.now() - parsed.savedAt > PROGRESS_STORAGE_TTL_MS) {
|
|
93
|
+
window.localStorage.removeItem(key);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return parsed.value;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Write a stored progress entry with the current timestamp. On
|
|
104
|
+
* `QuotaExceededError`, sweep stale keys once and retry — that keeps the
|
|
105
|
+
* write path resilient on long-running kiosks without surfacing the error
|
|
106
|
+
* to the user.
|
|
107
|
+
*/
|
|
108
|
+
export function writeProgressEntry(key, value) {
|
|
109
|
+
if (!hasLocalStorage()) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const payload = JSON.stringify({ savedAt: Date.now(), value });
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
window.localStorage.setItem(key, payload);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
sweepStaleProgressKeys();
|
|
119
|
+
try {
|
|
120
|
+
window.localStorage.setItem(key, payload);
|
|
121
|
+
} catch (retryError) {
|
|
122
|
+
console.warn('Unable to persist progress to local storage.', retryError);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Remove a progress entry. Safe to call when the entry does not exist.
|
|
129
|
+
*/
|
|
130
|
+
export function clearProgressEntry(key) {
|
|
131
|
+
if (!hasLocalStorage()) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
window.localStorage.removeItem(key);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.warn('Unable to clear progress entry from local storage.', error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -18,7 +18,17 @@ import { ExternalWindow } from '../../components';
|
|
|
18
18
|
import { Dashboard } from '../../models';
|
|
19
19
|
import * as genericComponents from './../../lib';
|
|
20
20
|
import { Button, Card, Location } from './../../lib';
|
|
21
|
+
import {
|
|
22
|
+
base64AudioToBlob,
|
|
23
|
+
buildGuestStepGuide,
|
|
24
|
+
extractGeminiAudio,
|
|
25
|
+
getElevenLabsApiKey,
|
|
26
|
+
getGeminiApiKey,
|
|
27
|
+
getOpenAIApiKey,
|
|
28
|
+
getSarvamApiKey,
|
|
29
|
+
} from './narration';
|
|
21
30
|
import { createOpenAIRealtimeSession, hasOpenAIRealtimeCredentials } from './openai-realtime';
|
|
31
|
+
import { clearProgressEntry, readProgressEntry, sweepStaleProgressKeys, writeProgressEntry } from './progress-storage';
|
|
22
32
|
import './steps.scss';
|
|
23
33
|
|
|
24
34
|
const TOUCH_NAV_HIDE_DELAY = 2800;
|
|
@@ -36,39 +46,6 @@ const FIRST_STEP_LABELS = Object.freeze({
|
|
|
36
46
|
consultation: 'Start Consultation',
|
|
37
47
|
});
|
|
38
48
|
|
|
39
|
-
const STEP_WELCOME_LINES = [
|
|
40
|
-
'Welcome to your AI Automated Consultation process.',
|
|
41
|
-
'You are in the right place for a smooth and guided health journey.',
|
|
42
|
-
'This experience is designed to keep your consultation easy and stress-free.',
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const STEP_INTRO_LINES = [
|
|
46
|
-
'In this process, we will walk you through a seamless and friendly AI interaction experience.',
|
|
47
|
-
'Each step is simple, guided, and focused on helping you feel prepared.',
|
|
48
|
-
'We will guide you through each stage so you always know what happens next.',
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
const STEP_EXPECTATION_LINES = [
|
|
52
|
-
'A care specialist may ask quick clarification questions to ensure your details are accurate.',
|
|
53
|
-
'This step focuses on collecting clear inputs so the care team can support you faster.',
|
|
54
|
-
'You can expect a guided workflow with minimal waiting and clear instructions.',
|
|
55
|
-
'The goal in this step is to keep your consultation organized and easy to follow.',
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
const STEP_COMFORT_LINES = [
|
|
59
|
-
'Take your time. There is no rush and assistance is always available nearby.',
|
|
60
|
-
'If anything feels unclear, the next prompt will guide you before you continue.',
|
|
61
|
-
'You can pause and review information before moving to the next stage.',
|
|
62
|
-
'Your responses here help personalize the rest of your consultation flow.',
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
const STEP_TIP_LINES = [
|
|
66
|
-
'Tip: Keep your previous reports or test details ready for quicker progress.',
|
|
67
|
-
'Tip: Follow on-screen prompts one at a time for the smoothest experience.',
|
|
68
|
-
'Tip: If you are unsure about a question, answer what you know and continue.',
|
|
69
|
-
'Tip: Stay relaxed; this process is built to be simple and patient-friendly.',
|
|
70
|
-
];
|
|
71
|
-
|
|
72
49
|
const VOICE_PROVIDER_OPTIONS = [
|
|
73
50
|
{ label: 'Gemini', value: 'gemini' },
|
|
74
51
|
{ label: 'ElevenLabs', value: 'elevenlabs' },
|
|
@@ -133,157 +110,6 @@ const SARVAM_TARGET_LANGUAGE_CODE = process.env.SARVAM_TARGET_LANGUAGE_CODE || p
|
|
|
133
110
|
const SARVAM_OUTPUT_AUDIO_CODEC = process.env.SARVAM_OUTPUT_AUDIO_CODEC || process.env.REACT_APP_SARVAM_OUTPUT_AUDIO_CODEC || 'wav';
|
|
134
111
|
const NARRATION_CONTROLS_ENABLED = false;
|
|
135
112
|
|
|
136
|
-
function getFromStorage(storageKey) {
|
|
137
|
-
if (typeof window === 'undefined' || !window.localStorage) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
return window.localStorage.getItem(storageKey);
|
|
143
|
-
} catch (error) {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function getElevenLabsApiKey() {
|
|
149
|
-
return (
|
|
150
|
-
process.env.ELEVEN_LABS_KEY ||
|
|
151
|
-
process.env.ELEVENLABS_API_KEY ||
|
|
152
|
-
process.env.REACT_APP_ELEVEN_LABS_KEY ||
|
|
153
|
-
process.env.REACT_APP_ELEVENLABS_API_KEY ||
|
|
154
|
-
getFromStorage('eleven_labs_key') ||
|
|
155
|
-
getFromStorage('elevenlabs_api_key') ||
|
|
156
|
-
getFromStorage('ELEVEN_LABS_KEY') ||
|
|
157
|
-
getFromStorage('REACT_APP_ELEVEN_LABS_KEY') ||
|
|
158
|
-
getFromStorage('REACT_APP_ELEVENLABS_API_KEY')
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function getGeminiApiKey() {
|
|
163
|
-
return (
|
|
164
|
-
process.env.GEMINI_API_KEY ||
|
|
165
|
-
process.env.REACT_APP_GEMINI_API_KEY ||
|
|
166
|
-
getFromStorage('gemini_api_key') ||
|
|
167
|
-
getFromStorage('GEMINI_API_KEY') ||
|
|
168
|
-
getFromStorage('REACT_APP_GEMINI_API_KEY')
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function getOpenAIApiKey() {
|
|
173
|
-
return (
|
|
174
|
-
process.env.OPEN_AI_KEY ||
|
|
175
|
-
process.env.OPENAI_API_KEY ||
|
|
176
|
-
process.env.REACT_APP_OPEN_AI_KEY ||
|
|
177
|
-
process.env.REACT_APP_OPENAI_API_KEY ||
|
|
178
|
-
getFromStorage('open_ai_key') ||
|
|
179
|
-
getFromStorage('openai_api_key') ||
|
|
180
|
-
getFromStorage('OPEN_AI_KEY') ||
|
|
181
|
-
getFromStorage('OPENAI_API_KEY') ||
|
|
182
|
-
getFromStorage('REACT_APP_OPEN_AI_KEY') ||
|
|
183
|
-
getFromStorage('REACT_APP_OPENAI_API_KEY')
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function getSarvamApiKey() {
|
|
188
|
-
return (
|
|
189
|
-
process.env.SARVAM_API_KEY ||
|
|
190
|
-
process.env.REACT_APP_SARVAM_API_KEY ||
|
|
191
|
-
getFromStorage('sarvam_api_key') ||
|
|
192
|
-
getFromStorage('REACT_APP_SARVAM_API_KEY')
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function base64AudioToBlob(base64Audio = '', mimeType = 'audio/wav') {
|
|
197
|
-
const cleanedBase64 = base64Audio.includes(',') ? base64Audio.split(',').pop() : base64Audio;
|
|
198
|
-
const binaryString = typeof window !== 'undefined' && window.atob ? window.atob(cleanedBase64) : atob(cleanedBase64);
|
|
199
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
200
|
-
|
|
201
|
-
for (let index = 0; index < binaryString.length; index += 1) {
|
|
202
|
-
bytes[index] = binaryString.charCodeAt(index);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return new Blob([bytes], { type: mimeType });
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function extractGeminiAudio(payload) {
|
|
209
|
-
const candidates = payload && payload.candidates ? payload.candidates : [];
|
|
210
|
-
|
|
211
|
-
for (const candidate of candidates) {
|
|
212
|
-
const parts = candidate && candidate.content && candidate.content.parts ? candidate.content.parts : [];
|
|
213
|
-
|
|
214
|
-
for (const part of parts) {
|
|
215
|
-
const inlineData = part.inlineData || part.inline_data || part.audio;
|
|
216
|
-
|
|
217
|
-
if (inlineData && inlineData.data) {
|
|
218
|
-
return {
|
|
219
|
-
mimeType: inlineData.mimeType || inlineData.mime_type || 'audio/wav',
|
|
220
|
-
data: inlineData.data,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function hashText(value = '') {
|
|
230
|
-
let hash = 0;
|
|
231
|
-
|
|
232
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
233
|
-
hash = (hash << 5) - hash + value.charCodeAt(index);
|
|
234
|
-
hash |= 0;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return Math.abs(hash);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function pickBySeed(items = [], seed = 0) {
|
|
241
|
-
if (!items.length) {
|
|
242
|
-
return '';
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return items[seed % items.length];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function buildGuestStepGuide(step, index, total) {
|
|
249
|
-
if (!step) {
|
|
250
|
-
return {
|
|
251
|
-
welcome: STEP_WELCOME_LINES[0],
|
|
252
|
-
intro: STEP_INTRO_LINES[0],
|
|
253
|
-
expectation: 'We are preparing your consultation journey.',
|
|
254
|
-
comfort: STEP_COMFORT_LINES[0],
|
|
255
|
-
tip: STEP_TIP_LINES[0],
|
|
256
|
-
narration: '',
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const stepName = step.step_name || `Step ${index + 1}`;
|
|
261
|
-
const seedSource = `${step.step_id || step.id || index}-${stepName}`;
|
|
262
|
-
const seed = hashText(seedSource);
|
|
263
|
-
|
|
264
|
-
const welcome = index === 0 ? STEP_WELCOME_LINES[0] : `Now entering ${stepName}.`;
|
|
265
|
-
const intro = index === 0 ? STEP_INTRO_LINES[0] : pickBySeed(STEP_INTRO_LINES, seed + 1);
|
|
266
|
-
const expectation = step.step_description || pickBySeed(STEP_EXPECTATION_LINES, seed + 2);
|
|
267
|
-
const comfort = pickBySeed(STEP_COMFORT_LINES, seed + 3);
|
|
268
|
-
const tip = pickBySeed(STEP_TIP_LINES, seed + 4);
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
welcome,
|
|
272
|
-
intro,
|
|
273
|
-
expectation,
|
|
274
|
-
comfort,
|
|
275
|
-
tip,
|
|
276
|
-
narration: [
|
|
277
|
-
`Step ${index + 1} of ${total}. ${stepName}.`,
|
|
278
|
-
welcome,
|
|
279
|
-
intro,
|
|
280
|
-
`What to expect: ${expectation}`,
|
|
281
|
-
`Comfort note: ${comfort}`,
|
|
282
|
-
`Helpful tip: ${tip}`,
|
|
283
|
-
].join(' '),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
113
|
export default function ProcessStepsPage({ match, CustomComponents = {}, ...props }) {
|
|
288
114
|
const allComponents = { ...genericComponents, ...CustomComponents };
|
|
289
115
|
const GuestInfoComponent = allComponents.EntryInfo;
|
|
@@ -296,6 +122,7 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
296
122
|
const [isStepCompleted, setIsStepCompleted] = useState(false);
|
|
297
123
|
|
|
298
124
|
const [nextProcessId, setNextProcessId] = useState(null);
|
|
125
|
+
const [previousProcessId, setPreviousProcessId] = useState(null);
|
|
299
126
|
const [stepStartTime, setStepStartTime] = useState(null);
|
|
300
127
|
const [processStartTime, setProcessStartTime] = useState(null);
|
|
301
128
|
const [processTimings, setProcessTimings] = useState([]);
|
|
@@ -335,64 +162,74 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
335
162
|
const isConsultationMode = String(urlParams?.consultation).toLowerCase() === 'true';
|
|
336
163
|
let processId = urlParams.processId;
|
|
337
164
|
const [currentProcessId, setCurrentProcessId] = useState(processId);
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Storage scope for per-guest progress.
|
|
168
|
+
* - The localStorage keys combine the process and the guest reference so
|
|
169
|
+
* guest A's resume marker cannot appear for guest B.
|
|
170
|
+
* - Sources tried in order: query params (`opb_id`, `reference_id`, `opno`,
|
|
171
|
+
* `reference_number`), then React Router `match.params`, then the final
|
|
172
|
+
* pathname segment. Only when none produce a non-empty value does the
|
|
173
|
+
* scope collapse to `anonymous` — which is the symptom of "stores step
|
|
174
|
+
* but not per guest" because all guests then share one key.
|
|
175
|
+
*/
|
|
176
|
+
const guestReference = (() => {
|
|
177
|
+
const fromQuery = urlParams?.opb_id || urlParams?.reference_id || urlParams?.opno || urlParams?.reference_number;
|
|
178
|
+
if (fromQuery) return String(fromQuery);
|
|
179
|
+
|
|
180
|
+
const params = match?.params || {};
|
|
181
|
+
const paramCandidates = [params.opb_id, params.reference_id, params.opno, params.reference_number, params.id];
|
|
182
|
+
const fromRoute = paramCandidates.find((value) => value != null && value !== '');
|
|
183
|
+
if (fromRoute) return String(fromRoute);
|
|
184
|
+
|
|
185
|
+
if (typeof window !== 'undefined' && window.location?.pathname) {
|
|
186
|
+
const segments = window.location.pathname.split('/').filter(Boolean);
|
|
187
|
+
const last = segments[segments.length - 1];
|
|
188
|
+
if (last) return String(last);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return 'anonymous';
|
|
192
|
+
})();
|
|
193
|
+
const timingsStorageKey = `processTimings_${currentProcessId}_${guestReference}`;
|
|
194
|
+
const activeStepStorageKey = `processActiveStep_${currentProcessId}_${guestReference}`;
|
|
195
|
+
|
|
338
196
|
// Load process details based on the current process ID
|
|
339
197
|
useEffect(() => {
|
|
340
|
-
|
|
198
|
+
sweepStaleProgressKeys();
|
|
341
199
|
|
|
342
|
-
|
|
343
|
-
try {
|
|
344
|
-
const saved = localStorage.getItem(`processTimings_${currentProcessId}`);
|
|
345
|
-
if (saved) {
|
|
346
|
-
const parsed = JSON.parse(saved);
|
|
347
|
-
if (Array.isArray(parsed)) {
|
|
348
|
-
savedTimings = parsed;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
} catch (error) {
|
|
352
|
-
console.warn('Unable to restore process timings from local storage.', error);
|
|
353
|
-
}
|
|
200
|
+
loadProcess(currentProcessId);
|
|
354
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Both the key AND the stored value are scoped to the guest. A read is
|
|
204
|
+
* only accepted when the value's `guestRef` matches the current guest;
|
|
205
|
+
* any mismatch (or a legacy unwrapped value from before this change) is
|
|
206
|
+
* treated as "no saved progress" so guest A's data can never appear for
|
|
207
|
+
* guest B even if a key collision somehow occurred.
|
|
208
|
+
*/
|
|
209
|
+
const savedTimingsEntry = readProgressEntry(timingsStorageKey);
|
|
210
|
+
const savedTimings =
|
|
211
|
+
savedTimingsEntry && savedTimingsEntry.guestRef === guestReference && Array.isArray(savedTimingsEntry.timings) ? savedTimingsEntry.timings : [];
|
|
355
212
|
setProcessTimings(savedTimings);
|
|
356
213
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const parsedStep = rawStep == null ? NaN : parseInt(rawStep, 10);
|
|
361
|
-
if (Number.isFinite(parsedStep) && parsedStep > 0) {
|
|
362
|
-
savedActiveStep = parsedStep;
|
|
363
|
-
}
|
|
364
|
-
} catch (error) {
|
|
365
|
-
console.warn('Unable to restore active step from local storage.', error);
|
|
366
|
-
}
|
|
214
|
+
const savedStepEntry = readProgressEntry(activeStepStorageKey);
|
|
215
|
+
const parsedStep = savedStepEntry && savedStepEntry.guestRef === guestReference ? Number(savedStepEntry.step) : NaN;
|
|
216
|
+
const savedActiveStep = Number.isFinite(parsedStep) && parsedStep > 0 ? parsedStep : 0;
|
|
367
217
|
setResumableStep(savedActiveStep > 0 ? savedActiveStep : null);
|
|
368
218
|
|
|
369
219
|
setProcessStartTime(Date.now());
|
|
370
220
|
setStepStartTime(Date.now());
|
|
371
221
|
setShowNextProcessAction(false);
|
|
372
|
-
|
|
222
|
+
setActiveStep(0);
|
|
223
|
+
}, [currentProcessId, guestReference]);
|
|
373
224
|
|
|
374
225
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
226
|
+
* The active step is persisted inline in `gotoStep` rather than via a
|
|
227
|
+
* useEffect — running it as an effect captured `activeStepStorageKey` from
|
|
228
|
+
* a closure that could go stale during URL changes, allowing one guest's
|
|
229
|
+
* progress to be written under another guest's key. Writing in `gotoStep`
|
|
230
|
+
* happens synchronously with the user action using the current render's
|
|
231
|
+
* key, so it can never target a different guest than the one interacting.
|
|
380
232
|
*/
|
|
381
|
-
useEffect(() => {
|
|
382
|
-
if (typeof window === 'undefined' || !window.localStorage || !currentProcessId) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (activeStep <= 0) {
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
localStorage.setItem(`processActiveStep_${currentProcessId}`, String(activeStep));
|
|
392
|
-
} catch (error) {
|
|
393
|
-
console.warn('Unable to persist active step to local storage.', error);
|
|
394
|
-
}
|
|
395
|
-
}, [activeStep, currentProcessId]);
|
|
396
233
|
|
|
397
234
|
/**
|
|
398
235
|
* Sync the loaded process name into the address bar.
|
|
@@ -409,24 +246,45 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
409
246
|
}
|
|
410
247
|
|
|
411
248
|
const params = new URLSearchParams(window.location.search);
|
|
412
|
-
|
|
249
|
+
let changed = false;
|
|
413
250
|
|
|
251
|
+
const trimmedName = typeof processName === 'string' ? processName.trim() : '';
|
|
414
252
|
if (trimmedName) {
|
|
415
|
-
if (params.get('process')
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
params.set('process', trimmedName);
|
|
419
|
-
} else {
|
|
420
|
-
if (!params.has('process')) {
|
|
421
|
-
return;
|
|
253
|
+
if (params.get('process') !== trimmedName) {
|
|
254
|
+
params.set('process', trimmedName);
|
|
255
|
+
changed = true;
|
|
422
256
|
}
|
|
257
|
+
} else if (params.has('process')) {
|
|
423
258
|
params.delete('process');
|
|
259
|
+
changed = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Mirror `currentProcessId` to the `processId` query param so previous /
|
|
264
|
+
* next process navigation reflects in the URL (e.g. when jumping from
|
|
265
|
+
* Verification → Consultation, the address bar should switch from
|
|
266
|
+
* `processId=1` to `processId=2`). Without this, deep-links and refreshes
|
|
267
|
+
* would still resolve to the originally loaded process.
|
|
268
|
+
*/
|
|
269
|
+
const processIdString = currentProcessId != null ? String(currentProcessId) : '';
|
|
270
|
+
if (processIdString) {
|
|
271
|
+
if (params.get('processId') !== processIdString) {
|
|
272
|
+
params.set('processId', processIdString);
|
|
273
|
+
changed = true;
|
|
274
|
+
}
|
|
275
|
+
} else if (params.has('processId')) {
|
|
276
|
+
params.delete('processId');
|
|
277
|
+
changed = true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!changed) {
|
|
281
|
+
return;
|
|
424
282
|
}
|
|
425
283
|
|
|
426
284
|
const search = params.toString();
|
|
427
285
|
const newUrl = `${window.location.pathname}${search ? `?${search}` : ''}${window.location.hash || ''}`;
|
|
428
286
|
window.history.replaceState(window.history.state, '', newUrl);
|
|
429
|
-
}, [processName]);
|
|
287
|
+
}, [processName, currentProcessId]);
|
|
430
288
|
|
|
431
289
|
//// Reset step start time whenever the active step changes
|
|
432
290
|
|
|
@@ -539,12 +397,7 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
539
397
|
const saveTimings = (updated) => {
|
|
540
398
|
const safeTimings = Array.isArray(updated) ? updated : [];
|
|
541
399
|
setProcessTimings(safeTimings);
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
localStorage.setItem(`processTimings_${currentProcessId}`, JSON.stringify(safeTimings));
|
|
545
|
-
} catch (error) {
|
|
546
|
-
console.warn('Unable to persist process timings to local storage.', error);
|
|
547
|
-
}
|
|
400
|
+
writeProgressEntry(timingsStorageKey, { guestRef: guestReference, timings: safeTimings });
|
|
548
401
|
};
|
|
549
402
|
// Record time spent on the current step
|
|
550
403
|
|
|
@@ -593,6 +446,7 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
593
446
|
async function loadProcess(processId) {
|
|
594
447
|
setLoading(true);
|
|
595
448
|
setNextProcessId(null);
|
|
449
|
+
setPreviousProcessId(null);
|
|
596
450
|
|
|
597
451
|
try {
|
|
598
452
|
const result = await Dashboard.loadProcess(processId);
|
|
@@ -600,6 +454,7 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
600
454
|
setSteps(result?.data?.steps || []);
|
|
601
455
|
setProcessName(result?.data?.process_name ?? null);
|
|
602
456
|
if (result?.data?.next_process_id) setNextProcessId(result.data);
|
|
457
|
+
if (result?.data?.previous_process_id) setPreviousProcessId(result.data);
|
|
603
458
|
} catch (e) {
|
|
604
459
|
console.error('Error loading process steps:', e);
|
|
605
460
|
} finally {
|
|
@@ -633,12 +488,8 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
633
488
|
const response = await Dashboard.processLog(payload);
|
|
634
489
|
|
|
635
490
|
if (response.success) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
localStorage.removeItem(`processActiveStep_${currentProcessId}`);
|
|
639
|
-
} catch (error) {
|
|
640
|
-
console.warn('Unable to clear process timings from local storage.', error);
|
|
641
|
-
}
|
|
491
|
+
clearProgressEntry(timingsStorageKey);
|
|
492
|
+
clearProgressEntry(activeStepStorageKey);
|
|
642
493
|
setProcessTimings([]);
|
|
643
494
|
setResumableStep(null);
|
|
644
495
|
return true;
|
|
@@ -674,6 +525,19 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
674
525
|
const updated = recordStepTime(status);
|
|
675
526
|
saveTimings(updated);
|
|
676
527
|
setActiveStep(nextIndex);
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Persist the resume marker synchronously here, not in a useEffect. The
|
|
531
|
+
* current render's `activeStepStorageKey` is guaranteed to belong to the
|
|
532
|
+
* guest the user is interacting with, so the write cannot leak to a
|
|
533
|
+
* different guest's key. Step 0 is the entry point — clear any marker
|
|
534
|
+
* so a returning visitor at step 0 does not see a stale banner.
|
|
535
|
+
*/
|
|
536
|
+
if (nextIndex > 0 && currentProcessId) {
|
|
537
|
+
writeProgressEntry(activeStepStorageKey, { guestRef: guestReference, step: nextIndex });
|
|
538
|
+
} else if (nextIndex === 0) {
|
|
539
|
+
clearProgressEntry(activeStepStorageKey);
|
|
540
|
+
}
|
|
677
541
|
};
|
|
678
542
|
/**
|
|
679
543
|
* Navigate to the next step
|
|
@@ -717,9 +581,7 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
717
581
|
const dismissResume = () => {
|
|
718
582
|
setResumableStep(null);
|
|
719
583
|
try {
|
|
720
|
-
|
|
721
|
-
localStorage.removeItem(`processActiveStep_${currentProcessId}`);
|
|
722
|
-
}
|
|
584
|
+
clearProgressEntry(activeStepStorageKey);
|
|
723
585
|
} catch (error) {
|
|
724
586
|
console.warn('Unable to clear resume marker from local storage.', error);
|
|
725
587
|
}
|
|
@@ -751,6 +613,23 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
751
613
|
setShowExternalWindow(true);
|
|
752
614
|
}
|
|
753
615
|
};
|
|
616
|
+
/**
|
|
617
|
+
* Go Back to Previous Process
|
|
618
|
+
* - Loads the previously linked process for the current guest.
|
|
619
|
+
* - Does NOT submit the current process; this is a "go back" navigation,
|
|
620
|
+
* not a completion. Step timings collected so far stay in localStorage
|
|
621
|
+
* under the current process's scoped key in case the user returns.
|
|
622
|
+
* - Updates `currentProcessId` which triggers the load effect to refresh
|
|
623
|
+
* process data, reset `activeStep` to 0, and re-derive storage scope.
|
|
624
|
+
*/
|
|
625
|
+
const handleStartPreviousProcess = async () => {
|
|
626
|
+
if (!previousProcessId?.previous_process_id) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
await loadProcess(previousProcessId.previous_process_id);
|
|
630
|
+
setCurrentProcessId(previousProcessId.previous_process_id);
|
|
631
|
+
setActiveStep(0);
|
|
632
|
+
};
|
|
754
633
|
|
|
755
634
|
function clearNarrationAudio() {
|
|
756
635
|
if (narrationAudioRef.current) {
|
|
@@ -1440,6 +1319,19 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1440
1319
|
{isStepFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
|
1441
1320
|
</Button>
|
|
1442
1321
|
|
|
1322
|
+
{/*
|
|
1323
|
+
Previous-process button.
|
|
1324
|
+
- Only relevant at the start of a process (`activeStep === 0`)
|
|
1325
|
+
AND when the backend signalled a `previous_process_id`. Mid-
|
|
1326
|
+
process the in-step Back button handles intra-process
|
|
1327
|
+
navigation, so showing this here would be ambiguous.
|
|
1328
|
+
*/}
|
|
1329
|
+
{activeStep === 0 && previousProcessId?.previous_process_id && (
|
|
1330
|
+
<Button type="default" icon={<ArrowLeftOutlined />} onClick={handleStartPreviousProcess}>
|
|
1331
|
+
{previousProcessId.previous_process_name ? `Back to ${previousProcessId.previous_process_name}` : 'Previous Process'}
|
|
1332
|
+
</Button>
|
|
1333
|
+
)}
|
|
1334
|
+
|
|
1443
1335
|
{activeStep > 0 && (
|
|
1444
1336
|
<Button type="default" icon={<ArrowLeftOutlined />} onClick={handlePrevious}>
|
|
1445
1337
|
Back
|
|
@@ -1483,8 +1375,7 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1483
1375
|
generic "Next" label. All non-first steps always render
|
|
1484
1376
|
"Next".
|
|
1485
1377
|
*/}
|
|
1486
|
-
{activeStep === 0 ? (FIRST_STEP_LABELS[processName?.trim().toLowerCase()] ?? 'Next') : 'Next'}
|
|
1487
|
-
<ArrowRightOutlined />
|
|
1378
|
+
{activeStep === 0 ? (FIRST_STEP_LABELS[processName?.trim().toLowerCase()] ?? 'Next') : 'Next'} <ArrowRightOutlined />
|
|
1488
1379
|
</Button>
|
|
1489
1380
|
)}
|
|
1490
1381
|
</div>
|