From cda7b607cb52ea882770352440a192a94cc26bdc Mon Sep 17 00:00:00 2001
From: balibabu <cike8899@users.noreply.github.com>
Date: Fri, 19 Apr 2024 16:55:23 +0800
Subject: [PATCH] feat: translate EmbedModal #345 (#455)

### What problem does this PR solve?

Embed the chat window into other websites through iframe

#345

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
---
 .../highlight-markdown/index.tsx}             |  10 +-
 web/src/hooks/chatHooks.ts                    |  25 +--
 web/src/less/mixins.less                      |   9 +
 web/src/locales/en.ts                         |  10 +-
 web/src/locales/zh-traditional.ts             |   9 +-
 web/src/locales/zh.ts                         |   9 +-
 .../pages/chat/chat-overview-modal/index.tsx  |  78 +++++----
 web/src/pages/chat/embed-modal/index.less     |   8 +
 web/src/pages/chat/embed-modal/index.tsx      |  70 ++++++++
 web/src/pages/chat/hooks.ts                   | 155 ++++++++++++++----
 web/src/pages/chat/index.tsx                  |  27 +--
 web/src/pages/chat/model.ts                   |   2 +-
 web/src/pages/chat/share/large.tsx            |   4 +-
 web/src/utils/request.ts                      |   4 +-
 14 files changed, 314 insertions(+), 106 deletions(-)
 rename web/src/{pages/chat/share/shared-markdown.tsx => components/highlight-markdown/index.tsx} (84%)
 create mode 100644 web/src/pages/chat/embed-modal/index.less
 create mode 100644 web/src/pages/chat/embed-modal/index.tsx

diff --git a/web/src/pages/chat/share/shared-markdown.tsx b/web/src/components/highlight-markdown/index.tsx
similarity index 84%
rename from web/src/pages/chat/share/shared-markdown.tsx
rename to web/src/components/highlight-markdown/index.tsx
index 2c1a3c0..7c393fa 100644
--- a/web/src/pages/chat/share/shared-markdown.tsx
+++ b/web/src/components/highlight-markdown/index.tsx
@@ -2,7 +2,11 @@ import Markdown from 'react-markdown';
 import SyntaxHighlighter from 'react-syntax-highlighter';
 import remarkGfm from 'remark-gfm';
 
-const SharedMarkdown = ({ content }: { content: string }) => {
+const HightLightMarkdown = ({
+  children,
+}: {
+  children: string | null | undefined;
+}) => {
   return (
     <Markdown
       remarkPlugins={[remarkGfm]}
@@ -24,9 +28,9 @@ const SharedMarkdown = ({ content }: { content: string }) => {
         } as any
       }
     >
-      {content}
+      {children}
     </Markdown>
   );
 };
 
