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

View File

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

View File

@ -33,3 +33,12 @@
.pointerCursor() {
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).',
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',

View File

@ -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: '概述',

View File

@ -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: '概要',

View File

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

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,
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

View File

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

View File

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

View File

@ -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} />
)}

View File

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