mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? feat: Expose the agent's chat window to third parties #1842 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
72
web/src/components/api-service/chat-api-key-modal/index.tsx
Normal file
72
web/src/components/api-service/chat-api-key-modal/index.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IToken } from '@/interfaces/database/chat';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import type { TableProps } from 'antd';
|
||||
import { Button, Modal, Space, Table } from 'antd';
|
||||
import { useOperateApiKey } from '../hooks';
|
||||
|
||||
const ChatApiKeyModal = ({
|
||||
dialogId,
|
||||
hideModal,
|
||||
idKey,
|
||||
}: IModalProps<any> & { dialogId: string; idKey: string }) => {
|
||||
const { createToken, removeToken, tokenList, listLoading, creatingLoading } =
|
||||
useOperateApiKey(dialogId, idKey);
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
const columns: TableProps<IToken>['columns'] = [
|
||||
{
|
||||
title: 'Token',
|
||||
dataIndex: 'token',
|
||||
key: 'token',
|
||||
render: (text) => <a>{text}</a>,
|
||||
},
|
||||
{
|
||||
title: t('created'),
|
||||
dataIndex: 'create_date',
|
||||
key: 'create_date',
|
||||
render: (text) => formatDate(text),
|
||||
},
|
||||
{
|
||||
title: t('action'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<CopyToClipboard text={record.token}></CopyToClipboard>
|
||||
<DeleteOutlined
|
||||
onClick={() => removeToken(record.token, record.tenant_id)}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={t('apiKey')}
|
||||
open
|
||||
onCancel={hideModal}
|
||||
cancelButtonProps={{ style: { display: 'none' } }}
|
||||
style={{ top: 300 }}
|
||||
onOk={hideModal}
|
||||
width={'50vw'}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tokenList}
|
||||
rowKey={'token'}
|
||||
loading={listLoading}
|
||||
/>
|
||||
<Button onClick={createToken} loading={creatingLoading}>
|
||||
{t('createNewKey')}
|
||||
</Button>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatApiKeyModal;
|
||||
@ -0,0 +1,21 @@
|
||||
.chartWrapper {
|
||||
height: 40vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chartItem {
|
||||
height: 300px;
|
||||
padding: 10px 0 50px;
|
||||
}
|
||||
|
||||
.chartLabel {
|
||||
display: inline-block;
|
||||
padding-left: 60px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.linkText {
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
background-color: #eff8ff;
|
||||
border: 1px;
|
||||
}
|
||||
148
web/src/components/api-service/chat-overview-modal/index.tsx
Normal file
148
web/src/components/api-service/chat-overview-modal/index.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import LineChart from '@/components/line-chart';
|
||||
import { useFetchNextStats } from '@/hooks/chat-hooks';
|
||||
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IStats } from '@/interfaces/database/chat';
|
||||
import { formatDate } from '@/utils/date';
|
||||
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 ChatApiKeyModal from '../chat-api-key-modal';
|
||||
import EmbedModal from '../embed-modal';
|
||||
import {
|
||||
usePreviewChat,
|
||||
useSelectChartStatsList,
|
||||
useShowEmbedModal,
|
||||
} from '../hooks';
|
||||
import styles from './index.less';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const StatsLineChart = ({ statsType }: { statsType: keyof IStats }) => {
|
||||
const { t } = useTranslate('chat');
|
||||
const chartList = useSelectChartStatsList();
|
||||
const list =
|
||||
chartList[statsType]?.map((x) => ({
|
||||
...x,
|
||||
xAxis: formatDate(x.xAxis),
|
||||
})) ?? [];
|
||||
|
||||
return (
|
||||
<div className={styles.chartItem}>
|
||||
<b className={styles.chartLabel}>{t(camelCase(statsType))}</b>
|
||||
<LineChart data={list}></LineChart>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatOverviewModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
id,
|
||||
name = '',
|
||||
idKey,
|
||||
}: IModalProps<any> & { id: string; name?: string; idKey: string }) => {
|
||||
const { t } = useTranslate('chat');
|
||||
const {
|
||||
visible: apiKeyVisible,
|
||||
hideModal: hideApiKeyModal,
|
||||
showModal: showApiKeyModal,
|
||||
} = useSetModalState();
|
||||
const {
|
||||
embedVisible,
|
||||
hideEmbedModal,
|
||||
showEmbedModal,
|
||||
embedToken,
|
||||
errorContextHolder,
|
||||
} = useShowEmbedModal(id, idKey);
|
||||
|
||||
const { pickerValue, setPickerValue } = useFetchNextStats();
|
||||
|
||||
const disabledDate: RangePickerProps['disabledDate'] = (current) => {
|
||||
return current && current > dayjs().endOf('day');
|
||||
};
|
||||
|
||||
const { handlePreview, contextHolder } = usePreviewChat(id, idKey);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={t('overview')}
|
||||
open={visible}
|
||||
onCancel={hideModal}
|
||||
cancelButtonProps={{ style: { display: 'none' } }}
|
||||
onOk={hideModal}
|
||||
width={'100vw'}
|
||||
okText={t('close', { keyPrefix: 'common' })}
|
||||
>
|
||||
<Flex vertical gap={'middle'}>
|
||||
<Card title={t('backendServiceApi')}>
|
||||
<Flex gap={8} vertical>
|
||||
{t('serviceApiEndpoint')}
|
||||
<Paragraph copyable className={styles.linkText}>
|
||||
{location.origin}
|
||||
/v1/api/
|
||||
</Paragraph>
|
||||
</Flex>
|
||||
<Space size={'middle'}>
|
||||
<Button onClick={showApiKeyModal}>{t('apiKey')}</Button>
|
||||
<a
|
||||
href={
|
||||
'https://github.com/infiniflow/ragflow/blob/main/docs/references/api.md'
|
||||
}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Button>{t('apiReference')}</Button>
|
||||
</a>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card title={`${name} Web App`}>
|
||||
<Flex gap={8} vertical>
|
||||
<Space size={'middle'}>
|
||||
<Button onClick={handlePreview}>{t('preview')}</Button>
|
||||
<Button onClick={showEmbedModal}>{t('embedded')}</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
<Space>
|
||||
<b>{t('dateRange')}</b>
|
||||
<RangePicker
|
||||
disabledDate={disabledDate}
|
||||
value={pickerValue}
|
||||
onChange={setPickerValue}
|
||||
allowClear={false}
|
||||
/>
|
||||
</Space>
|
||||
<div className={styles.chartWrapper}>
|
||||
<StatsLineChart statsType={'pv'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'round'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'speed'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'thumb_up'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'tokens'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'uv'}></StatsLineChart>
|
||||
</div>
|
||||
</Flex>
|
||||
{apiKeyVisible && (
|
||||
<ChatApiKeyModal
|
||||
hideModal={hideApiKeyModal}
|
||||
dialogId={id}
|
||||
idKey={idKey}
|
||||
></ChatApiKeyModal>
|
||||
)}
|
||||
<EmbedModal
|
||||
token={embedToken}
|
||||
visible={embedVisible}
|
||||
hideModal={hideEmbedModal}
|
||||
></EmbedModal>
|
||||
{contextHolder}
|
||||
{errorContextHolder}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatOverviewModal;
|
||||
8
web/src/components/api-service/embed-modal/index.less
Normal file
8
web/src/components/api-service/embed-modal/index.less
Normal file
@ -0,0 +1,8 @@
|
||||
.codeCard {
|
||||
.clearCardBody();
|
||||
}
|
||||
|
||||
.codeText {
|
||||
padding: 10px;
|
||||
background-color: #e8e8ea;
|
||||
}
|
||||
70
web/src/components/api-service/embed-modal/index.tsx
Normal file
70
web/src/components/api-service/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/common-hooks';
|
||||
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="${location.origin}/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;
|
||||
151
web/src/components/api-service/hooks.ts
Normal file
151
web/src/components/api-service/hooks.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import {
|
||||
useCreateNextToken,
|
||||
useFetchNextStats,
|
||||
useFetchTokenList,
|
||||
useRemoveNextToken,
|
||||
} from '@/hooks/chat-hooks';
|
||||
import {
|
||||
useSetModalState,
|
||||
useShowDeleteConfirm,
|
||||
useTranslate,
|
||||
} from '@/hooks/common-hooks';
|
||||
import { IStats } from '@/interfaces/database/chat';
|
||||
import { message } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useOperateApiKey = (dialogId: string, idKey: string) => {
|
||||
const { removeToken } = useRemoveNextToken();
|
||||
const { createToken, loading: creatingLoading } = useCreateNextToken();
|
||||
const { data: tokenList, loading: listLoading } = useFetchTokenList({
|
||||
[idKey]: dialogId,
|
||||
});
|
||||
|
||||
const showDeleteConfirm = useShowDeleteConfirm();
|
||||
|
||||
const onRemoveToken = (token: string, tenantId: string) => {
|
||||
showDeleteConfirm({
|
||||
onOk: () => removeToken({ dialogId, tokens: [token], tenantId }),
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateToken = useCallback(() => {
|
||||
createToken({ [idKey]: dialogId });
|
||||
}, [createToken, idKey, dialogId]);
|
||||
|
||||
return {
|
||||
removeToken: onRemoveToken,
|
||||
createToken: onCreateToken,
|
||||
tokenList,
|
||||
creatingLoading,
|
||||
listLoading,
|
||||
};
|
||||
};
|
||||
|
||||
type ChartStatsType = {
|
||||
[k in keyof IStats]: Array<{ xAxis: string; yAxis: number }>;
|
||||
};
|
||||
|
||||
export const useSelectChartStatsList = (): ChartStatsType => {
|
||||
const { data: stats } = useFetchNextStats();
|
||||
|
||||
return Object.keys(stats).reduce((pre, cur) => {
|
||||
const item = stats[cur as keyof IStats];
|
||||
if (item.length > 0) {
|
||||
pre[cur as keyof IStats] = item.map((x) => ({
|
||||
xAxis: x[0] as string,
|
||||
yAxis: x[1] as number,
|
||||
}));
|
||||
}
|
||||
return pre;
|
||||
}, {} 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, idKey: string) => {
|
||||
const { showTokenEmptyError, contextHolder } = useShowTokenEmptyError();
|
||||
|
||||
const { data: tokenList, refetch } = useFetchTokenList({ [idKey]: dialogId });
|
||||
|
||||
const token =
|
||||
Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : '';
|
||||
|
||||
const handleOperate = useCallback(async () => {
|
||||
const ret = await refetch();
|
||||
const list = ret.data;
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
return list[0]?.token;
|
||||
} else {
|
||||
showTokenEmptyError();
|
||||
return false;
|
||||
}
|
||||
}, [showTokenEmptyError, refetch]);
|
||||
|
||||
return {
|
||||
token,
|
||||
contextHolder,
|
||||
handleOperate,
|
||||
};
|
||||
};
|
||||
|
||||
export const useShowEmbedModal = (dialogId: string, idKey: string) => {
|
||||
const {
|
||||
visible: embedVisible,
|
||||
hideModal: hideEmbedModal,
|
||||
showModal: showEmbedModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const { handleOperate, token, contextHolder } =
|
||||
useFetchTokenListBeforeOtherStep(dialogId, idKey);
|
||||
|
||||
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, idKey: string) => {
|
||||
const { handleOperate, contextHolder } = useFetchTokenListBeforeOtherStep(
|
||||
dialogId,
|
||||
idKey,
|
||||
);
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user