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)
This commit is contained in:
balibabu 2024-04-19 16:55:23 +08:00 committed by GitHub
parent 962c66714e
commit cda7b607cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 314 additions and 106 deletions

View File

@ -2,7 +2,11 @@ import Markdown from 'react-markdown';
import SyntaxHighlighter from 'react-syntax-highlighter'; import SyntaxHighlighter from 'react-syntax-highlighter';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
const SharedMarkdown = ({ content }: { content: string }) => { const HightLightMarkdown = ({
children,
}: {
children: string | null | undefined;
}) => {
return ( return (
<Markdown <Markdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
@ -24,9 +28,9 @@ const SharedMarkdown = ({ content }: { content: string }) => {
} as any } as any
} }
> >
{content} {children}
</Markdown> </Markdown>
); );
}; };
export default SharedMarkdown; export default HightLightMarkdown;

View File

@ -4,7 +4,7 @@ import {
IStats, IStats,
IToken, IToken,
} from '@/interfaces/database/chat'; } from '@/interfaces/database/chat';
import { useCallback, useEffect, useState } from 'react'; import { useCallback } from 'react';
import { useDispatch, useSelector } from 'umi'; import { useDispatch, useSelector } from 'umi';
export const useFetchDialogList = () => { export const useFetchDialogList = () => {
@ -299,27 +299,4 @@ export const useCompleteSharedConversation = () => {
return completeSharedConversation; 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 //#endregion

View File

@ -33,3 +33,12 @@
.pointerCursor() { .pointerCursor() {
cursor: pointer; cursor: pointer;
} }
.clearCardBody() {
:global {
.ant-card-body {
padding: 0;
margin: 0;
}
}
}

View File

@ -349,7 +349,7 @@ export default {
'This sets the maximum length of the models output, measured in the number of tokens (words or pieces of words).', 'This sets the maximum length of the models output, measured in the number of tokens (words or pieces of words).',
quote: 'Show Quote', quote: 'Show Quote',
quoteTip: 'Should the source of the original text be displayed?', quoteTip: 'Should the source of the original text be displayed?',
overview: 'Overview', overview: 'API',
pv: 'Number of messages', pv: 'Number of messages',
uv: 'Active user number', uv: 'Active user number',
speed: 'Token output speed', speed: 'Token output speed',
@ -367,6 +367,14 @@ export default {
createNewKey: 'Create new key', createNewKey: 'Create new key',
created: 'Created', created: 'Created',
action: 'Action', 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: { setting: {
profile: 'Profile', profile: 'Profile',

View File

@ -321,7 +321,7 @@ export default {
'這設置了模型輸出的最大長度,以標記(單詞或單詞片段)的數量來衡量。', '這設置了模型輸出的最大長度,以標記(單詞或單詞片段)的數量來衡量。',
quote: '顯示引文', quote: '顯示引文',
quoteTip: '是否應該顯示原文出處?', quoteTip: '是否應該顯示原文出處?',
overview: '概覽', overview: 'API',
pv: '消息數', pv: '消息數',
uv: '活躍用戶數', uv: '活躍用戶數',
speed: 'Token 輸出速度', speed: 'Token 輸出速度',
@ -339,6 +339,13 @@ export default {
createNewKey: '創建新密鑰', createNewKey: '創建新密鑰',
created: '創建於', created: '創建於',
action: '操作', action: '操作',
embedModalTitle: '嵌入網站',
comingSoon: '即將推出',
fullScreenTitle: '全屏嵌入',
fullScreenDescription: '將以下iframe嵌入您的網站處於所需位置',
partialTitle: '部分嵌入',
extensionTitle: 'Chrome 插件',
tokenError: '請先創建 Api Token!',
}, },
setting: { setting: {
profile: '概述', profile: '概述',

View File

@ -338,7 +338,7 @@ export default {
'这设置了模型输出的最大长度,以标记(单词或单词片段)的数量来衡量。', '这设置了模型输出的最大长度,以标记(单词或单词片段)的数量来衡量。',
quote: '显示引文', quote: '显示引文',
quoteTip: '是否应该显示原文出处?', quoteTip: '是否应该显示原文出处?',
overview: '概览', overview: 'API',
pv: '消息数', pv: '消息数',
uv: '活跃用户数', uv: '活跃用户数',
speed: 'Token 输出速度', speed: 'Token 输出速度',
@ -356,6 +356,13 @@ export default {
createNewKey: '创建新密钥', createNewKey: '创建新密钥',
created: '创建于', created: '创建于',
action: '操作', action: '操作',
embedModalTitle: '嵌入网站',
comingSoon: '即将推出',
fullScreenTitle: '全屏嵌入',
fullScreenDescription: '将以下iframe嵌入您的网站处于所需位置',
partialTitle: '部分嵌入',
extensionTitle: 'Chrome 插件',
tokenError: '请先创建 Api Token!',
}, },
setting: { setting: {
profile: '概要', profile: '概要',

View File

@ -1,17 +1,19 @@
import CopyToClipboard from '@/components/copy-to-clipboard';
import LineChart from '@/components/line-chart'; import LineChart from '@/components/line-chart';
import { useCreatePublicUrlToken } from '@/hooks/chatHooks';
import { useSetModalState, useTranslate } from '@/hooks/commonHooks'; import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { IDialog, IStats } from '@/interfaces/database/chat'; import { IDialog, IStats } from '@/interfaces/database/chat';
import { ReloadOutlined } from '@ant-design/icons';
import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd'; import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd';
import { RangePickerProps } from 'antd/es/date-picker'; import { RangePickerProps } from 'antd/es/date-picker';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import camelCase from 'lodash/camelCase'; import camelCase from 'lodash/camelCase';
import { Link } from 'umi';
import ChatApiKeyModal from '../chat-api-key-modal'; 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'; import styles from './index.less';
const { Paragraph } = Typography; const { Paragraph } = Typography;
@ -24,16 +26,18 @@ const ChatOverviewModal = ({
}: IModalProps<any> & { dialog: IDialog }) => { }: IModalProps<any> & { dialog: IDialog }) => {
const { t } = useTranslate('chat'); const { t } = useTranslate('chat');
const chartList = useSelectChartStatsList(); const chartList = useSelectChartStatsList();
const { urlWithToken, createUrlToken, token } = useCreatePublicUrlToken(
dialog.id,
visible,
);
const { const {
visible: apiKeyVisible, visible: apiKeyVisible,
hideModal: hideApiKeyModal, hideModal: hideApiKeyModal,
showModal: showApiKeyModal, showModal: showApiKeyModal,
} = useSetModalState(); } = useSetModalState();
const {
embedVisible,
hideEmbedModal,
showEmbedModal,
embedToken,
errorContextHolder,
} = useShowEmbedModal(dialog.id);
const { pickerValue, setPickerValue } = useFetchStatsOnMount(visible); const { pickerValue, setPickerValue } = useFetchStatsOnMount(visible);
@ -41,6 +45,8 @@ const ChatOverviewModal = ({
return current && current > dayjs().endOf('day'); return current && current > dayjs().endOf('day');
}; };
const { handlePreview, contextHolder } = usePreviewChat(dialog.id);
return ( return (
<> <>
<Modal <Modal
@ -50,36 +56,41 @@ const ChatOverviewModal = ({
width={'100vw'} width={'100vw'}
> >
<Flex vertical gap={'middle'}> <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')}> <Card title={t('backendServiceApi')}>
<Flex gap={8} vertical> <Flex gap={8} vertical>
{t('serviceApiEndpoint')} {t('serviceApiEndpoint')}
<Paragraph copyable className={styles.linkText}> <Paragraph copyable className={styles.linkText}>
This is a copyable text. https://demo.ragflow.io/v1/api/
</Paragraph> </Paragraph>
</Flex> </Flex>
<Space size={'middle'}> <Space size={'middle'}>
<Button onClick={showApiKeyModal}>{t('apiKey')}</Button> <Button onClick={showApiKeyModal}>{t('apiKey')}</Button>
<a
href={
'https://github.com/infiniflow/ragflow/blob/main/docs/conversation_api.md'
}
target="_blank"
rel="noreferrer"
>
<Button>{t('apiReference')}</Button> <Button>{t('apiReference')}</Button>
</a>
</Space> </Space>
</Card> </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> <Space>
<b>{t('dateRange')}</b> <b>{t('dateRange')}</b>
<RangePicker <RangePicker
@ -103,6 +114,13 @@ const ChatOverviewModal = ({
hideModal={hideApiKeyModal} hideModal={hideApiKeyModal}
dialogId={dialog.id} dialogId={dialog.id}
></ChatApiKeyModal> ></ChatApiKeyModal>
<EmbedModal
token={embedToken}
visible={embedVisible}
hideModal={hideEmbedModal}
></EmbedModal>
{contextHolder}
{errorContextHolder}
</Modal> </Modal>
</> </>
); );

View File

@ -0,0 +1,8 @@
.codeCard {
.clearCardBody();
}
.codeText {
padding: 10px;
background-color: #e8e8ea;
}

View File

@ -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;

View File

@ -14,15 +14,21 @@ import {
useRemoveToken, useRemoveToken,
useSelectConversationList, useSelectConversationList,
useSelectDialogList, useSelectDialogList,
useSelectStats,
useSelectTokenList, useSelectTokenList,
useSetDialog, useSetDialog,
useUpdateConversation, useUpdateConversation,
} from '@/hooks/chatHooks'; } from '@/hooks/chatHooks';
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/commonHooks'; import {
useSetModalState,
useShowDeleteConfirm,
useTranslate,
} from '@/hooks/commonHooks';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { IConversation, IDialog, IStats } from '@/interfaces/database/chat'; import { IConversation, IDialog, IStats } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge'; import { IChunk } from '@/interfaces/database/knowledge';
import { getFileExtension } from '@/utils'; import { getFileExtension } from '@/utils';
import { message } from 'antd';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { import {
@ -777,35 +783,35 @@ type ChartStatsType = {
}; };
export const useSelectChartStatsList = (): ChartStatsType => { export const useSelectChartStatsList = (): ChartStatsType => {
// const stats: IStats = useSelectStats(); const stats: IStats = useSelectStats();
const stats = { // const stats = {
pv: [ // pv: [
['2024-06-01', 1], // ['2024-06-01', 1],
['2024-07-24', 3], // ['2024-07-24', 3],
['2024-09-01', 10], // ['2024-09-01', 10],
], // ],
uv: [ // uv: [
['2024-02-01', 0], // ['2024-02-01', 0],
['2024-03-01', 99], // ['2024-03-01', 99],
['2024-05-01', 3], // ['2024-05-01', 3],
], // ],
speed: [ // speed: [
['2024-09-01', 2], // ['2024-09-01', 2],
['2024-09-01', 3], // ['2024-09-01', 3],
], // ],
tokens: [ // tokens: [
['2024-09-01', 1], // ['2024-09-01', 1],
['2024-09-01', 3], // ['2024-09-01', 3],
], // ],
round: [ // round: [
['2024-09-01', 0], // ['2024-09-01', 0],
['2024-09-01', 3], // ['2024-09-01', 3],
], // ],
thumb_up: [ // thumb_up: [
['2024-09-01', 3], // ['2024-09-01', 3],
['2024-09-01', 9], // ['2024-09-01', 9],
], // ],
}; // };
return Object.keys(stats).reduce((pre, cur) => { return Object.keys(stats).reduce((pre, cur) => {
const item = stats[cur as keyof IStats]; const item = stats[cur as keyof IStats];
@ -819,4 +825,93 @@ export const useSelectChartStatsList = (): ChartStatsType => {
}, {} as 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 //#endregion

View File

@ -1,6 +1,11 @@
import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg'; import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg';
import RenameModal from '@/components/rename-modal'; import RenameModal from '@/components/rename-modal';
import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons'; import {
CloudOutlined,
DeleteOutlined,
EditOutlined,
FormOutlined,
} from '@ant-design/icons';
import { import {
Avatar, Avatar,
Button, Button,
@ -185,16 +190,16 @@ const Chat = () => {
), ),
}, },
{ type: 'divider' }, { type: 'divider' },
// { {
// key: '3', key: '3',
// onClick: handleShowOverviewModal(dialog), onClick: handleShowOverviewModal(dialog),
// label: ( label: (
// <Space> <Space>
// <ProfileOutlined /> <CloudOutlined />
// {t('overview')} {t('overview')}
// </Space> </Space>
// ), ),
// }, },
]; ];
return appItems; return appItems;

View File

@ -202,7 +202,7 @@ const model: DvaModel<ChatModelState> = {
payload: data.data, payload: data.data,
}); });
} }
return data.retcode; return data;
}, },
*removeToken({ payload }, { call, put }) { *removeToken({ payload }, { call, put }) {
const { data } = yield call( const { data } = yield call(

View File

@ -6,10 +6,10 @@ import { Avatar, Button, Flex, Input, Skeleton, Spin } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useSelectConversationLoading } from '../hooks'; import { useSelectConversationLoading } from '../hooks';
import HightLightMarkdown from '@/components/highlight-markdown';
import React, { ChangeEventHandler, forwardRef } from 'react'; import React, { ChangeEventHandler, forwardRef } from 'react';
import { IClientConversation } from '../interface'; import { IClientConversation } from '../interface';
import styles from './index.less'; import styles from './index.less';
import SharedMarkdown from './shared-markdown';
const MessageItem = ({ item }: { item: Message }) => { const MessageItem = ({ item }: { item: Message }) => {
const isAssistant = item.role === MessageType.Assistant; const isAssistant = item.role === MessageType.Assistant;
@ -46,7 +46,7 @@ const MessageItem = ({ item }: { item: Message }) => {
<b>{isAssistant ? '' : 'You'}</b> <b>{isAssistant ? '' : 'You'}</b>
<div className={styles.messageText}> <div className={styles.messageText}>
{item.content !== '' ? ( {item.content !== '' ? (
<SharedMarkdown content={item.content}></SharedMarkdown> <HightLightMarkdown>{item.content}</HightLightMarkdown>
) : ( ) : (
<Skeleton active className={styles.messageEmpty} /> <Skeleton active className={styles.messageEmpty} />
)} )}

View File

@ -98,8 +98,8 @@ request.interceptors.request.use((url: string, options: any) => {
url, url,
options: { options: {
...options, ...options,
// data, data,
// params, params,
headers: { headers: {
...(options.skipToken ? undefined : { [Authorization]: authorization }), ...(options.skipToken ? undefined : { [Authorization]: authorization }),
...options.headers, ...options.headers,