-export default SharedMarkdown;
+export default HightLightMarkdown;
diff --git a/web/src/hooks/chatHooks.ts b/web/src/hooks/chatHooks.ts
index 3f33f6b..34e0d74 100644
--- a/web/src/hooks/chatHooks.ts
+++ b/web/src/hooks/chatHooks.ts
@@ -4,7 +4,7 @@ import {
   IStats,
   IToken,
 } from '@/interfaces/database/chat';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback } from 'react';
 import { useDispatch, useSelector } from 'umi';
 
 export const useFetchDialogList = () => {
@@ -299,27 +299,4 @@ export const useCompleteSharedConversation = () => {
   return completeSharedConversation;
 };
 
-export const useCreatePublicUrlToken = (dialogId: string, visible: boolean) => {
-  const [token, setToken] = useState();
-  const createToken = useCreateToken(dialogId);
-  const { protocol, host } = window.location;
-
-  const urlWithToken = `${protocol}//${host}/chat/share?shared_id=${token}`;
-
-  const createUrlToken = useCallback(async () => {
-    if (visible) {
-      const data = await createToken();
-      const urlToken = data.data?.token;
-      if (urlToken) {
-        setToken(urlToken);
-      }
-    }
-  }, [createToken, visible]);
-
-  useEffect(() => {
-    createUrlToken();
-  }, [createUrlToken]);
-
-  return { token, createUrlToken, urlWithToken };
-};
 //#endregion
diff --git a/web/src/less/mixins.less b/web/src/less/mixins.less
index b5256f7..ba363ec 100644
--- a/web/src/less/mixins.less
+++ b/web/src/less/mixins.less
@@ -33,3 +33,12 @@
 .pointerCursor() {
   cursor: pointer;
 }
+
+.clearCardBody() {
+  :global {
+    .ant-card-body {
+      padding: 0;
+      margin: 0;
+    }
+  }
+}
diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts
index dafe2a0..ea3f507 100644
--- a/web/src/locales/en.ts
+++ b/web/src/locales/en.ts
@@ -349,7 +349,7 @@ export default {
         'This sets the maximum length of the model’s output, measured in the number of tokens (words or pieces of words).',
       quote: 'Show Quote',
       quoteTip: 'Should the source of the original text be displayed?',
-      overview: 'Overview',
+      overview: 'API',
       pv: 'Number of messages',
       uv: 'Active user number',
       speed: 'Token output speed',
@@ -367,6 +367,14 @@ export default {
       createNewKey: 'Create new key',
       created: 'Created',
       action: 'Action',
+      embedModalTitle: 'Embed into website',
+      comingSoon: 'Coming Soon',
+      fullScreenTitle: 'Full Embed',
+      fullScreenDescription:
+        'Embed the following iframe into your website at the desired location',
+      partialTitle: 'Partial Embed',
+      extensionTitle: 'Chrome Extension',
+      tokenError: 'Please create API Token first!',
     },
     setting: {
       profile: 'Profile',
diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts
index f469759..73c150a 100644
--- a/web/src/locales/zh-traditional.ts
+++ b/web/src/locales/zh-traditional.ts
@@ -321,7 +321,7 @@ export default {
         '這設置了模型輸出的最大長度,以標記(單詞或單詞片段)的數量來衡量。',
       quote: '顯示引文',
       quoteTip: '是否應該顯示原文出處?',
-      overview: '概覽',
+      overview: 'API',
       pv: '消息數',
       uv: '活躍用戶數',
       speed: 'Token 輸出速度',
@@ -339,6 +339,13 @@ export default {
       createNewKey: '創建新密鑰',
       created: '創建於',
       action: '操作',
+      embedModalTitle: '嵌入網站',
+      comingSoon: '即將推出',
+      fullScreenTitle: '全屏嵌入',
+      fullScreenDescription: '將以下iframe嵌入您的網站處於所需位置',
+      partialTitle: '部分嵌入',
+      extensionTitle: 'Chrome 插件',
+      tokenError: '請先創建 Api Token!',
     },
     setting: {
       profile: '概述',
diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts
index 6d66234..b379303 100644
--- a/web/src/locales/zh.ts
+++ b/web/src/locales/zh.ts
@@ -338,7 +338,7 @@ export default {
         '这设置了模型输出的最大长度,以标记(单词或单词片段)的数量来衡量。',
       quote: '显示引文',
       quoteTip: '是否应该显示原文出处?',
-      overview: '概览',
+      overview: 'API',
       pv: '消息数',
       uv: '活跃用户数',
       speed: 'Token 输出速度',
@@ -356,6 +356,13 @@ export default {
       createNewKey: '创建新密钥',
       created: '创建于',
       action: '操作',
+      embedModalTitle: '嵌入网站',
+      comingSoon: '即将推出',
+      fullScreenTitle: '全屏嵌入',
+      fullScreenDescription: '将以下iframe嵌入您的网站处于所需位置',
+      partialTitle: '部分嵌入',
+      extensionTitle: 'Chrome 插件',
+      tokenError: '请先创建 Api Token!',
     },
     setting: {
       profile: '概要',
diff --git a/web/src/pages/chat/chat-overview-modal/index.tsx b/web/src/pages/chat/chat-overview-modal/index.tsx
index c8a1c41..6381d65 100644
--- a/web/src/pages/chat/chat-overview-modal/index.tsx
+++ b/web/src/pages/chat/chat-overview-modal/index.tsx
@@ -1,17 +1,19 @@
-import CopyToClipboard from '@/components/copy-to-clipboard';
 import LineChart from '@/components/line-chart';
-import { useCreatePublicUrlToken } from '@/hooks/chatHooks';
 import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
 import { IModalProps } from '@/interfaces/common';
 import { IDialog, IStats } from '@/interfaces/database/chat';
-import { ReloadOutlined } from '@ant-design/icons';
 import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd';
 import { RangePickerProps } from 'antd/es/date-picker';
 import dayjs from 'dayjs';
 import camelCase from 'lodash/camelCase';
-import { Link } from 'umi';
 import ChatApiKeyModal from '../chat-api-key-modal';
-import { useFetchStatsOnMount, useSelectChartStatsList } from '../hooks';
+import EmbedModal from '../embed-modal';
+import {
+  useFetchStatsOnMount,
+  usePreviewChat,
+  useSelectChartStatsList,
+  useShowEmbedModal,
+} from '../hooks';
 import styles from './index.less';
 
 const { Paragraph } = Typography;
@@ -24,16 +26,18 @@ const ChatOverviewModal = ({
 }: IModalProps<any> & { dialog: IDialog }) => {
   const { t } = useTranslate('chat');
   const chartList = useSelectChartStatsList();
-  const { urlWithToken, createUrlToken, token } = useCreatePublicUrlToken(
-    dialog.id,
-    visible,
-  );
-
   const {
     visible: apiKeyVisible,
     hideModal: hideApiKeyModal,
     showModal: showApiKeyModal,
   } = useSetModalState();
+  const {
+    embedVisible,
+    hideEmbedModal,
+    showEmbedModal,
+    embedToken,
+    errorContextHolder,
+  } = useShowEmbedModal(dialog.id);
 
   const { pickerValue, setPickerValue } = useFetchStatsOnMount(visible);
 
@@ -41,6 +45,8 @@ const ChatOverviewModal = ({
     return current && current > dayjs().endOf('day');
   };
 
+  const { handlePreview, contextHolder } = usePreviewChat(dialog.id);
+
   return (
     <>
       <Modal
@@ -50,36 +56,41 @@ const ChatOverviewModal = ({
         width={'100vw'}
       >
         <Flex vertical gap={'middle'}>
-          <Card title={dialog.name}>
-            <Flex gap={8} vertical>
-              {t('publicUrl')}
-              <Flex className={styles.linkText} gap={10}>
-                <span>{urlWithToken}</span>
-                <CopyToClipboard text={urlWithToken}></CopyToClipboard>
-                <ReloadOutlined onClick={createUrlToken} />
-              </Flex>
-              <Space size={'middle'}>
-                <Button>
-                  <Link to={`/chat/share?shared_id=${token}`} target="_blank">
-                    {t('preview')}
-                  </Link>
-                </Button>
-                <Button>{t('embedded')}</Button>
-              </Space>
-            </Flex>
-          </Card>
           <Card title={t('backendServiceApi')}>
             <Flex gap={8} vertical>
               {t('serviceApiEndpoint')}
               <Paragraph copyable className={styles.linkText}>
-                This is a copyable text.
+                https://demo.ragflow.io/v1/api/
               </Paragraph>
             </Flex>
             <Space size={'middle'}>
               <Button onClick={showApiKeyModal}>{t('apiKey')}</Button>
-              <Button>{t('apiReference')}</Button>
+              <a
+                href={
+                  'https://github.com/infiniflow/ragflow/blob/main/docs/conversation_api.md'
+                }
+                target="_blank"
+                rel="noreferrer"
+              >
+                <Button>{t('apiReference')}</Button>
+              </a>
             </Space>
           </Card>
+          <Card title={dialog.name}>
+            <Flex gap={8} vertical>
+              {t('publicUrl')}
+              {/* <Flex className={styles.linkText} gap={10}>
+                <span>{urlWithToken}</span>
+                <CopyToClipboard text={urlWithToken}></CopyToClipboard>
+                <ReloadOutlined onClick={createUrlToken} />
+              </Flex> */}
+              <Space size={'middle'}>
+                <Button onClick={handlePreview}>{t('preview')}</Button>
+                <Button onClick={showEmbedModal}>{t('embedded')}</Button>
+              </Space>
+            </Flex>
+          </Card>
+
           <Space>
             <b>{t('dateRange')}</b>
             <RangePicker
@@ -103,6 +114,13 @@ const ChatOverviewModal = ({
           hideModal={hideApiKeyModal}
           dialogId={dialog.id}
         ></ChatApiKeyModal>
+        <EmbedModal
+          token={embedToken}
+          visible={embedVisible}
+          hideModal={hideEmbedModal}
+        ></EmbedModal>
+        {contextHolder}
+        {errorContextHolder}
       </Modal>
     </>
   );
diff --git a/web/src/pages/chat/embed-modal/index.less b/web/src/pages/chat/embed-modal/index.less
new file mode 100644
index 0000000..5e807d8
--- /dev/null
+++ b/web/src/pages/chat/embed-modal/index.less
@@ -0,0 +1,8 @@
+.codeCard {
+  .clearCardBody();
+}
+
+.codeText {
+  padding: 10px;
+  background-color: #e8e8ea;
+}
diff --git a/web/src/pages/chat/embed-modal/index.tsx b/web/src/pages/chat/embed-modal/index.tsx
new file mode 100644
index 0000000..44d3967
--- /dev/null
+++ b/web/src/pages/chat/embed-modal/index.tsx
@@ -0,0 +1,70 @@
+import CopyToClipboard from '@/components/copy-to-clipboard';
+import HightLightMarkdown from '@/components/highlight-markdown';
+import { useTranslate } from '@/hooks/commonHooks';
+import { IModalProps } from '@/interfaces/common';
+import { Card, Modal, Tabs, TabsProps } from 'antd';
+import styles from './index.less';
+
+const EmbedModal = ({
+  visible,
+  hideModal,
+  token = '',
+}: IModalProps<any> & { token: string }) => {
+  const { t } = useTranslate('chat');
+
+  const text = `
+  ~~~ html
+  <iframe
+  src="https://demo.ragflow.io/chat/share?shared_id=${token}"
+  style="width: 100%; height: 100%; min-height: 600px"
+  frameborder="0"
+>
+</iframe>
+~~~
+  `;
+
+  const items: TabsProps['items'] = [
+    {
+      key: '1',
+      label: t('fullScreenTitle'),
+      children: (
+        <Card
+          title={t('fullScreenDescription')}
+          extra={<CopyToClipboard text={text}></CopyToClipboard>}
+          className={styles.codeCard}
+        >
+          <HightLightMarkdown>{text}</HightLightMarkdown>
+        </Card>
+      ),
+    },
+    {
+      key: '2',
+      label: t('partialTitle'),
+      children: t('comingSoon'),
+    },
+    {
+      key: '3',
+      label: t('extensionTitle'),
+      children: t('comingSoon'),
+    },
+  ];
+
+  const onChange = (key: string) => {
+    console.log(key);
+  };
+
+  return (
+    <Modal
+      title={t('embedModalTitle')}
+      open={visible}
+      style={{ top: 300 }}
+      width={'50vw'}
+      onOk={hideModal}
+      onCancel={hideModal}
+    >
+      <Tabs defaultActiveKey="1" items={items} onChange={onChange} />
+    </Modal>
+  );
+};
+
+export default EmbedModal;
diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts
index cf5c858..e44155d 100644
--- a/web/src/pages/chat/hooks.ts
+++ b/web/src/pages/chat/hooks.ts
@@ -14,15 +14,21 @@ import {
   useRemoveToken,
   useSelectConversationList,
   useSelectDialogList,
+  useSelectStats,
   useSelectTokenList,
   useSetDialog,
   useUpdateConversation,
 } from '@/hooks/chatHooks';
-import { useSetModalState, useShowDeleteConfirm } from '@/hooks/commonHooks';
+import {
+  useSetModalState,
+  useShowDeleteConfirm,
+  useTranslate,
+} from '@/hooks/commonHooks';
 import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
 import { IConversation, IDialog, IStats } from '@/interfaces/database/chat';
 import { IChunk } from '@/interfaces/database/knowledge';
 import { getFileExtension } from '@/utils';
+import { message } from 'antd';
 import dayjs, { Dayjs } from 'dayjs';
 import omit from 'lodash/omit';
 import {
@@ -777,35 +783,35 @@ type ChartStatsType = {
 };
 
 export const useSelectChartStatsList = (): ChartStatsType => {
-  // const stats: IStats = useSelectStats();
-  const stats = {
-    pv: [
-      ['2024-06-01', 1],
-      ['2024-07-24', 3],
-      ['2024-09-01', 10],
-    ],
-    uv: [
-      ['2024-02-01', 0],
-      ['2024-03-01', 99],
-      ['2024-05-01', 3],
-    ],
-    speed: [
-      ['2024-09-01', 2],
-      ['2024-09-01', 3],
-    ],
-    tokens: [
-      ['2024-09-01', 1],
-      ['2024-09-01', 3],
-    ],
-    round: [
-      ['2024-09-01', 0],
-      ['2024-09-01', 3],
-    ],
-    thumb_up: [
-      ['2024-09-01', 3],
-      ['2024-09-01', 9],
-    ],
-  };
+  const stats: IStats = useSelectStats();
+  // const stats = {
+  //   pv: [
+  //     ['2024-06-01', 1],
+  //     ['2024-07-24', 3],
+  //     ['2024-09-01', 10],
+  //   ],
+  //   uv: [
+  //     ['2024-02-01', 0],
+  //     ['2024-03-01', 99],
+  //     ['2024-05-01', 3],
+  //   ],
+  //   speed: [
+  //     ['2024-09-01', 2],
+  //     ['2024-09-01', 3],
+  //   ],
+  //   tokens: [
+  //     ['2024-09-01', 1],
+  //     ['2024-09-01', 3],
+  //   ],
+  //   round: [
+  //     ['2024-09-01', 0],
+  //     ['2024-09-01', 3],
+  //   ],
+  //   thumb_up: [
+  //     ['2024-09-01', 3],
+  //     ['2024-09-01', 9],
+  //   ],
+  // };
 
   return Object.keys(stats).reduce((pre, cur) => {
     const item = stats[cur as keyof IStats];
@@ -819,4 +825,93 @@ export const useSelectChartStatsList = (): ChartStatsType => {
   }, {} as ChartStatsType);
 };
 
+export const useShowTokenEmptyError = () => {
+  const [messageApi, contextHolder] = message.useMessage();
+  const { t } = useTranslate('chat');
+
+  const showTokenEmptyError = useCallback(() => {
+    messageApi.error(t('tokenError'));
+  }, [messageApi, t]);
+  return { showTokenEmptyError, contextHolder };
+};
+
+const getUrlWithToken = (token: string) => {
+  const { protocol, host } = window.location;
+  return `${protocol}//${host}/chat/share?shared_id=${token}`;
+};
+
+const useFetchTokenListBeforeOtherStep = (dialogId: string) => {
+  const { showTokenEmptyError, contextHolder } = useShowTokenEmptyError();
+
+  const listToken = useListToken();
+  const tokenList = useSelectTokenList();
+
+  const token =
+    Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : '';
+
+  const handleOperate = useCallback(async () => {
+    const data = await listToken(dialogId);
+    const list = data.data;
+    if (data.retcode === 0 && Array.isArray(list) && list.length > 0) {
+      return list[0]?.token;
+    } else {
+      showTokenEmptyError();
+      return false;
+    }
+  }, [dialogId, listToken, showTokenEmptyError]);
+
+  return {
+    token,
+    contextHolder,
+    handleOperate,
+  };
+};
+
+export const useShowEmbedModal = (dialogId: string) => {
+  const {
+    visible: embedVisible,
+    hideModal: hideEmbedModal,
+    showModal: showEmbedModal,
+  } = useSetModalState();
+
+  const { handleOperate, token, contextHolder } =
+    useFetchTokenListBeforeOtherStep(dialogId);
+
+  const handleShowEmbedModal = useCallback(async () => {
+    const succeed = await handleOperate();
+    if (succeed) {
+      showEmbedModal();
+    }
+  }, [handleOperate, showEmbedModal]);
+
+  return {
+    showEmbedModal: handleShowEmbedModal,
+    hideEmbedModal,
+    embedVisible,
+    embedToken: token,
+    errorContextHolder: contextHolder,
+  };
+};
+
+export const usePreviewChat = (dialogId: string) => {
+  const { handleOperate, contextHolder } =
+    useFetchTokenListBeforeOtherStep(dialogId);
+
+  const open = useCallback((t: string) => {
+    window.open(getUrlWithToken(t), '_blank');
+  }, []);
+
+  const handlePreview = useCallback(async () => {
+    const token = await handleOperate();
+    if (token) {
+      open(token);
+    }
+  }, [handleOperate, open]);
+
+  return {
+    handlePreview,
+    contextHolder,
+  };
+};
+
 //#endregion
diff --git a/web/src/pages/chat/index.tsx b/web/src/pages/chat/index.tsx
index b07897b..a6499d5 100644
--- a/web/src/pages/chat/index.tsx
+++ b/web/src/pages/chat/index.tsx
@@ -1,6 +1,11 @@
 import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg';
 import RenameModal from '@/components/rename-modal';
-import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons';
+import {
+  CloudOutlined,
+  DeleteOutlined,
+  EditOutlined,
+  FormOutlined,
+} from '@ant-design/icons';
 import {
   Avatar,
   Button,
@@ -185,16 +190,16 @@ const Chat = () => {
         ),
       },
       { type: 'divider' },
-      // {
-      //   key: '3',
-      //   onClick: handleShowOverviewModal(dialog),
-      //   label: (
-      //     <Space>
-      //       <ProfileOutlined />
-      //       {t('overview')}
-      //     </Space>
-      //   ),
-      // },
+      {
+        key: '3',
+        onClick: handleShowOverviewModal(dialog),
+        label: (
+          <Space>
+            <CloudOutlined />
+            {t('overview')}
+          </Space>
+        ),
+      },
     ];
 
     return appItems;
diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts
index 5c302a2..e1a6122 100644
--- a/web/src/pages/chat/model.ts
+++ b/web/src/pages/chat/model.ts
@@ -202,7 +202,7 @@ const model: DvaModel<ChatModelState> = {
           payload: data.data,
         });
       }
-      return data.retcode;
+      return data;
     },
     *removeToken({ payload }, { call, put }) {
       const { data } = yield call(
diff --git a/web/src/pages/chat/share/large.tsx b/web/src/pages/chat/share/large.tsx
index 1e510af..1a5ba45 100644
--- a/web/src/pages/chat/share/large.tsx
+++ b/web/src/pages/chat/share/large.tsx
@@ -6,10 +6,10 @@ import { Avatar, Button, Flex, Input, Skeleton, Spin } from 'antd';
 import classNames from 'classnames';
 import { useSelectConversationLoading } from '../hooks';
 
+import HightLightMarkdown from '@/components/highlight-markdown';
 import React, { ChangeEventHandler, forwardRef } from 'react';
 import { IClientConversation } from '../interface';
 import styles from './index.less';
-import SharedMarkdown from './shared-markdown';
 
 const MessageItem = ({ item }: { item: Message }) => {
   const isAssistant = item.role === MessageType.Assistant;
@@ -46,7 +46,7 @@ const MessageItem = ({ item }: { item: Message }) => {
             <b>{isAssistant ? '' : 'You'}</b>
             <div className={styles.messageText}>
               {item.content !== '' ? (
-                <SharedMarkdown content={item.content}></SharedMarkdown>
+                <HightLightMarkdown>{item.content}</HightLightMarkdown>
               ) : (
                 <Skeleton active className={styles.messageEmpty} />
               )}
diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts
index 39d30da..267831d 100644
--- a/web/src/utils/request.ts
+++ b/web/src/utils/request.ts
@@ -98,8 +98,8 @@ request.interceptors.request.use((url: string, options: any) => {
     url,
     options: {
       ...options,
-      // data,
-      // params,
+      data,
+      params,
       headers: {
         ...(options.skipToken ? undefined : { [Authorization]: authorization }),
         ...options.headers,
-- 
GitLab