ui-soxo-bootstrap-core 2.6.37 → 2.6.40-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -160,7 +160,7 @@ export default function AssignRole() {
160
160
  const role_id = role.id;
161
161
  try {
162
162
  const res = await MenusAPI.getCoreMenuByRoleId(role_id);
163
- const allMenus = res.result || [];
163
+ const allMenus = Array.isArray(res?.result) ? res.result : [];
164
164
 
165
165
  setModules(allMenus);
166
166
  } catch (e) {
@@ -264,11 +264,12 @@ export default function AssignRole() {
264
264
  const handleViewAll = async () => {
265
265
  setLoadingMenus(true);
266
266
  setModules([]);
267
- setActiveRole({ name: 'All Roles' });
267
+ setActiveRole({ name: 'All Roles', isViewAll: true });
268
268
 
269
269
  try {
270
270
  const res = await MenusAPI.getMenubyUser(id);
271
- setModules(res?.result?.menus ?? []);
271
+ const userMenus = Array.isArray(res?.result?.menus) ? res.result.menus : [];
272
+ setModules(userMenus);
272
273
  setLoadingMenus(false);
273
274
  } catch (err) {
274
275
  setLoadingMenus(false);
@@ -277,6 +278,18 @@ export default function AssignRole() {
277
278
  }
278
279
  };
279
280
 
281
+ /**
282
+ * Context-specific empty-state message for the right (menus) panel:
283
+ * - View Access for a user with no roles → no role assigned
284
+ * - A selected role with no menus → no menus assigned
285
+ * - Nothing selected yet → prompt to pick a role
286
+ */
287
+ const emptyMenusMessage = !activeRole
288
+ ? 'Select a role to view menus'
289
+ : activeRole.isViewAll
290
+ ? 'No role has been assigned to this user.'
291
+ : 'No menus have been assigned to this role.';
292
+
280
293
  return (
281
294
  <section className="assign-role">
282
295
  {/* LEFT PANEL */}
@@ -383,17 +396,19 @@ export default function AssignRole() {
383
396
  padding: 16,
384
397
  }}
385
398
  >
386
- <div className="menus-header">
387
- <div className="title">Menus {activeRole ? `– ${activeRole.name}` : ''}</div>
388
- <div className="sub-text">You don’t have permission to edit this here. Update it in Role Settings.</div>
389
- </div>
399
+ {modules.length > 0 && (
400
+ <div className="menus-header">
401
+ <div className="title">Menus {activeRole ? `– ${activeRole.name}` : ''}</div>
402
+ <div className="sub-text">You don’t have permission to edit this here. Update it in Role Settings.</div>
403
+ </div>
404
+ )}
390
405
 
391
406
  <div className="menus-content">
392
407
  {loadingMenus ? (
393
408
  <Skeleton active paragraph={{ rows: 6 }} />
394
409
  ) : modules.length === 0 ? (
395
410
  <div className="empty-state">
396
- <Empty description="Select a role to view menus" />
411
+ <Empty description={emptyMenusMessage} />
397
412
  </div>
398
413
  ) : (
399
414
  <MenuTree menus={modules} selectedMenus={selectedMenus} toggleMenu={toggleMenu} showCheckbox={false} />
@@ -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
+ }