mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-23 23:16:58 +08:00
### 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:
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
8
web/src/pages/chat/embed-modal/index.less
Normal file
8
web/src/pages/chat/embed-modal/index.less
Normal file
@ -0,0 +1,8 @@
|
||||
.codeCard {
|
||||
.clearCardBody();
|
||||
}
|
||||
|
||||
.codeText {
|
||||
padding: 10px;
|
||||
background-color: #e8e8ea;
|
||||
}
|
||||
70
web/src/pages/chat/embed-modal/index.tsx
Normal file
70
web/src/pages/chat/embed-modal/index.tsx
Normal 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;
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -202,7 +202,7 @@ const model: DvaModel<ChatModelState> = {
|
||||
payload: data.data,
|
||||
});
|
||||
}
|
||||
return data.retcode;
|
||||
return data;
|
||||
},
|
||||
*removeToken({ payload }, { call, put }) {
|
||||
const { data } = yield call(
|
||||
|
||||
@ -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} />
|
||||
)}
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import Markdown from 'react-markdown';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
const SharedMarkdown = ({ content }: { content: string }) => {
|
||||
return (
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={
|
||||
{
|
||||
code(props: any) {
|
||||
const { children, className, node, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return match ? (
|
||||
<SyntaxHighlighter {...rest} PreTag="div" language={match[1]}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code {...rest} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
} as any
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</Markdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharedMarkdown;
|
||||
Reference in New Issue
Block a user