浏览代码

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)
tags/v0.3.0
balibabu 1年前
父节点
当前提交
cda7b607cb
没有帐户链接到提交者的电子邮件

web/src/pages/chat/share/shared-markdown.tsx → web/src/components/highlight-markdown/index.tsx 查看文件

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]}
} as any } as any
} }
> >
{content}
{children}
</Markdown> </Markdown>
); );
}; };


export default SharedMarkdown;
export default HightLightMarkdown;

+ 1
- 24
web/src/hooks/chatHooks.ts 查看文件

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 = () => {
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

+ 9
- 0
web/src/less/mixins.less 查看文件

.pointerCursor() { .pointerCursor() {
cursor: pointer; cursor: pointer;
} }

.clearCardBody() {
:global {
.ant-card-body {
padding: 0;
margin: 0;
}
}
}

+ 9
- 1
web/src/locales/en.ts 查看文件

'This sets the maximum length of the model’s output, measured in the number of tokens (words or pieces of words).', 'This sets the maximum length of the model’s 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',
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',

+ 8
- 1
web/src/locales/zh-traditional.ts 查看文件

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

+ 8
- 1
web/src/locales/zh.ts 查看文件

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

+ 48
- 30
web/src/pages/chat/chat-overview-modal/index.tsx 查看文件

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;
}: 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);


return current && current > dayjs().endOf('day'); return current && current > dayjs().endOf('day');
}; };


const { handlePreview, contextHolder } = usePreviewChat(dialog.id);

return ( return (
<> <>
<Modal <Modal
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>
<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> </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
hideModal={hideApiKeyModal} hideModal={hideApiKeyModal}
dialogId={dialog.id} dialogId={dialog.id}
></ChatApiKeyModal> ></ChatApiKeyModal>
<EmbedModal
token={embedToken}
visible={embedVisible}
hideModal={hideEmbedModal}
></EmbedModal>
{contextHolder}
{errorContextHolder}
</Modal> </Modal>
</> </>
); );

+ 8
- 0
web/src/pages/chat/embed-modal/index.less 查看文件

.codeCard {
.clearCardBody();
}

.codeText {
padding: 10px;
background-color: #e8e8ea;
}

+ 70
- 0
web/src/pages/chat/embed-modal/index.tsx 查看文件

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;

+ 125
- 30
web/src/pages/chat/hooks.ts 查看文件

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


export const useSelectChartStatsList = (): 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) => { return Object.keys(stats).reduce((pre, cur) => {
const item = stats[cur as keyof IStats]; const item = stats[cur as keyof IStats];
}, {} 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

+ 16
- 11
web/src/pages/chat/index.tsx 查看文件

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,
), ),
}, },
{ type: 'divider' }, { 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; return appItems;

+ 1
- 1
web/src/pages/chat/model.ts 查看文件

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(

+ 2
- 2
web/src/pages/chat/share/large.tsx 查看文件

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

+ 2
- 2
web/src/utils/request.ts 查看文件

url, url,
options: { options: {
...options, ...options,
// data,
// params,
data,
params,
headers: { headers: {
...(options.skipToken ? undefined : { [Authorization]: authorization }), ...(options.skipToken ? undefined : { [Authorization]: authorization }),
...options.headers, ...options.headers,

正在加载...
取消
保存