@memori.ai/memori-react 8.38.8 → 8.39.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.
@@ -381,7 +381,7 @@ export interface LayoutProps {
381
381
 
382
382
  export interface Props {
383
383
  memori: Memori;
384
- ownerUserName?: string | null;
384
+ // ownerUserName?: string | null;
385
385
  ownerUserID?: string | null;
386
386
  tenantID: string;
387
387
  memoriConfigs?: MemoriConfig[];
@@ -453,7 +453,7 @@ const MemoriWidget = ({
453
453
  memori,
454
454
  memoriConfigs,
455
455
  ownerUserID,
456
- ownerUserName,
456
+ // ownerUserName,
457
457
  tenantID,
458
458
  memoriLang,
459
459
  uiLang,
@@ -565,7 +565,6 @@ const MemoriWidget = ({
565
565
  const [showLoginDrawer, setShowLoginDrawer] = useState(false);
566
566
 
567
567
  const [clickedStart, setClickedStart] = useState(false);
568
- const [gotErrorInOpening, setGotErrorInOpening] = useState(false);
569
568
 
570
569
  const language =
571
570
  memori.culture?.split('-')?.[0]?.toUpperCase()! ||
@@ -707,7 +706,9 @@ const MemoriWidget = ({
707
706
  setRuntimeShowMessageConsumption(
708
707
  getLocalConfig(
709
708
  'showMessageConsumption',
710
- showMessageConsumption ?? integrationConfig?.showMessageConsumption ?? false
709
+ showMessageConsumption ??
710
+ integrationConfig?.showMessageConsumption ??
711
+ false
711
712
  )
712
713
  );
713
714
 
@@ -770,17 +771,11 @@ const MemoriWidget = ({
770
771
  longitude?: number;
771
772
  uncertaintyKm?: number;
772
773
  } = {};
773
- if (
774
- venue.latitude != null &&
775
- venue.longitude != null
776
- ) {
774
+ if (venue.latitude != null && venue.longitude != null) {
777
775
  place.latitude = venue.latitude;
778
776
  place.longitude = venue.longitude;
779
777
  if (venue.placeName) place.placeName = venue.placeName;
780
- if (
781
- venue.uncertainty != null &&
782
- venue.uncertainty > 0
783
- )
778
+ if (venue.uncertainty != null && venue.uncertainty > 0)
784
779
  place.uncertaintyKm = venue.uncertainty;
785
780
  } else if (venue.placeName) {
786
781
  place.placeName = venue.placeName;
@@ -1393,6 +1388,10 @@ const MemoriWidget = ({
1393
1388
  return;
1394
1389
  }
1395
1390
 
1391
+ if (!(await checkCredits({ notify: true, goBack: true }))) {
1392
+ return;
1393
+ }
1394
+
1396
1395
  setLoading(true);
1397
1396
 
1398
1397
  try {
@@ -1469,10 +1468,9 @@ const MemoriWidget = ({
1469
1468
  ) {
1470
1469
  console.warn(session);
1471
1470
  toast.error(t('underageTwinSession', { age: minAge }));
1472
- setGotErrorInOpening(true);
1473
1471
  }
1474
1472
  // Handle authentication error
1475
- else if (session?.resultCode === 403) {
1473
+ else if (session?.resultCode === 403 && memori.privacyType !== 'PUBLIC') {
1476
1474
  setMemoriPwd(undefined);
1477
1475
  setAuthModalState('password');
1478
1476
  return session;
@@ -1498,7 +1496,6 @@ const MemoriWidget = ({
1498
1496
  duration: Infinity,
1499
1497
  }
1500
1498
  );
1501
- setGotErrorInOpening(true);
1502
1499
  return session;
1503
1500
  }
1504
1501
  } catch (err) {
@@ -1565,6 +1562,11 @@ const MemoriWidget = ({
1565
1562
  return;
1566
1563
  }
1567
1564
 
1565
+ if (!(await checkCredits({ notify: true, goBack: true }))) {
1566
+ setLoading(false);
1567
+ return null;
1568
+ }
1569
+
1568
1570
  // Get current URL as referral
1569
1571
  let referral;
1570
1572
  try {
@@ -1692,10 +1694,12 @@ const MemoriWidget = ({
1692
1694
  ) {
1693
1695
  console.error('[REOPEN_SESSION] Age restriction error:', response);
1694
1696
  toast.error(t('underageTwinSession', { age: minAge }));
1695
- setGotErrorInOpening(true);
1696
1697
  }
1697
1698
  // Handle authentication error
1698
- else if (response?.resultCode === 403) {
1699
+ else if (
1700
+ response?.resultCode === 403 &&
1701
+ memori.privacyType !== 'PUBLIC'
1702
+ ) {
1699
1703
  console.error('[REOPEN_SESSION] Authentication error');
1700
1704
  setMemoriPwd(undefined);
1701
1705
  setAuthModalState('password');
@@ -1704,7 +1708,6 @@ const MemoriWidget = ({
1704
1708
  else {
1705
1709
  console.error('[REOPEN_SESSION] Other error:', response);
1706
1710
  toast.error(t(getErrori18nKey(response.resultCode)));
1707
- setGotErrorInOpening(true);
1708
1711
  }
1709
1712
  } catch (err) {
1710
1713
  console.error('[REOPEN_SESSION] Caught error:', err);
@@ -2370,6 +2373,12 @@ const MemoriWidget = ({
2370
2373
  return;
2371
2374
  }
2372
2375
 
2376
+ if (!(await checkCredits({ notify: true, goBack: true }))) {
2377
+ setClickedStart(false);
2378
+ setLoading(false);
2379
+ return;
2380
+ }
2381
+
2373
2382
  // Handle age verification
2374
2383
  if (!sessionID && !!minAge && !birth) {
2375
2384
  setShowAgeVerification(true);
@@ -2377,12 +2386,11 @@ const MemoriWidget = ({
2377
2386
  }
2378
2387
  // Handle authentication
2379
2388
  else if (
2380
- (!sessionID &&
2381
- memori.privacyType !== 'PUBLIC' &&
2382
- !memori.secretToken &&
2383
- !memoriPwd &&
2384
- !memoriTokens) ||
2385
- (!sessionID && gotErrorInOpening)
2389
+ !sessionID &&
2390
+ memori.privacyType !== 'PUBLIC' &&
2391
+ !memori.secretToken &&
2392
+ !memoriPwd &&
2393
+ !memoriTokens
2386
2394
  ) {
2387
2395
  setAuthModalState('password');
2388
2396
  setClickedStart(false);
@@ -2391,7 +2399,6 @@ const MemoriWidget = ({
2391
2399
  // Create new session if needed
2392
2400
  else if (!sessionID || initialSessionExpired) {
2393
2401
  setClickedStart(false);
2394
- setGotErrorInOpening(false);
2395
2402
  const session = await fetchSession({
2396
2403
  memoriID: memori.engineMemoriID!,
2397
2404
  password: secret || memoriPwd || memori.secretToken,
@@ -2513,7 +2520,6 @@ const MemoriWidget = ({
2513
2520
 
2514
2521
  if (response.resultCode !== 0 || !currentState) {
2515
2522
  const { chatLogs } = await getSessionChatLogs(sessionID!, sessionID!);
2516
- setGotErrorInOpening(true);
2517
2523
  setSessionId(undefined);
2518
2524
  setClickedStart(false);
2519
2525
  await onClickStart(undefined, true, chatLogs?.[0]);
@@ -2722,7 +2728,6 @@ const MemoriWidget = ({
2722
2728
  );
2723
2729
  }
2724
2730
  }
2725
-
2726
2731
  }
2727
2732
  // Default case - just translate and activate
2728
2733
  else {
@@ -2817,36 +2822,77 @@ const MemoriWidget = ({
2817
2822
  // check if owner has enough credits
2818
2823
  const needsCredits = tenant?.billingDelegation;
2819
2824
  const [hasEnoughCredits, setHasEnoughCredits] = useState<boolean>(true);
2820
- const checkCredits = useCallback(async () => {
2821
- if (!tenant?.billingDelegation) return;
2825
+ const handleNotEnoughCredits = useCallback(
2826
+ (goBack = false) => {
2827
+ setHasEnoughCredits(false);
2828
+ setAuthModalState(null);
2829
+ toast.error(t('notEnoughCredits'));
2830
+
2831
+ if (goBack && window.history.length > 1) {
2832
+ window.history.back();
2833
+ }
2834
+ },
2835
+ [t]
2836
+ );
2837
+ const checkCredits = useCallback(
2838
+ async (options?: { notify?: boolean; goBack?: boolean }) => {
2839
+ if (!tenant?.billingDelegation) return true;
2840
+
2841
+ // Billing delegation is active: credits MUST be verified.
2842
+ // Without an ownerUserID we cannot call the API, so we fail closed
2843
+ // instead of silently letting the session start unverified.
2844
+ if (!ownerUserID) {
2845
+ console.warn('Cannot verify credits: missing ownerUserID');
2846
+ if (options?.notify) {
2847
+ handleNotEnoughCredits(!!options.goBack);
2848
+ } else {
2849
+ setHasEnoughCredits(false);
2850
+ }
2851
+ return false;
2852
+ }
2822
2853
 
2823
- try {
2824
- const resp = await getCredits({
2825
- operation: deepThoughtEnabled
2826
- ? 'dt_session_creation'
2827
- : 'session_creation',
2828
- baseUrl: baseUrl,
2829
- userID: ownerUserID,
2830
- userName: ownerUserName,
2831
- tenant: tenantID,
2832
- });
2854
+ try {
2855
+ const resp = await getCredits({
2856
+ operation: deepThoughtEnabled
2857
+ ? 'dt_session_creation'
2858
+ : 'session_creation',
2859
+ baseUrl: baseUrl,
2860
+ userID: ownerUserID,
2861
+ tenant: tenantID,
2862
+ });
2833
2863
 
2834
- if (resp.enough) {
2835
- setHasEnoughCredits(true);
2836
- } else {
2837
- setHasEnoughCredits(false);
2838
- console.warn('Not enough credits. Required:', resp.required);
2864
+ if (resp.enough) {
2865
+ setHasEnoughCredits(true);
2866
+ return true;
2867
+ } else {
2868
+ console.warn('Not enough credits. Required:', resp.required);
2869
+ if (options?.notify) {
2870
+ handleNotEnoughCredits(!!options.goBack);
2871
+ } else {
2872
+ setHasEnoughCredits(false);
2873
+ }
2874
+ return false;
2875
+ }
2876
+ } catch (e) {
2877
+ let err = e as Error;
2878
+ console.debug(err);
2879
+ return true;
2839
2880
  }
2840
- } catch (e) {
2841
- let err = e as Error;
2842
- console.debug(err);
2843
- }
2844
- }, [tenant?.billingDelegation, deepThoughtEnabled]);
2881
+ },
2882
+ [
2883
+ baseUrl,
2884
+ deepThoughtEnabled,
2885
+ handleNotEnoughCredits,
2886
+ ownerUserID,
2887
+ tenant?.billingDelegation,
2888
+ tenantID,
2889
+ ]
2890
+ );
2845
2891
  useEffect(() => {
2846
2892
  if (tenant?.billingDelegation) {
2847
2893
  checkCredits();
2848
2894
  }
2849
- }, [tenant?.billingDelegation, deepThoughtEnabled]);
2895
+ }, [tenant?.billingDelegation, deepThoughtEnabled, checkCredits]);
2850
2896
 
2851
2897
  useEffect(() => {
2852
2898
  if (__WEBCOMPONENT__) return;
@@ -3203,12 +3249,6 @@ const MemoriWidget = ({
3203
3249
  }
3204
3250
  })
3205
3251
  .catch(error => {
3206
- if (
3207
- !(error instanceof Error) ||
3208
- error.message !== 'AUTH_FAILED'
3209
- ) {
3210
- setGotErrorInOpening(true);
3211
- }
3212
3252
  throw error;
3213
3253
  });
3214
3254
  }}
@@ -3254,7 +3294,6 @@ const MemoriWidget = ({
3254
3294
  })
3255
3295
  .catch(() => {
3256
3296
  setShowAgeVerification(false);
3257
- setGotErrorInOpening(true);
3258
3297
  });
3259
3298
  } else {
3260
3299
  setShowAgeVerification(false);
@@ -381,3 +381,24 @@ WithCompletionProviderDown.args = {
381
381
  onClickStart: () => {},
382
382
  _TEST_forceProviderStatus: 'major',
383
383
  };
384
+
385
+ /** Public agent whose owner has run out of credits: the start button is
386
+ * disabled and a "not enough credits" badge is shown instead of starting a
387
+ * session or asking for a password. */
388
+ export const NotEnoughCredits = Template.bind({});
389
+ NotEnoughCredits.args = {
390
+ memori: {
391
+ ...memori,
392
+ privacyType: 'PUBLIC',
393
+ },
394
+ tenant,
395
+ language: 'it',
396
+ userLang: 'en',
397
+ setUserLang: () => {},
398
+ openPositionDrawer: () => {},
399
+ instruct: false,
400
+ sessionId: sessionID,
401
+ clickedStart: false,
402
+ onClickStart: () => {},
403
+ notEnoughCredits: true,
404
+ };
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { render } from '@testing-library/react';
2
+ import { render, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
3
4
  import { memori, tenant, sessionID, integration, user } from '../../mocks/data';
4
5
  import StartPanel from './StartPanel';
5
6
 
@@ -293,3 +294,67 @@ it('renders StartPanel with completion provider down unchanged', () => {
293
294
  );
294
295
  expect(container).toMatchSnapshot();
295
296
  });
297
+
298
+ it('renders StartPanel with not enough credits unchanged', () => {
299
+ const { container } = render(
300
+ <StartPanel
301
+ memori={memori}
302
+ tenant={tenant}
303
+ language="it"
304
+ userLang="en"
305
+ setUserLang={() => {}}
306
+ openPositionDrawer={() => {}}
307
+ instruct={false}
308
+ clickedStart={false}
309
+ onClickStart={() => {}}
310
+ setShowLoginDrawer={jest.fn()}
311
+ notEnoughCredits
312
+ />
313
+ );
314
+ expect(container).toMatchSnapshot();
315
+ });
316
+
317
+ // When the agent owner has not enough credits, opening the chat for a PUBLIC
318
+ // agent must not start a session nor ask for a password: the start button is
319
+ // disabled and a "not enough credits" badge is shown instead.
320
+ it('blocks start and shows credits badge when owner has not enough credits', () => {
321
+ const onClickStart = jest.fn();
322
+ const { container, getByText, queryByPlaceholderText } = render(
323
+ <StartPanel
324
+ memori={{ ...memori, privacyType: 'PUBLIC' }}
325
+ tenant={tenant}
326
+ language="it"
327
+ userLang="en"
328
+ setUserLang={() => {}}
329
+ openPositionDrawer={() => {}}
330
+ instruct={false}
331
+ clickedStart={false}
332
+ onClickStart={onClickStart}
333
+ setShowLoginDrawer={jest.fn()}
334
+ notEnoughCredits
335
+ />
336
+ );
337
+
338
+ const startButton = container.querySelector(
339
+ '.memori--start-button'
340
+ ) as HTMLButtonElement;
341
+ expect(startButton).toBeInTheDocument();
342
+ expect(startButton).toBeDisabled();
343
+
344
+ // Clicking the disabled button must not attempt to open a session.
345
+ fireEvent.click(startButton);
346
+ expect(onClickStart).not.toHaveBeenCalled();
347
+
348
+ // No password field is ever rendered for a public agent.
349
+ expect(queryByPlaceholderText('Password')).not.toBeInTheDocument();
350
+
351
+ // The credits badge is rendered and surfaces the proper message on hover.
352
+ const badge = container.querySelector('.blocked-memori-badge--wrapper');
353
+ expect(badge).toBeInTheDocument();
354
+
355
+ const tooltipTrigger = container.querySelector(
356
+ '.blocked-memori-badge--tooltip'
357
+ ) as HTMLElement;
358
+ fireEvent.mouseEnter(tooltipTrigger);
359
+ expect(getByText('notEnoughCredits')).toBeInTheDocument();
360
+ });
@@ -1773,6 +1773,162 @@ exports[`renders StartPanel with multilangual unchanged 1`] = `
1773
1773
  </div>
1774
1774
  `;
1775
1775
 
1776
+ exports[`renders StartPanel with not enough credits unchanged 1`] = `
1777
+ <div>
1778
+ <div
1779
+ class="memori--start-panel"
1780
+ >
1781
+ <div
1782
+ class="memori--cover"
1783
+ />
1784
+ <picture
1785
+ class="memori--avatar"
1786
+ >
1787
+ <source
1788
+ src="https://aisuru.com/images/aisuru/square_logo.png"
1789
+ />
1790
+ <img
1791
+ alt="Memori"
1792
+ src="https://aisuru.com/images/aisuru/square_logo.png"
1793
+ />
1794
+ </picture>
1795
+ <h2
1796
+ class="memori--title"
1797
+ >
1798
+ Memori
1799
+ </h2>
1800
+ <div
1801
+ class="memori--description"
1802
+ >
1803
+ <p>
1804
+ <div
1805
+ class="memori-expandable memori--description-text"
1806
+ >
1807
+ <div
1808
+ class="memori-expandable--inner"
1809
+ style="max-height: 9999px;"
1810
+ >
1811
+ Lorem ipsum.
1812
+ </div>
1813
+ </div>
1814
+ </p>
1815
+ <div
1816
+ class="memori--start-privacy-explanation-container"
1817
+ >
1818
+ <p
1819
+ class="memori--start-privacy-explanation"
1820
+ >
1821
+ write_and_speak.pagePrivacyExplanation
1822
+ </p>
1823
+ <div
1824
+ class="memori-tooltip memori-tooltip--align-topLeft"
1825
+ >
1826
+ <div
1827
+ class="memori-tooltip--trigger"
1828
+ >
1829
+ <svg
1830
+ aria-hidden="true"
1831
+ class="memori--start-privacy-explanation-icon"
1832
+ fill="none"
1833
+ focusable="false"
1834
+ role="img"
1835
+ stroke="currentColor"
1836
+ stroke-linecap="round"
1837
+ stroke-linejoin="round"
1838
+ stroke-width="1.5"
1839
+ viewBox="0 0 24 24"
1840
+ xmlns="http://www.w3.org/2000/svg"
1841
+ >
1842
+ <circle
1843
+ cx="12"
1844
+ cy="12"
1845
+ r="10"
1846
+ />
1847
+ <path
1848
+ d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
1849
+ />
1850
+ <path
1851
+ d="M12 17L12.01 17"
1852
+ />
1853
+ </svg>
1854
+ </div>
1855
+ </div>
1856
+ </div>
1857
+ <button
1858
+ class="memori-button memori-button--primary memori-button--rounded memori-button--padded memori--start-button"
1859
+ disabled=""
1860
+ >
1861
+ write_and_speak.tryMeButton
1862
+ </button>
1863
+ <div
1864
+ class="memori--completion-provider-status--loading"
1865
+ >
1866
+ <div
1867
+ class="memori-spin memori-spin--spinning"
1868
+ >
1869
+ <div
1870
+ class="memori-spin--spinner"
1871
+ >
1872
+ <svg
1873
+ aria-hidden="true"
1874
+ class="memori-loading-icon"
1875
+ focusable="false"
1876
+ role="img"
1877
+ viewBox="0 0 1024 1024"
1878
+ xmlns="http://www.w3.org/2000/svg"
1879
+ >
1880
+ <path
1881
+ d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
1882
+ />
1883
+ </svg>
1884
+ </div>
1885
+ </div>
1886
+ </div>
1887
+ <p
1888
+ class="memori--start-description"
1889
+ >
1890
+ write_and_speak.pageTryMeExplanation
1891
+ </p>
1892
+ <div
1893
+ class="memori-tooltip memori-tooltip--align-right blocked-memori-badge--tooltip"
1894
+ >
1895
+ <div
1896
+ class="memori-tooltip--trigger"
1897
+ >
1898
+ <div
1899
+ class="blocked-memori-badge--wrapper"
1900
+ >
1901
+ <div
1902
+ class="blocked-memori-badge margin-left"
1903
+ >
1904
+ <svg
1905
+ aria-hidden="true"
1906
+ class="blocked-memori-badge--icon"
1907
+ color="currentColor"
1908
+ focusable="false"
1909
+ role="img"
1910
+ viewBox="0 0 1024 1024"
1911
+ xmlns="http://www.w3.org/2000/svg"
1912
+ >
1913
+ <path
1914
+ d="M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"
1915
+ fill="currentColor"
1916
+ />
1917
+ </svg>
1918
+ </div>
1919
+ <span
1920
+ class="blocked-memori-badge--title"
1921
+ >
1922
+ memoriBlockedTitle
1923
+ </span>
1924
+ </div>
1925
+ </div>
1926
+ </div>
1927
+ </div>
1928
+ </div>
1929
+ </div>
1930
+ `;
1931
+
1776
1932
  exports[`renders StartPanel with position required unchanged 1`] = `
1777
1933
  <div>
1778
1934
  <div
@@ -1,23 +1,35 @@
1
1
  // POST http://localhost:3000/api/verify-tokens operation=session_creation userID=585ec0ff-e805-495e-b8fc-5b0b8dd288ff tenant=aisuru-staging-tokenized.aclambda.online
2
+ export type CreditsOperation =
3
+ | 'twin_creation'
4
+ | 'session_creation'
5
+ | 'import_document'
6
+ // accepted by the API and normalized to session_creation
7
+ | 'dt_session_creation';
8
+
2
9
  export const getCredits = async ({
3
10
  operation = 'session_creation',
4
11
  baseUrl,
5
12
  userID,
6
- userName,
7
13
  tenant,
14
+ characters,
8
15
  }: {
9
- operation?: string;
16
+ operation?: CreditsOperation;
10
17
  baseUrl: string;
11
- userID?: string | null;
12
- userName?: string | null;
18
+ userID: string;
13
19
  tenant: string;
20
+ characters?: number;
14
21
  }): Promise<{
15
22
  enough: boolean;
16
23
  required: number;
24
+ tokens?: number;
17
25
  }> => {
18
- if (!userID && !userName) {
19
- throw new Error('Either userID or userName must be provided');
26
+ if (!userID) {
27
+ throw new Error('userID must be provided');
28
+ }
29
+ if (operation === 'import_document' && characters == null) {
30
+ throw new Error('characters must be provided for import_document');
20
31
  }
32
+
21
33
  const resp = await fetch(`${baseUrl}/api/verify-tokens`, {
22
34
  method: 'POST',
23
35
  headers: {
@@ -26,8 +38,8 @@ export const getCredits = async ({
26
38
  body: JSON.stringify({
27
39
  operation,
28
40
  userID,
29
- userName,
30
41
  tenant,
42
+ ...(operation === 'import_document' ? { characters } : {}),
31
43
  }),
32
44
  });
33
45
 
package/src/index.tsx CHANGED
@@ -464,7 +464,6 @@ const Memori: React.FC<Props> = ({
464
464
  secretToken,
465
465
  }}
466
466
  __WEBCOMPONENT__={__WEBCOMPONENT__}
467
- ownerUserName={ownerUserName ?? memori.ownerUserName}
468
467
  ownerUserID={ownerUserID ?? memori.ownerUserID}
469
468
  tenant={tenant}
470
469
  tenantID={tenantID}
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const version = '8.38.8';
2
+ export const version = '8.39.0';